Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/02-标题摘要初筛开发计划.md
HaHafeng 66255368b7 feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1):
- Database: Add user_modules table for fine-grained module permissions
- Database: Add 4 user permissions (view/create/edit/delete) to role_permissions
- Backend: UserService (780 lines) - CRUD with tenant isolation
- Backend: UserController + UserRoutes (648 lines) - 13 API endpoints
- Backend: Batch import users from Excel
- Frontend: UserListPage (412 lines) - list/filter/search/pagination
- Frontend: UserFormPage (341 lines) - create/edit with module config
- Frontend: UserDetailPage (393 lines) - details/tenant/module management
- Frontend: 3 modal components (592 lines) - import/assign/configure
- API: GET/POST/PUT/DELETE /api/admin/users/* endpoints

Architecture Upgrade - Module Permission System:
- Backend: Add getUserModules() method in auth.service
- Backend: Login API returns modules array in user object
- Frontend: AuthContext adds hasModule() method
- Frontend: Navigation filters modules based on user.modules
- Frontend: RouteGuard checks requiredModule instead of requiredVersion
- Frontend: Remove deprecated version-based permission system
- UX: Only show accessible modules in navigation (clean UI)
- UX: Smart redirect after login (avoid 403 for regular users)

Fixes:
- Fix UTF-8 encoding corruption in ~100 docs files
- Fix pageSize type conversion in userService (String to Number)
- Fix authUser undefined error in TopNavigation
- Fix login redirect logic with role-based access check
- Update Git commit guidelines v1.2 with UTF-8 safety rules

Database Changes:
- CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled)
- ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code)
- INSERT 4 permissions + role assignments
- UPDATE PUBLIC tenant with 8 module subscriptions

Technical:
- Backend: 5 new files (~2400 lines)
- Frontend: 10 new files (~2500 lines)
- Docs: 1 development record + 2 status updates + 1 guideline update
- Total: ~4900 lines of code

Status: User management 100% complete, module permission system operational
2026-01-16 13:42:10 +08:00

30 KiB
Raw Permalink Blame History

标题摘要初筛模块 - 详细开发计划MVP阶段

文档版本: V3.0
创建日期: 2025-11-16
开发周期: 4 周
负责团队: ASL 开发组
最后更新: 2025-11-16
重要基于真实架构Frontend-v2 + Backend增量演进 + asl_schema


📋 模块概述

标题摘要初筛是 ASL 模块的第一个核心功能,也是 MVP 阶段的唯一交付功能。

功能范围

  1. 设置与启动视图PICO 标准展示、Excel 文献导入、启动筛选任务
  2. 审核工作台视图:双模型判断对比、冲突标记、人工复核
  3. 初筛结果视图统计概览、PRISMA 排除总结、结果导出

技术栈

层级 技术 说明
前端 React 19 + TypeScript + Ant Design 5 + xlsx Frontend-v2架构
后端 Node.js + Fastify + TypeScript + Prisma Backend/modules/asl/
LLM DeepSeek-V3 + Qwen3-72B 复用 common/llm/adapters/
数据库 PostgreSQL 15 (asl_schema) Schema隔离

🏗️ 架构前提(已完成)

Frontend-v2 架构Week 2 Day 6-7 完成)

frontend-v2/src/
├── framework/layout/
│   ├── MainLayout.tsx          # ✅ 顶部导航布局
│   └── TopNavigation.tsx       # ✅ 6个模块导航
├── framework/modules/
│   ├── moduleRegistry.ts       # ✅ 模块注册中心
│   └── types.ts                # ✅ ModuleDefinition接口
└── modules/asl/
    └── index.tsx               # 🚧 占位页面(待替换)

Backend 架构Week 2 Day 8-9 完成)

backend/src/
├── common/llm/adapters/        # ✅ LLMFactory可复用
├── common/utils/jsonParser.js  # ✅ JSON解析可复用
└── modules/
    └── asl/                    # 🚧 空目录(待创建)

Database SchemaWeek 1 完成)

// backend/prisma/schema.prisma
datasource db {
  schemas = [
    "asl_schema",  # ✅ 已预留,待定义表结构
    // ...其他9个Schema
  ]
}

🌥️ 云原生开发注意事项2025-11-16 新增)

重要更新:本模块开发需遵循阿里云 Serverless 部署架构要求
详细规范云原生开发规范
部署指南云原生部署架构指南

🎯 本地开发 + 云端部署双兼容策略

环境 存储方式 配置 说明
本地开发 LocalAdapter STORAGE_TYPE=local 文件存储到 ./uploads/
生产环境 OSSAdapter STORAGE_TYPE=oss 文件存储到阿里云 OSS

核心原则

  • Excel导入:内存解析(xlsx.read(buffer)),不落盘
  • PDF上传V1.0):使用 StorageFactory,本地/OSS自动切换
  • 异步任务LLM筛选任务必须异步处理> 10秒任务
  • 环境变量:所有配置从 .env 读取
  • 数据库连接池:使用全局 prisma 实例,不新建连接

禁止的做法

禁止操作 正确做法 原因
fs.writeFileSync('./temp.xlsx') xlsx.read(buffer) 内存解析 Serverless容器重启丢失文件
new PrismaClient() 每次新建连接 使用全局 prisma 实例 避免连接数暴增
硬编码 apiKey = 'sk-xxx' process.env.LLM_API_KEY 配置管理混乱
同步处理1000条文献筛选 异步任务 + 进度轮询 超过30秒超时限制

MVP阶段开发检查清单

在提交代码前,请确认:

  • Excel导入是否使用内存解析xlsx.read(buffer)
  • 是否使用全局 prisma 实例(import { prisma } from '@/config/database'
  • 是否所有配置都从环境变量读取?
  • LLM筛选任务是否异步处理POST /screening/start 立即返回taskId
  • 是否预留了 OSS 字段(pdfUrl, pdfOssKey, pdfFileSize
  • 是否使用存储抽象层(StorageFactory.create()

预留字段说明

  • MVP阶段仅做标题摘要筛选不处理PDF
  • V1.0阶段实现全文PDF筛选时使用预留的OSS字段

📅 四周开发计划

Week 1: 数据库Schema + 后端API框架 + 存储抽象层
Week 2: LLM筛选核心 + 异步批处理逻辑
Week 3: 前端模块开发 + 审核工作台内存解析Excel
Week 4: 结果展示 + 导出 + 集成测试

🗓️ Week 1: 数据库Schema与后端API框架

Day 1: Prisma Schema 设计

任务1: 设计 asl_schema 表结构

backend/prisma/schema.prisma 中添加:

// ==================== ASL 筛选项目表 ====================
model AslScreeningProject {
  id              String   @id @default(uuid())
  userId          String   @map("user_id")
  user            User     @relation("AslProjects", fields: [userId], references: [id], onDelete: Cascade)
  
  projectName     String   @map("project_name")
  
  // PICO标准
  picoCriteria    Json     @map("pico_criteria") // { population, intervention, comparison, outcome, studyDesign }
  
  // 筛选标准
  inclusionCriteria String @map("inclusion_criteria") @db.Text
  exclusionCriteria String @map("exclusion_criteria") @db.Text
  
  // 状态
  status          String   @default("draft") // draft, screening, completed
  
  // 筛选配置
  screeningConfig Json?    @map("screening_config") // { models: ["deepseek", "qwen"], temperature: 0 }
  
  // 关联
  literatures     AslLiterature[]
  screeningTasks  AslScreeningTask[]
  screeningResults AslScreeningResult[]
  
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  
  @@map("screening_projects")
  @@schema("asl_schema")
  @@index([userId])
  @@index([status])
}

// ==================== ASL 文献条目表 ====================
model AslLiterature {
  id              String   @id @default(uuid())
  projectId       String   @map("project_id")
  project         AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
  
  // 文献基本信息
  pmid            String?
  title           String   @db.Text
  abstract        String   @db.Text
  authors         String?
  journal         String?
  publicationYear Int?     @map("publication_year")
  doi             String?
  
  // 云原生存储字段V1.0 阶段使用MVP阶段预留
  pdfUrl          String?  @map("pdf_url")        // PDF访问URL
  pdfOssKey       String?  @map("pdf_oss_key")    // OSS存储Key用于删除
  pdfFileSize     Int?     @map("pdf_file_size")  // 文件大小(字节)
  
  // 关联
  screeningResults AslScreeningResult[]
  
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  
  @@map("literatures")
  @@schema("asl_schema")
  @@index([projectId])
  @@index([doi])
  @@unique([projectId, pmid])
}

// ==================== ASL 筛选结果表 ====================
model AslScreeningResult {
  id              String   @id @default(uuid())
  projectId       String   @map("project_id")
  project         AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
  literatureId    String   @map("literature_id")
  literature      AslLiterature @relation(fields: [literatureId], references: [id], onDelete: Cascade)
  
  // DeepSeek模型判断
  dsModelName     String   @map("ds_model_name") // "deepseek-chat"
  dsPJudgment     String?  @map("ds_p_judgment") // "match" | "partial" | "mismatch"
  dsIJudgment     String?  @map("ds_i_judgment")
  dsCJudgment     String?  @map("ds_c_judgment")
  dsSJudgment     String?  @map("ds_s_judgment")
  dsConclusion    String?  @map("ds_conclusion") // "include" | "exclude" | "uncertain"
  dsConfidence    Float?   @map("ds_confidence") // 0-1
  
  // DeepSeek模型证据
  dsPEvidence     String?  @map("ds_p_evidence") @db.Text
  dsIEvidence     String?  @map("ds_i_evidence") @db.Text
  dsCEvidence     String?  @map("ds_c_evidence") @db.Text
  dsSEvidence     String?  @map("ds_s_evidence") @db.Text
  dsReason        String?  @map("ds_reason") @db.Text
  
  // Qwen模型判断
  qwenModelName   String   @map("qwen_model_name") // "qwen-max"
  qwenPJudgment   String?  @map("qwen_p_judgment")
  qwenIJudgment   String?  @map("qwen_i_judgment")
  qwenCJudgment   String?  @map("qwen_c_judgment")
  qwenSJudgment   String?  @map("qwen_s_judgment")
  qwenConclusion  String?  @map("qwen_conclusion")
  qwenConfidence  Float?   @map("qwen_confidence")
  
  // Qwen模型证据
  qwenPEvidence   String?  @map("qwen_p_evidence") @db.Text
  qwenIEvidence   String?  @map("qwen_i_evidence") @db.Text
  qwenCEvidence   String?  @map("qwen_c_evidence") @db.Text
  qwenSEvidence   String?  @map("qwen_s_evidence") @db.Text
  qwenReason      String?  @map("qwen_reason") @db.Text
  
  // 冲突状态
  conflictStatus  String   @default("none") @map("conflict_status") // "none" | "conflict" | "resolved"
  conflictFields  Json?    @map("conflict_fields") // ["P", "I", "conclusion"]
  
  // 最终决策
  finalDecision   String?  @map("final_decision") // "include" | "exclude" | "pending"
  finalDecisionBy String?  @map("final_decision_by") // userId
  finalDecisionAt DateTime? @map("final_decision_at")
  exclusionReason String?  @map("exclusion_reason") @db.Text
  
  // AI处理状态
  aiProcessingStatus String @default("pending") @map("ai_processing_status") // "pending" | "processing" | "completed" | "failed"
  aiProcessedAt   DateTime? @map("ai_processed_at")
  aiErrorMessage  String?  @map("ai_error_message") @db.Text
  
  // 可追溯信息
  promptVersion   String   @default("v1.0.0") @map("prompt_version")
  rawOutput       Json?    @map("raw_output") // 原始LLM输出备份
  
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  
  @@map("screening_results")
  @@schema("asl_schema")
  @@index([projectId])
  @@index([literatureId])
  @@index([conflictStatus])
  @@index([finalDecision])
  @@unique([projectId, literatureId])
}

// ==================== ASL 筛选任务表 ====================
model AslScreeningTask {
  id              String   @id @default(uuid())
  projectId       String   @map("project_id")
  project         AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
  
  taskType        String   @map("task_type") // "title_abstract" | "full_text"
  status          String   @default("pending") // "pending" | "running" | "completed" | "failed"
  
  // 进度统计
  totalItems      Int      @map("total_items")
  processedItems  Int      @default(0) @map("processed_items")
  successItems    Int      @default(0) @map("success_items")
  failedItems     Int      @default(0) @map("failed_items")
  conflictItems   Int      @default(0) @map("conflict_items")
  
  // 时间信息
  startedAt       DateTime? @map("started_at")
  completedAt     DateTime? @map("completed_at")
  estimatedEndAt  DateTime? @map("estimated_end_at")
  
  // 错误信息
  errorMessage    String?  @map("error_message") @db.Text
  
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  
  @@map("screening_tasks")
  @@schema("asl_schema")
  @@index([projectId])
  @@index([status])
}

// ==================== 用户表关联添加到User模型====================
// 在 platform_schema 的 User 模型中添加:
// aslProjects AslScreeningProject[] @relation("AslProjects")

执行迁移:

cd backend
npx prisma migrate dev --name add_asl_screening_tables
npx prisma generate

验收标准

  • 数据库表创建成功4张表
  • Prisma Client 生成成功
  • 可查询 asl_schema 表

Day 2: 后端目录结构创建

前置条件2025-11-17 更新):平台基础设施已完成实施
完成状态8个核心模块100%测试通过
完成报告平台基础设施实施完成报告
使用指南backend/src/common/README.md

平台已提供的8个核心模块无需ASL模块实现

平台基础设施路径backend/src/common/

# 模块 使用方式 功能说明
1 存储服务 import { storage } from '@/common/storage' 文件上传下载(本地/OSS切换
2 日志系统 import { logger } from '@/common/logging' 结构化JSON日志
3 缓存服务 import { cache } from '@/common/cache' 内存/Redis缓存
4 异步任务 import { jobQueue } from '@/common/jobs' 长时间任务处理
5 健康检查 import { registerHealthRoutes } from '@/common/health' SAE健康检查
6 监控指标 import { Metrics } from '@/common/monitoring' 性能监控和告警
7 数据库连接池 import { prisma } from '@/config/database' 全局Prisma实例
8 环境配置 import { env } from '@/config/env' 统一配置管理

存储服务使用示例

// ASL模块直接使用一行代码
import { storage } from '@/common/storage'

// 上传文件不关心本地还是OSS
const url = await storage.upload('asl/literature/123.pdf', pdfBuffer)

// 下载文件
const buffer = await storage.download('asl/literature/123.pdf')

// 删除文件
await storage.delete('asl/literature/123.pdf')

支持的部署环境

  • 本地开发LocalAdapter文件存储到 ./uploads/
  • 云端SaaSOSSAdapter文件存储到阿里云OSS
  • 私有化部署LocalAdapter文件存储到服务器
  • 单机版LocalAdapter文件存储到用户本地

环境切换:修改一个环境变量即可

# 本地开发
STORAGE_TYPE=local

# 生产环境
STORAGE_TYPE=oss

核心优势

  • ASL模块无需关心基础设施实现细节
  • 代码零改动切换环境(本地 ↔ 云端)
  • 所有业务模块AIA/PKB/DC等复用同一套基础设施
  • 统一维护、统一升级、统一监控

任务1: 创建 backend/src/modules/asl/ 目录

cd backend/src/modules/asl
mkdir routes controllers services schemas types utils
touch routes/index.ts
touch controllers/projectController.ts
touch controllers/literatureController.ts
touch controllers/screeningController.ts
touch services/projectService.ts
touch services/literatureService.ts
touch services/llmScreeningService.ts
touch schemas/screening.schema.ts
touch types/screening.types.ts

任务2: 创建路由文件

backend/src/modules/asl/routes/index.ts

import { FastifyInstance } from 'fastify'
import * as projectController from '../controllers/projectController.js'
import * as literatureController from '../controllers/literatureController.js'
import * as screeningController from '../controllers/screeningController.js'

/**
 * ASL 模块路由注册
 * 
 * @description
 * - 注册到 /api/v1/asl 前缀
 * - 参考 legacy/routes/ 的风格
 * 
 * @version Week 3 Day 2
 */
export async function aslRoutes(fastify: FastifyInstance) {
  // 项目管理
  fastify.post('/projects', projectController.createProject)
  fastify.get('/projects', projectController.listProjects)
  fastify.get('/projects/:projectId', projectController.getProject)
  fastify.put('/projects/:projectId', projectController.updateProject)
  fastify.delete('/projects/:projectId', projectController.deleteProject)
  
  // 文献管理
  fastify.post('/projects/:projectId/literatures/import', literatureController.importLiteratures)
  fastify.get('/projects/:projectId/literatures', literatureController.listLiteratures)
  
  // 筛选管理
  fastify.post('/projects/:projectId/screening/start', screeningController.startScreening)
  fastify.get('/projects/:projectId/screening/results', screeningController.getScreeningResults)
  fastify.put('/screening/results/:resultId', screeningController.updateScreeningResult)
  fastify.post('/screening/results/batch-update', screeningController.batchUpdateResults)
  fastify.get('/screening/tasks/:taskId', screeningController.getTaskStatus)
  fastify.get('/screening/tasks/:taskId/progress', screeningController.getTaskProgress)
}

验收标准

  • 目录结构清晰
  • 路由文件创建完成
  • 可正常使用平台服务
    • import { storage } from '@/common/storage' 可用
    • import { logger } from '@/common/logging' 可用
    • import { prisma } from '@/config/database' 可用
    • import { jobQueue } from '@/common/jobs' 可用
    • import { cache } from '@/common/cache' 可用

Day 3: 在 index.ts 中注册ASL路由

backend/src/index.ts(修改):

// ============================================
// 【新架构】ASL 模块 - Week 3 新增
// ============================================
import { aslRoutes } from './modules/asl/routes/index.js';

// ... 其他代码

// 注册 ASL 模块路由
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });

console.log('✅ ASL 路由已注册到 /api/v1/asl/*');

验收标准

  • 后端启动成功
  • 访问 http://localhost:3001/api/v1/asl/projects 返回 200即使是空列表

🗓️ Week 2: LLM筛选核心

Day 4-5: LLM筛选服务实现

任务1: 定义 JSON Schema

backend/src/modules/asl/schemas/screening.schema.ts

export const screeningOutputSchema = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["decision", "reason", "confidence", "pico"],
  "properties": {
    "decision": {
      "type": "string",
      "enum": ["include", "exclude", "uncertain"]
    },
    "reason": {
      "type": "string",
      "minLength": 10,
      "maxLength": 500
    },
    "confidence": {
      "type": "number",
      "minimum": 0,
      "maximum": 1
    },
    "pico": {
      "type": "object",
      "required": ["population", "intervention", "comparison", "outcome"],
      "properties": {
        "population": {
          "type": "string",
          "enum": ["match", "partial", "mismatch"]
        },
        "intervention": {
          "type": "string",
          "enum": ["match", "partial", "mismatch"]
        },
        "comparison": {
          "type": "string",
          "enum": ["match", "partial", "mismatch", "not_applicable"]
        },
        "outcome": {
          "type": "string",
          "enum": ["match", "partial", "mismatch"]
        }
      }
    },
    "studyDesign": {
      "type": "string",
      "enum": ["RCT", "cohort", "case-control", "cross-sectional", "review", "other"]
    },
    "evidences": {
      "type": "object",
      "properties": {
        "population": { "type": "string" },
        "intervention": { "type": "string" },
        "comparison": { "type": "string" },
        "outcome": { "type": "string" }
      }
    }
  }
};

任务2: 创建提示词模板

backend/prompts/asl/screening/v1.0.0-basic.txt

你是一位医学文献筛选专家。请根据以下 PICO 标准判断这篇文献是否应该纳入系统评价。

# PICO 标准
- **Population (研究对象)**: {{population}}
- **Intervention (干预措施)**: {{intervention}}
- **Comparison (对照措施)**: {{comparison}}
- **Outcome (结局指标)**: {{outcome}}
- **Study Design (研究设计)**: {{studyDesign}}

# 纳入标准
{{inclusionCriteria}}

# 排除标准
{{exclusionCriteria}}

# 待筛选文献
**标题**: {{title}}

**摘要**: {{abstract}}

# 输出要求

请严格按照以下 JSON Schema 输出结果输出纯JSON不要包含任何其他文字

{
  "decision": "include/exclude/uncertain",
  "reason": "判断理由10-500字",
  "confidence": 0.95,
  "pico": {
    "population": "match/partial/mismatch",
    "intervention": "match/partial/mismatch",
    "comparison": "match/partial/mismatch/not_applicable",
    "outcome": "match/partial/mismatch"
  },
  "studyDesign": "RCT/cohort/case-control/cross-sectional/review/other",
  "evidences": {
    "population": "原文中的关键证据短语",
    "intervention": "原文中的关键证据短语",
    "comparison": "原文中的关键证据短语",
    "outcome": "原文中的关键证据短语"
  }
}

# 注意事项
1. decision 只能是 "include"(纳入)、"exclude"(排除)或 "uncertain"(不确定)
2. reason 必须具体说明判断依据
3. confidence 为 0-1 之间的数值
4. pico 字段逐项评估匹配程度
5. evidences 字段提取原文中的关键短语作为证据

任务3: 实现 LLM 筛选服务

backend/src/modules/asl/services/llmScreeningService.ts

import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { parseJSON } from '../../../common/utils/jsonParser.js';
import Ajv from 'ajv';
import { screeningOutputSchema } from '../schemas/screening.schema.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const ajv = new Ajv();
const validateSchema = ajv.compile(screeningOutputSchema);

/**
 * LLM 筛选服务
 * 
 * @description
 * - 复用 common/llm/adapters/LLMFactory.ts
 * - 双模型并行调用DeepSeek + Qwen
 * - JSON Schema 验证
 * - 冲突检测
 * 
 * @version Week 3 Day 4-5
 */
class LLMScreeningService {
  /**
   * 双模型并行筛选
   */
  async dualModelScreening(literature: any, protocol: any) {
    // 构建提示词
    const prompt = this.buildPrompt(literature, protocol);

    // 并行调用两个模型
    const [resultA, resultB] = await Promise.all([
      this.callModel('deepseek', prompt),
      this.callModel('qwen', prompt)
    ]);

    // 解析JSON结果
    const decisionA = await this.parseModelOutput(resultA.content, 'deepseek');
    const decisionB = await this.parseModelOutput(resultB.content, 'qwen');

    // 一致性判断
    const { consensus, conflictFields } = this.compareDecisions(decisionA, decisionB);

    // 自动分流
    const needReview = this.shouldReview(consensus, decisionA, decisionB);

    return {
      consensus,
      finalDecision: consensus === 'high' ? decisionA.decision : 'uncertain',
      needReview,
      conflictFields,
      modelA: decisionA,
      modelB: decisionB
    };
  }

  /**
   * 调用LLM模型复用common/llm
   */
  private async callModel(modelName: string, prompt: string) {
    const llm = LLMFactory.createLLM(modelName);
    
    const response = await llm.chat({
      messages: [
        { role: 'user', content: prompt }
      ],
      temperature: 0,  // 确定性输出
      max_tokens: 1000
    });

    return response;
  }

  /**
   * 构建提示词
   */
  private buildPrompt(literature: any, protocol: any): string {
    // 读取提示词模板
    const templatePath = path.resolve(__dirname, '../../../../prompts/asl/screening/v1.0.0-basic.txt');
    let template = fs.readFileSync(templatePath, 'utf-8');

    // 替换变量
    template = template.replace('{{population}}', protocol.picoCriteria.population);
    template = template.replace('{{intervention}}', protocol.picoCriteria.intervention);
    template = template.replace('{{comparison}}', protocol.picoCriteria.comparison);
    template = template.replace('{{outcome}}', protocol.picoCriteria.outcome);
    template = template.replace('{{studyDesign}}', protocol.picoCriteria.studyDesign);
    template = template.replace('{{inclusionCriteria}}', protocol.inclusionCriteria);
    template = template.replace('{{exclusionCriteria}}', protocol.exclusionCriteria);
    template = template.replace('{{title}}', literature.title);
    template = template.replace('{{abstract}}', literature.abstract);

    return template;
  }

  /**
   * 解析模型输出
   */
  private async parseModelOutput(content: string, modelName: string) {
    // 使用JSON解析器复用common/utils
    const parsed = parseJSON(content);

    // JSON Schema 验证
    const valid = validateSchema(parsed);
    if (!valid) {
      console.error('JSON Schema验证失败', validateSchema.errors);
      throw new Error(`模型${modelName}输出格式不符合Schema`);
    }

    return {
      modelName,
      decision: parsed.decision,
      reason: parsed.reason,
      confidence: parsed.confidence,
      pico: parsed.pico,
      evidences: parsed.evidences,
      studyDesign: parsed.studyDesign
    };
  }

  /**
   * 对比两个模型的决策
   */
  private compareDecisions(decisionA: any, decisionB: any) {
    const conflicts: string[] = [];

    // 比较最终决策
    if (decisionA.decision !== decisionB.decision) {
      conflicts.push('decision');
    }

    // 比较PICO各维度
    if (decisionA.pico.population !== decisionB.pico.population) conflicts.push('P');
    if (decisionA.pico.intervention !== decisionB.pico.intervention) conflicts.push('I');
    if (decisionA.pico.comparison !== decisionB.pico.comparison) conflicts.push('C');
    if (decisionA.pico.outcome !== decisionB.pico.outcome) conflicts.push('O');

    const consensus = conflicts.length === 0 ? 'high' : 'conflict';

    return { consensus, conflictFields: conflicts };
  }

  /**
   * 自动分流规则
   */
  private shouldReview(consensus: string, decisionA: any, decisionB: any): boolean {
    // 规则1冲突 → 必须复核
    if (consensus === 'conflict') {
      return true;
    }

    // 规则2低置信度 → 需要复核
    const avgConfidence = (decisionA.confidence + decisionB.confidence) / 2;
    if (avgConfidence < 0.7) {
      return true;
    }

    // 规则3高置信度 + 一致 → 自动通过
    return false;
  }

  /**
   * 批量筛选
   */
  async batchScreening(literatures: any[], protocol: any, progressCallback?: (progress: number) => void) {
    const batchSize = 15;  // 每批15篇
    const results = [];

    for (let i = 0; i < literatures.length; i += batchSize) {
      const batch = literatures.slice(i, i + batchSize);

      // 并行处理当前批次
      const batchResults = await Promise.all(
        batch.map(lit => this.dualModelScreening(lit, protocol))
      );

      results.push(...batchResults);

      // 推送进度
      const progress = Math.round(((i + batch.length) / literatures.length) * 100);
      progressCallback?.(progress);
    }

    return results;
  }
}

export const llmScreeningService = new LLMScreeningService();

验收标准

  • LLM双模型调用成功
  • JSON Schema验证通过率 > 95%
  • 冲突检测准确

🗓️ Week 3: 前端模块开发

Day 6-7: 前端模块结构创建

任务1: 更新模块定义

frontend-v2/src/modules/asl/index.tsx(修改):

import { lazy } from 'react'
import { ModuleDefinition } from '@/framework/modules/types'
import { FileSearchOutlined } from '@ant-design/icons'

/**
 * ASL 模块定义
 * 
 * @description
 * - 移除占位标记
 * - 实现真实模块路由
 * 
 * @version Week 3 Day 6
 */
const ASLModule: ModuleDefinition = {
  id: 'literature-platform',
  name: 'AI智能文献',
  path: '/literature',
  icon: FileSearchOutlined,
  component: lazy(() => import('./routes')),
  placeholder: false,  // ✅ 改为 false
  requiredVersion: 'advanced',
  description: 'AI驱动的文献筛选和分析系统',
}

export default ASLModule

任务2: 创建目录结构

cd frontend-v2/src/modules/asl
mkdir pages components api hooks types utils
touch routes.tsx
touch pages/ProjectList.tsx
touch pages/ScreeningSettings.tsx
touch pages/ScreeningWorkbench.tsx
touch pages/ScreeningResults.tsx
touch api/index.ts

任务3: 实现路由配置

frontend-v2/src/modules/asl/routes.tsx

import { lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'

const ProjectList = lazy(() => import('./pages/ProjectList'))
const ScreeningSettings = lazy(() => import('./pages/ScreeningSettings'))
const ScreeningWorkbench = lazy(() => import('./pages/ScreeningWorkbench'))
const ScreeningResults = lazy(() => import('./pages/ScreeningResults'))

/**
 * ASL 模块路由
 * 
 * @description
 * - /literature - 项目列表
 * - /literature/project/:id/settings - 设置与启动
 * - /literature/project/:id/workbench - 审核工作台
 * - /literature/project/:id/results - 初筛结果
 * 
 * @version Week 3 Day 6
 */
export default function ASLRoutes() {
  return (
    <Routes>
      <Route index element={<ProjectList />} />
      <Route path="project/:projectId">
        <Route path="settings" element={<ScreeningSettings />} />
        <Route path="workbench" element={<ScreeningWorkbench />} />
        <Route path="results" element={<ScreeningResults />} />
      </Route>
    </Routes>
  )
}

验收标准

  • 顶部导航显示"AI智能文献"(不再是占位)
  • 点击后进入项目列表页(即使是空列表)

Day 8-10: 实现核心页面

(由于篇幅限制,核心实现代码请参考任务分解文档)

验收标准

  • Excel上传功能正常
  • 审核工作台可展示筛选结果
  • 双视图模态框可弹出

🗓️ Week 4: 集成测试与验收

Day 11-14: 端到端测试

(详细测试计划见任务分解文档)

验收标准

  • 完整流程:上传 → 筛选 → 复核 → 导出
  • 准确率 ≥ 85%
  • 性能达标100篇 < 10分钟

📚 相关文档


更新日志

  • 2025-11-18: V3.1 更新补充平台基础设施完成状态8个核心模块
  • 2025-11-16: V3.0 重写基于真实架构Frontend-v2 + Backend + asl_schema
  • 2025-11-16: V2.0 重写,详细到每天的任务和代码示例
  • 2025-10-29: V1.0 创建,初始版本