Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_MVP开发计划_V1.0.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

2062 lines
68 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工具C - 科研数据编辑器 MVP开发计划
> **文档版本:** V1.0 (务实快速验证版)
> **创建日期:** 2025-12-06
> **计划周期:** 3周15个工作日
> **核心策略:** 用最小成本验证核心假设,快速失败优于完美计划
> **状态:** 待启动
---
## ⚠️ 开发前必读:严格遵守现有架构与规范
### 🔴 不要重复造轮子!复用平台能力
**本项目已有完整的3层架构体系和平台基础设施所有代码必须复用现有能力**
#### 平台基础层(✅ 已完成,直接使用)
| 服务 | 导入方式 | 用途 | 文档 |
|------|---------|------|------|
| **存储服务** | `import { storage } from '@/common/storage'` | 文件上传下载 | ✅ 必须使用 |
| **日志系统** | `import { logger } from '@/common/logging'` | 标准化日志 | ✅ 必须使用 |
| **缓存服务** | `import { cache } from '@/common/cache'` | 分布式缓存 | ✅ 必须使用 |
| **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务 | ✅ 必须使用 |
| **数据库** | `import { prisma } from '@/config/database'` | 数据库操作 | ✅ 必须使用 |
| **LLM能力** | `import { LLMFactory } from '@/common/llm'` | LLM调用 | ✅ 必须使用 |
#### 云原生开发规范(✅ 强制执行)
**详细文档**`docs/04-开发规范/08-云原生开发规范.md`
**核心要求:**
-**文件存储**:使用`storage.upload()`,不要用`fs.writeFile()`
-**Session管理**:存数据库,不要用`Map<sessionId, data>`
-**日志输出**:使用`logger.info()`,不要用`console.log()`
-**数据库连接**:使用全局`prisma`实例,不要`new PrismaClient()`
-**LLM调用**:使用`LLMFactory.getLLM()`,不要自己集成
-**禁止本地文件存储**Excel直接从内存解析
-**禁止内存缓存Map**用数据库或cache服务
-**禁止硬编码配置**:使用环境变量
#### 代码文件夹结构(✅ 参考tool-b
**后端**
```
backend/src/modules/dc/tool-c/ ← 已存在
├── services/ ← 业务逻辑
├── controllers/ ← HTTP控制器
├── routes/ ← 路由定义
└── utils/ ← 工具函数
```
**前端**
```
frontend-v2/src/modules/dc/pages/tool-c/ ← 已存在
```
#### 常见错误示例(❌ 严禁)
```typescript
// ❌ 错误1自己实现Session管理
const sessions = new Map<string, any>(); // 违反规范!
// ❌ 错误2本地文件存储
fs.writeFileSync('./uploads/file.xlsx', buffer); // 违反规范!
// ❌ 错误3不用logger
console.log('User uploaded file'); // 违反规范!
// ❌ 错误4新建Prisma实例
const prisma = new PrismaClient(); // 违反规范!
// ❌ 错误5自己集成LLM
import Anthropic from '@anthropic-ai/sdk'; // 违反规范!
```
**正确示例(✅ 必须)**参考本文档Day 1-5的代码示例
---
## 📋 目录
- [开发前必读](#开发前必读严格遵守现有架构与规范)
- [一、MVP核心目标](#一mvp核心目标)
- [二、技术架构方案(务实版)](#二技术架构方案务实版)
- [三、功能优先级矩阵](#三功能优先级矩阵)
- [四、3周详细开发计划](#四3周详细开发计划)
- [五、风险应对策略](#五风险应对策略)
- [六、验收标准](#六验收标准)
- [七、快速失败机制](#七快速失败机制)
---
## 一、MVP核心目标
### 1.1 核心假设验证
**我们需要验证的3个核心假设**
| 假设 | 验证方式 | 成功标准 | 失败后果 |
|------|---------|---------|---------|
| **H1: AI能生成高质量Pandas代码并成功执行** | 15个真实场景测试 | 总体成功率 > 80% | MVP失败改用代码模板库 |
| **H2: Python代码执行环境稳定可靠** | 复杂场景测试 | 高级场景成功率 > 60% | 简化为批处理模式 |
| **H3: 左表格+右AI的交互模式好用** | 用户体验测试 | 用户能独立完成任务 | 重新设计UI交互 |
| **H4: 性能可接受含Python执行** | 性能测试 | 端到端 < 20秒 | 优化Python执行或改批处理 |
**⚠️ 核心价值主张:**
- ✅ **AI生成代码 + 真实执行 + 表格刷新**是工具C的差异化价值
- ✅ 如果只生成代码不执行用户还不如直接用ChatGPT
- ✅ Python执行环境是MVP的**技术核心**,不是可选项
### 1.2 MVP功能范围
**✅ MVP必须有的P0**
1. 文件上传10MB限制
2. 表格展示AG Grid100行预览
3. AI对话界面右侧侧边栏
4. AI代码生成DeepSeek-V3
5. 代码执行Python沙箱
6. UI锁定机制AI处理时表格只读
7. AST安全检查
8. 表格自动刷新
9. 导出Excel
**❌ MVP明确不做的**
- 手动编辑单元格专注AI能力
- 撤销/回滚(节省内存)
- Apache Arrow先用JSON
- Redis会话用进程内存
- 样式保留(直接覆盖)
- 操作审计日志
- 协同编辑
### 1.3 MVP交付物
**最终演示场景:**
```
用户上传 "lung_cancer_patients.xlsx" (5000行 × 20列)
左侧显示数据表格右侧AI助手准备就绪
用户对AI说"把年龄大于60的患者标记为老年组"
AI生成代码 → 展示预操作卡片 → 用户确认 → 执行成功
表格自动刷新,新增"age_group"列
用户继续说:"删除所有缺失患者ID的行"
AI执行 → 表格刷新显示删除了23行
用户点击"导出"下载处理后的Excel
```
**成功标准:**
- ✅ 整个流程 < 2分钟完成
- ✅ AI代码一次性执行成功
- ✅ 用户无需看文档就能操作
---
## 二、技术架构方案(务实版)
### 2.1 总体架构(简化版)- ⚠️ 重要:复用平台能力
```
┌─────────────────────────────────────────────────────────┐
│ 前端 (React) │
│ frontend-v2/src/modules/dc/pages/tool-c/ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ AG Grid (70%) │ │ AI Chat Sidebar (30%) │ │
│ │ • 展示100行数据 │ │ • 自然语言输入 │ │
│ │ • 只读模式切换 │ │ • 预操作卡片 │ │
│ │ • 锁定遮罩 │ │ • 消息历史 │ │
│ └──────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↕ REST API (JSON)
┌─────────────────────────────────────────────────────────┐
│ Node.js后端 (backend/src/modules/dc/tool-c/) │
│ • 文件上传复用storage服务✅ │
│ • 会话管理存数据库不用内存Map✅ │
│ • LLM集成复用LLMFactory✅ │
│ • 日志记录复用logger✅ │
│ • 异步任务复用jobQueue✅ │
└─────────────────────────────────────────────────────────┘
↕ HTTP如需Python执行
┌─────────────────────────────────────────────────────────┐
│ Python微服务可选扩展文档处理引擎
│ • DataFrame管理存数据库不用内存
│ • exec()代码执行 │
│ • AST静态安全检查 │
│ • JSON序列化返回 │
└─────────────────────────────────────────────────────────┘
```
**⚠️ 云原生架构强制要求:**
-**复用storage服务**`import { storage } from '@/common/storage'`
-**复用logger服务**`import { logger } from '@/common/logging'`
-**复用cache服务**`import { cache } from '@/common/cache'`
-**复用LLMFactory**`import { LLMFactory } from '@/common/llm'`
-**复用prisma实例**`import { prisma } from '@/config/database'`
-**Session存数据库**不用内存Map违反规范
-**禁止本地文件存储**Excel直接从内存解析
-**禁止新建Prisma实例**:使用全局实例
**关键决策:**
-**不用Apache Arrow**JSON够快100行 ≈ 20KB
-**Session存数据库**:符合云原生规范,支持多实例
-**不做撤销**:节省开发时间,用户重新上传即可
-**不保留样式**直接生成新Excel
### 2.2 技术栈选型(✅ 复用现有技术栈)
#### 前端frontend-v2
```json
{
"framework": "React 19 + TypeScript 5 (已有)",
"table": "AG Grid Community (免费版)",
"ui": "Ant Design 5 (已有)",
"state": "React Query v5 (已有)",
"http": "axios (已有)",
"routing": "React Router DOM v6 (已有)"
}
```
#### Node.js后端backend
```json
{
"framework": "Fastify v4 (已有)",
"llm": "✅ 复用 LLMFactory (平台通用能力层)",
"storage": "✅ 复用 storage服务 (平台基础层)",
"logging": "✅ 复用 logger服务 (平台基础层)",
"cache": "✅ 复用 cache服务 (平台基础层)",
"database": "✅ 复用 prisma全局实例 (平台基础层)",
"session": "✅ PostgreSQL (dc_tool_c_sessions表)",
"excel": "xlsx 库(内存解析)",
"validation": "Joi"
}
```
#### Python微服务 核心功能,扩展现有服务)⭐⭐⭐⭐⭐
**决策**:✅ **系统已有Python微服务FastAPI需扩展代码执行功能**
**📦 现有Python服务已完成**
-**extraction_service**FastAPI + PyMuPDF + Pandas + openpyxl
-**端口**8000已运行
-**功能**PDF/Docx/Txt文档提取、语言检测
-**集成**Node.js通过ExtractionClient调用
-**依赖**Pandas、openpyxl、chardet、langdetect已安装
**🔧 工具C需要的新功能**
| 功能 | 现有服务 | 需求 | 方案 |
|------|---------|------|------|
| **Pandas代码执行** | ❌ 不支持 | ✅ 核心 | **新增API端点** `/api/dc/execute` |
| **Excel上传** | ❌ 不支持 | ✅ 需要 | 复用MultiPart上传 |
| **DataFrame管理** | ❌ 不支持 | ✅ 会话 | 新增Session管理 |
| **AST代码检查** | ❌ 不支持 | ✅ 必须 | 新增AST模块 |
| **Excel导出** | ❌ 不支持 | ✅ 需要 | 使用openpyxl已安装 |
**⚠️ 不需要重复开发:**
- ❌ 不需要新建Python项目
- ❌ 不需要重新安装Pandas/openpyxl已安装
- ❌ 不需要重新配置FastAPI已运行
- ❌ 不需要重新写Node.js调用逻辑ExtractionClient已存在
**✅ MVP扩展方案**
```python
# extraction_service/services/dc_executor.py新增
import pandas as pd
import ast
from typing import Dict, Any
def validate_code(code: str) -> Dict[str, Any]:
"""AST静态检查安全验证"""
try:
tree = ast.parse(code)
# 禁止import os、sys、subprocess等
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in ['os', 'sys', 'subprocess', 'socket']:
return {'valid': False, 'error': f'禁止导入{alias.name}'}
return {'valid': True}
except Exception as e:
return {'valid': False, 'error': str(e)}
def execute_pandas_code(data: list, code: str) -> Dict[str, Any]:
"""执行Pandas代码⭐核心功能"""
try:
# 1. 安全检查
validation = validate_code(code)
if not validation['valid']:
return {'success': False, 'error': validation['error']}
# 2. 加载数据
df = pd.DataFrame(data)
# 3. 执行代码注入df和pd
exec(code, {'df': df, 'pd': pd})
# 4. 返回结果前100行
return {
'success': True,
'data': df.head(100).to_dict('records'),
'totalRows': len(df),
'totalCols': len(df.columns),
'columns': df.columns.tolist()
}
except Exception as e:
return {'success': False, 'error': str(e)}
```
```python
# extraction_service/main.py扩展
from services.dc_executor import execute_pandas_code, validate_code
@app.post("/api/dc/execute")
async def execute_code(
data: List[Dict],
code: str
):
"""工具C执行Pandas代码"""
result = execute_pandas_code(data, code)
return JSONResponse(result)
@app.post("/api/dc/validate")
async def validate_code_endpoint(code: str):
"""工具CAST代码检查"""
result = validate_code(code)
return JSONResponse(result)
```
**✅ Node.js调用复用ExtractionClient模式**
```typescript
// backend/src/modules/dc/tool-c/services/PythonExecutorService.ts
import axios from 'axios';
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000';
export class PythonExecutorService {
async executeCode(data: any[], code: string) {
const response = await axios.post(`${EXTRACTION_SERVICE_URL}/api/dc/execute`, {
data,
code
});
return response.data;
}
}
```
### 2.3 数据流设计
**会话生命周期:**
```python
# 1. 用户上传文件
POST /api/tool-c/session/init
{
file: File,
userId: string
}
# Node.js: 转发到Python
# Python:
# - 读取Excel → DataFrame
# - 存入内存sessions[sessionId] = df
# - 返回sessionId + 数据概览
# 2. 用户发送AI指令
POST /api/tool-c/ai/execute
{
sessionId: string,
prompt: string
}
# Node.js:
# - 构建System Prompt包含数据上下文
# - 调用LLM生成代码
# - 转发代码到Python
# Python:
# - AST检查
# - exec(code)修改df
# - 返回前100行JSON
# 3. 用户导出
GET /api/tool-c/session/export/{sessionId}
# Python:
# - df.to_excel(buffer)
# - 返回二进制流
```
---
## 三、功能优先级矩阵
### 3.1 P0级必须开发核心闭环⭐⭐⭐⭐⭐
| ID | 功能 | 描述 | 验证目标 | 工时 | 负责人 |
|----|------|------|---------|------|--------|
| **P0-001** | 文件上传 | 前端Upload组件 + 10MB限制 | 能否解析Excel | 0.5天 | 前端 |
| **P0-002** | Session初始化 | 后端接收文件Python加载DataFrame | 中文列名、GBK编码 | 1天 | 后端 |
| **P0-003** | 表格展示 | AG Grid展示100行数据 | 列类型识别、空值高亮 | 1天 | 前端 |
| **P0-004** | AI对话UI | 右侧侧边栏,消息列表 | 聊天交互流畅 | 0.5天 | 前端 |
| **P0-005** | System Prompt构建 | 包含数据上下文的Prompt | AI能理解数据结构 | 1天 | 后端 |
| **P0-006** | AI代码生成 | 调用DeepSeek-V3生成Pandas代码 | **成功率>80%** | 2天 | 后端 |
| **P0-007** | AST安全检查 | 拦截危险代码 | `import os`被拦截 | 1天 | Python |
| **P0-008** | 代码执行 | exec()在沙箱中运行代码 | 修改DataFrame成功 | 1天 | Python |
| **P0-009** | 预操作卡片 | 展示代码,用户确认后执行 | 用户能看懂代码意图 | 0.5天 | 前端 |
| **P0-010** | 表格刷新 | 执行后自动拉取新数据 | 前端实时更新 | 0.5天 | 前端 |
| **P0-011** | UI锁定机制 | AI处理时表格变灰+遮罩 | 物理禁止并发操作 | 0.5天 | 前端 |
| **P0-012** | 导出Excel | 下载处理后的文件 | 文件完整性 | 1天 | Python |
**P0小计11.5天**
### 3.2 P1级必须验证可简化实现 ⭐⭐⭐⭐
| ID | 功能 | MVP简化方案 | 完整版 | 工时 |
|----|------|------------|--------|------|
| **P1-001** | 会话管理 | 进程内存Map单实例 | Redis分布式 | 0.5天 |
| **P1-002** | 心跳保活 | 固定10分钟过期不续期 | 前端心跳续期 | 0.5天 |
| **P1-003** | 编码检测 | chardet自动检测+友好报错 | 自动转换 | 1天 |
| **P1-004** | AI自我修复 | 失败后重试1次 | 多次重试+学习 | 1天 |
| **P1-005** | 快捷模板 | 3个常用模板年龄分组等 | 10+模板库 | 0.5天 |
**P1小计3.5天**
### 3.3 P2级延后或不做 ⭐⭐
| 功能 | 延后原因 | 何时做 |
|------|---------|--------|
| 手动编辑单元格 | MVP专注AI | P2阶段 |
| 撤销/回滚 | 节省内存 | P2阶段 |
| Apache Arrow | JSON够用 | 性能不达标时 |
| Redis分布式 | 单实例够用 | 横向扩展时 |
| 样式保留 | 复杂度高 | P3阶段 |
| 操作审计 | 非核心 | P3阶段 |
| 多Sheet支持 | 简化逻辑 | P3阶段 |
---
## 四、3周详细开发计划
### Week 1基础架构搭建5天
#### Day 1环境搭建 + 代码结构(⚠️ 参考tool-b结构 + Python环境
**任务清单:**
- [ ] 创建项目目录结构(**参考tool-b**
```
backend/src/modules/dc/tool-c/ ← 主目录
├── services/ ← 业务逻辑层
│ ├── SessionService.ts ← Session管理存数据库
│ ├── AICodeService.ts ← AI代码生成
│ ├── PythonExecutorService.ts ← ⭐ Python代码执行核心
│ └── DataProcessService.ts ← 数据处理逻辑
├── controllers/ ← 控制器层
│ └── ToolCController.ts ← HTTP请求处理
├── routes/ ← 路由层
│ └── index.ts ← 路由定义
└── utils/ ← 工具函数
├── codeValidator.ts ← AST代码检查
└── pythonScripts/ ← ⭐ Python执行脚本
└── executor.py ← Pandas代码执行器
frontend-v2/src/modules/dc/pages/tool-c/ ← 前端(已存在)
```
- [ ] **⭐ 扩展现有Python服务核心功能**
```bash
# 系统已有Python微服务extraction_service只需扩展功能
cd extraction_service
# 1. 创建DC执行器模块
cat > services/dc_executor.py << 'EOF'
import pandas as pd
import ast
from typing import Dict, Any, List
def validate_code(code: str) -> Dict[str, Any]:
"""AST静态检查安全验证"""
try:
tree = ast.parse(code)
# 禁止危险导入
forbidden_modules = ['os', 'sys', 'subprocess', 'socket', 'shutil', 'requests']
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden_modules:
return {'valid': False, 'error': f'禁止导入{alias.name}模块'}
elif isinstance(node, ast.ImportFrom):
if node.module in forbidden_modules:
return {'valid': False, 'error': f'禁止导入{node.module}模块'}
return {'valid': True}
except Exception as e:
return {'valid': False, 'error': f'代码语法错误: {str(e)}'}
def execute_pandas_code(data: List[Dict], code: str) -> Dict[str, Any]:
"""执行Pandas代码⭐核心功能"""
try:
# 1. 安全检查
validation = validate_code(code)
if not validation['valid']:
return {'success': False, 'error': validation['error']}
# 2. 加载数据到DataFrame
df = pd.DataFrame(data)
# 3. 执行代码注入df和pd隔离环境
local_env = {'df': df, 'pd': pd}
exec(code, {'__builtins__': {}}, local_env)
# 4. 获取执行后的df
df_result = local_env.get('df', df)
# 5. 返回结果前100行避免数据量过大
return {
'success': True,
'data': df_result.head(100).to_dict('records'),
'totalRows': len(df_result),
'totalCols': len(df_result.columns),
'columns': df_result.columns.tolist()
}
except Exception as e:
return {
'success': False,
'error': f'代码执行失败: {str(e)}'
}
EOF
# 2. 扩展main.py添加DC端点
# 在main.py末尾添加
cat >> main.py << 'EOF'
# ==================== DC工具C端点 ====================
from services.dc_executor import execute_pandas_code, validate_code
from pydantic import BaseModel
class ExecuteRequest(BaseModel):
data: List[Dict]
code: str
class ValidateRequest(BaseModel):
code: str
@app.post("/api/dc/execute")
async def dc_execute_code(request: ExecuteRequest):
"""工具C执行Pandas代码"""
logger.info(f"DC Execute: code length={len(request.code)}, data rows={len(request.data)}")
result = execute_pandas_code(request.data, request.code)
return JSONResponse(result)
@app.post("/api/dc/validate")
async def dc_validate_code(request: ValidateRequest):
"""工具CAST代码检查"""
logger.info(f"DC Validate: code length={len(request.code)}")
result = validate_code(request.code)
return JSONResponse(result)
EOF
```
- [ ] **Node.js调用Python服务PythonExecutorService**
```typescript
// backend/src/modules/dc/tool-c/services/PythonExecutorService.ts
import axios from 'axios';
import { logger } from '@/common/logging';
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000';
export interface ExecuteResult {
success: boolean;
data?: any[];
totalRows?: number;
totalCols?: number;
columns?: string[];
error?: string;
}
export class PythonExecutorService {
private baseUrl: string;
constructor() {
this.baseUrl = EXTRACTION_SERVICE_URL;
}
/**
* 执行Pandas代码⭐ 核心功能)
* 复用现有Python微服务添加新端点
*/
async executeCode(data: any[], code: string): Promise<ExecuteResult> {
try {
logger.info('Calling Python executor', {
dataRows: data.length,
codeLength: code.length
});
const response = await axios.post<ExecuteResult>(
`${this.baseUrl}/api/dc/execute`,
{ data, code },
{ timeout: 30000 } // 30秒超时
);
logger.info('Python execution success', {
totalRows: response.data.totalRows
});
return response.data;
} catch (error) {
logger.error('Python execution failed', { error });
if (axios.isAxiosError(error) && error.response) {
throw new Error(error.response.data.error || 'Python execution failed');
}
throw new Error('无法连接到Python执行服务');
}
}
/**
* AST代码检查执行前验证
*/
async validateCode(code: string): Promise<{ valid: boolean; error?: string }> {
try {
const response = await axios.post(
`${this.baseUrl}/api/dc/validate`,
{ code },
{ timeout: 5000 }
);
return response.data;
} catch (error) {
logger.error('Code validation failed', { error });
throw new Error('代码验证失败');
}
}
}
export const pythonExecutorService = new PythonExecutorService();
```
- [ ] **强制检查**:确保复用平台服务
```typescript
// backend/src/modules/dc/tool-c/services/SessionService.ts
// ✅ 必须导入这些平台服务
import { storage } from '@/common/storage';
import { logger } from '@/common/logging';
import { cache } from '@/common/cache';
import { prisma } from '@/config/database';
// ❌ 禁止自己实现
// const sessions = new Map() ← 违反规范!
```
- [ ] 数据库Schema设计dc_schema
```prisma
// prisma/schema.prisma添加到dc_schema
model DcToolCSession {
id String @id @default(uuid())
userId String
sessionId String @unique
fileName String
dataSnapshot Json // 存储100行预览数据
totalRows Int
totalCols Int
columns Json // 列信息
expiresAt DateTime // 10分钟过期
createdAt DateTime @default(now())
@@schema("dc_schema")
@@map("dc_tool_c_sessions")
}
```
**验收标准:**
- ✅ 文件夹结构与tool-b一致
- ✅ 导入了所有必须的平台服务
- ✅ 没有内存Map、没有本地文件存储
**负责人:** 后端开发
---
#### Day 2Session管理 + 数据加载(⚠️ 存数据库,不用内存)
**任务清单:**
- [ ] 实现SessionService**存数据库,符合云原生规范**
```typescript
// backend/src/modules/dc/tool-c/services/SessionService.ts
import { storage } from '@/common/storage';
import { logger } from '@/common/logging';
import { prisma } from '@/config/database';
import * as xlsx from 'xlsx';
import chardet from 'chardet';
export class SessionService {
/**
* 创建Session✅ 存数据库不用内存Map
*/
async createSession(userId: string, fileBuffer: Buffer, fileName: string): Promise<string> {
// 1. 检测编码
const detected = chardet.detect(fileBuffer);
if (detected.encoding.toLowerCase() !== 'utf-8') {
throw new Error(`文件编码为${detected.encoding}请转换为UTF-8`);
}
// 2. 内存解析Excel✅ 云原生:不落盘)
const workbook = xlsx.read(fileBuffer, { type: 'buffer' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = xlsx.utils.sheet_to_json(sheet);
// 3. 提取列信息
const columns = Object.keys(data[0] || {}).map(col => ({
name: col,
type: typeof data[0][col]
}));
// 4. Session存数据库✅ 符合云原生规范)
const sessionId = `session_${Date.now()}_${Math.random().toString(36)}`;
await prisma.dcToolCSession.create({
data: {
sessionId,
userId,
fileName,
dataSnapshot: data.slice(0, 100), // 只存前100行预览
totalRows: data.length,
totalCols: columns.length,
columns,
expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10分钟过期
}
});
// 5. 完整数据上传到OSS✅ 云原生:持久化存储)
const dataKey = `dc/tool-c/${sessionId}/full-data.json`;
await storage.uploadBuffer(dataKey, Buffer.from(JSON.stringify(data)));
logger.info('Session created', { sessionId, totalRows: data.length });
return sessionId;
}
/**
* 获取Session从数据库读取
*/
async getSession(sessionId: string) {
const session = await prisma.dcToolCSession.findUnique({
where: { sessionId }
});
if (!session || new Date() > session.expiresAt) {
throw new Error('Session已过期');
}
return session;
}
}
```
**测试用例:**
- [ ] 上传UTF-8编码的Excel正常解析
- [ ] 上传GBK编码的Excel被拦截并提示
- [ ] 上传中文列名的Excel无乱码
- [ ] Session存入数据库可查询
- [ ] 10分钟后Session自动过期
**验收标准:**
- ✅ Session存储在数据库不是内存Map
- ✅ 完整数据存储在OSS
- ✅ 符合云原生开发规范
**负责人:** Node.js后端
---
#### Day 3Node.js BFF + 文件上传
**任务清单:**
- [ ] 创建Fastify路由
```typescript
// backend/src/modules/dc/tool-c/routes/index.ts
import { FastifyInstance } from 'fastify';
export async function registerToolCRoutes(fastify: FastifyInstance) {
// 文件上传
fastify.post('/api/v1/dc/tool-c/session/init', async (req, reply) => {
const file = await req.file();
// 1. 文件大小检查
if (file.file.bytesRead > 10 * 1024 * 1024) {
return reply.code(413).send({
success: false,
error: '文件过大请压缩后重试限10MB'
});
}
// 2. 转发到Python服务
const formData = new FormData();
formData.append('file', file.file, file.filename);
const response = await axios.post('http://localhost:8001/api/python/init', formData);
return {
success: true,
sessionId: response.data.sessionId,
data: response.data
};
});
}
```
- [ ] 集成到主应用
```typescript
// backend/src/modules/dc/index.ts
import { registerToolCRoutes } from './tool-c/routes';
export async function registerDCRoutes(fastify: FastifyInstance) {
await registerToolBRoutes(fastify);
await registerToolCRoutes(fastify); // 新增
}
```
**测试用例:**
- [ ] 上传9MB文件成功
- [ ] 上传11MB文件被拦截
- [ ] 并发上传2个文件sessionId不冲突
**验收标准:**
- ✅ Node.js能正确转发文件到Python
- ✅ 文件大小限制生效
**负责人:** Node.js后端
---
#### Day 4前端框架搭建
**任务清单:**
- [ ] 创建Tool C页面
```typescript
// frontend-v2/src/modules/dc/pages/tool-c/index.tsx
import { AgGridReact } from 'ag-grid-react';
import { Upload, Spin } from 'antd';
export default function ToolCEditor() {
const [sessionId, setSessionId] = useState<string>();
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const handleUpload = async (file: File) => {
setIsLoading(true);
const formData = new FormData();
formData.append('file', file);
const response = await api.post('/api/v1/dc/tool-c/session/init', formData);
setSessionId(response.data.sessionId);
setData(response.data.data.preview);
setColumns(response.data.data.columns);
setIsLoading(false);
};
return (
<div className="h-screen flex">
{/* 左侧:表格区域 */}
<div className="flex-1">
{!sessionId ? (
<Upload beforeUpload={handleUpload}>
<Button>上传Excel文件限10MB</Button>
</Upload>
) : (
<AgGridReact
rowData={data}
columnDefs={columns.map(col => ({
field: col.name,
headerName: col.name
}))}
/>
)}
</div>
{/* 右侧AI侧边栏占位 */}
<div className="w-96 border-l bg-white">
<h3>AI助手</h3>
</div>
</div>
);
}
```
- [ ] 集成AG Grid
```bash
cd frontend-v2
npm install ag-grid-react ag-grid-community
```
- [ ] 路由配置
```typescript
// frontend-v2/src/modules/dc/routes.tsx
{
path: 'tool-c',
element: <ToolCEditor />
}
```
**测试用例:**
- [ ] 上传文件,表格正确显示
- [ ] 中文列名显示正常
- [ ] 空值单元格有视觉提示
**验收标准:**
- ✅ 用户能看到左右分栏界面
- ✅ 表格能展示100行数据
**负责人:** 前端开发
---
#### Day 5System Prompt构建
**任务清单:**
- [ ] 创建Prompt模板
```typescript
// backend/src/modules/dc/tool-c/prompts/system-prompt.ts
export function buildSystemPrompt(dataContext: DataContext): string {
return `# AI医疗数据清洗助手 - 系统角色
## 当前数据结构
- 总行数:${dataContext.totalRows} 行
- 总列数:${dataContext.totalCols} 列
- 列名:${dataContext.columns.map(c => c.name).join(', ')}
### 前5行数据示例
${JSON.stringify(dataContext.headData, null, 2)}
## 严格规则
1. 只能使用pandas操作已预导入为pd
2. 变量名必须是df不要用其他名字
3. 就地修改df['new'] = ... 或 df.drop(...)
4. 不要print()、display()等输出
5. 禁止import os、sys、requests等
## Few-shot示例分层难度基础→中等→高级
### 🟢 基础场景(单步骤操作)
#### 场景1年龄分组
用户:"把年龄大于60的标记为老年组"
代码:
df['age_group'] = df['age'].apply(lambda x: '老年组' if pd.notna(x) and x > 60 else '非老年组')
#### 场景2删除缺失
用户:"删除没有患者ID的行"
代码:
df.dropna(subset=['patient_id'], inplace=True)
#### 场景3性别编码
用户:"把性别转为数字"
代码:
df['gender_code'] = df['gender'].map({'男': 1, '女': 0})
### 🟡 中等场景(多步骤或跨列逻辑)
#### 场景4计算NLR并分组
用户:"计算中性粒细胞淋巴细胞比值NLR并按2.5分为高低两组"
代码:
df['NLR'] = df.apply(lambda row: row['neutrophil'] / row['lymphocyte'] if pd.notna(row['neutrophil']) and pd.notna(row['lymphocyte']) and row['lymphocyte'] > 0 else None, axis=1)
df['NLR_group'] = df['NLR'].apply(lambda x: 'High' if pd.notna(x) and x > 2.5 else 'Low')
#### 场景5字符串拆分血压
用户:"把血压列的'120/80'格式拆分成收缩压和舒张压,并判断是否高血压"
代码:
df[['systolic_bp', 'diastolic_bp']] = df['blood_pressure'].str.split('/', expand=True)
df['systolic_bp'] = pd.to_numeric(df['systolic_bp'], errors='coerce')
df['diastolic_bp'] = pd.to_numeric(df['diastolic_bp'], errors='coerce')
df['is_hypertension'] = ((df['systolic_bp'] > 140) | (df['diastolic_bp'] > 90)).astype(int)
#### 场景6时间差计算逻辑验证
用户:"计算住院天数,如果出院日期早于入院日期则标记为异常"
代码:
df['admission_date'] = pd.to_datetime(df['admission_date'], errors='coerce')
df['discharge_date'] = pd.to_datetime(df['discharge_date'], errors='coerce')
df['hospital_days'] = (df['discharge_date'] - df['admission_date']).dt.days
df['date_error'] = df['hospital_days'] < 0
df.loc[df['date_error'], 'hospital_days'] = None
### 🔴 高级场景(分组聚合、时间序列、医学规则)
#### 场景7生存时间计算复杂条件逻辑
用户:"生成生存状态和生存时间如果死亡日期存在则状态为1时间为死亡日期减诊断日期否则状态为0时间为随访截止日期减诊断日期"
代码:
df['diagnosis_date'] = pd.to_datetime(df['diagnosis_date'], errors='coerce')
df['death_date'] = pd.to_datetime(df['death_date'], errors='coerce')
df['followup_end_date'] = pd.to_datetime(df['followup_end_date'], errors='coerce')
df['vital_status'] = df['death_date'].notna().astype(int)
df['survival_days'] = df.apply(lambda row: (row['death_date'] - row['diagnosis_date']).days if pd.notna(row['death_date']) else (row['followup_end_date'] - row['diagnosis_date']).days, axis=1)
df['survival_months'] = (df['survival_days'] / 30.44).round(1)
#### 场景8分组聚合首末记录
用户:"对每个患者找出第一次化疗日期和最后一次化疗日期,计算持续时间"
代码:
df['chemo_date'] = pd.to_datetime(df['chemo_date'], errors='coerce')
patient_chemo = df.groupby('patient_id')['chemo_date'].agg(['min', 'max']).reset_index()
patient_chemo.columns = ['patient_id', 'first_chemo', 'last_chemo']
patient_chemo['chemo_duration_days'] = (patient_chemo['last_chemo'] - patient_chemo['first_chemo']).dt.days
df = df.merge(patient_chemo[['patient_id', 'first_chemo', 'last_chemo', 'chemo_duration_days']], on='patient_id', how='left')
#### 场景9时间序列变化率
用户:"按患者ID分组计算每次随访相比上次的肿瘤大小变化率"
代码:
df = df.sort_values(['patient_id', 'followup_date'])
df['prev_tumor_size'] = df.groupby('patient_id')['tumor_size'].shift(1)
df['tumor_change_rate'] = ((df['tumor_size'] - df['prev_tumor_size']) / df['prev_tumor_size'] * 100).round(2)
df['tumor_change_rate'] = df['tumor_change_rate'].fillna(0)
#### 场景10医学规则引擎肝功能分级
用户:"根据ALT、AST、ALP、TBIL判断肝功能分级"
代码:
def classify_liver_function(row):
abnormal_count = 0
if pd.notna(row.get('ALT')) and row['ALT'] > 40: abnormal_count += 1
if pd.notna(row.get('AST')) and row['AST'] > 40: abnormal_count += 1
if pd.notna(row.get('ALP')) and row['ALP'] > 125: abnormal_count += 1
if pd.notna(row.get('TBIL')) and row['TBIL'] > 20: abnormal_count += 1
if abnormal_count == 0: return '正常'
elif abnormal_count == 1: return '轻度异常'
elif abnormal_count == 2: return '中度异常'
else: return '重度异常'
df['liver_function_grade'] = df.apply(classify_liver_function, axis=1)
现在,请准备好接收用户的指令。记住:代码要安全、可靠、易懂,能处理从基础到高级的复杂医疗场景。
`;
}
```
- [ ] 集成LLMFactory✅ 复用平台通用能力层)
```typescript
// backend/src/modules/dc/tool-c/services/AICodeService.ts
import { LLMFactory } from '@/common/llm'; // ✅ 复用平台LLM能力
import { logger } from '@/common/logging'; // ✅ 复用日志
export class AICodeService {
/**
* 生成Pandas代码✅ 复用LLMFactory
*/
async generateCode(prompt: string, dataContext: DataContext): Promise<string> {
logger.info('Generating Pandas code', { prompt, dataContext });
// ✅ 复用平台LLM服务
const llm = LLMFactory.getLLM('deepseek-v3');
const systemPrompt = buildSystemPrompt(dataContext);
const response = await llm.chat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
]);
// 提取纯代码去除Markdown格式
const code = this.extractCode(response.content);
logger.info('Code generated successfully', { codeLength: code.length });
return code;
}
/**
* AI自我修复失败后重试1次
*/
async fixCode(originalCode: string, errorMsg: string, dataContext: DataContext): Promise<string> {
logger.warn('Code execution failed, attempting self-repair', { errorMsg });
const llm = LLMFactory.getLLM('deepseek-v3');
const fixPrompt = `以下代码执行失败,请修复:
错误信息:
${errorMsg}
原始代码:
${originalCode}
数据结构:
${JSON.stringify(dataContext)}
请生成修复后的代码不要有Markdown格式`;
const response = await llm.chat([{ role: 'user', content: fixPrompt }]);
return this.extractCode(response.content);
}
private extractCode(text: string): string {
// 去除```python```或```标记
const match = text.match(/```(?:python)?\n([\s\S]*?)\n```/);
return match ? match[1] : text;
}
}
```
**测试用例:**
- [ ] 输入"年龄分组",生成正确代码
- [ ] 输入"删除空行",生成正确代码
- [ ] 检查代码中是否包含危险语句
**验收标准:**
- ✅ AI能生成可执行的Pandas代码
- ✅ 代码符合规范无print、无import
**负责人:** Node.js后端
---
### Week 2核心功能开发5天
#### Day 6-7AI代码生成 + AST检查
**Day 6任务**
- [ ] 实现AST静态检查
```python
# python-service/code_validator.py
import ast
DANGEROUS_IMPORTS = {'os', 'sys', 'subprocess', 'requests', 'urllib'}
DANGEROUS_BUILTINS = {'eval', 'exec', 'compile', 'open', '__import__'}
def validate_code_safety(code: str) -> Tuple[bool, str]:
try:
tree = ast.parse(code)
for node in ast.walk(tree):
# 检查import
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in DANGEROUS_IMPORTS:
return False, f"禁止导入:{alias.name}"
# 检查函数调用
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id in DANGEROUS_BUILTINS:
return False, f"禁止使用:{node.func.id}"
return True, "验证通过"
except SyntaxError as e:
return False, f"语法错误:{str(e)}"
```
**Day 7任务**
- [ ] 实现AI代码生成API
```typescript
// Node.js
fastify.post('/api/v1/dc/tool-c/ai/generate', async (req, reply) => {
const { sessionId, prompt } = req.body;
// 1. 获取数据上下文
const context = await pythonService.getDataContext(sessionId);
// 2. 调用AI生成代码
const code = await aiService.generateCode(prompt, context);
return {
success: true,
code,
summary: `将执行以下操作:${extractSummary(code)}`
};
});
```
**测试场景15个真实医疗数据清洗场景**
**🟢 基础场景5个- 单步骤操作:**
1. [ ] "把年龄大于60的标记为老年组" - 简单条件判断
2. [ ] "删除所有患者ID为空的行" - 数据完整性清洗
3. [ ] "把性别转为数字男1女0" - 分类变量编码
4. [ ] "计算BMI = 体重 / (身高/100)^2" - 简单公式计算
5. [ ] "删除缺失率超过50%的列" - 列级数据质量控制
**🟡 中等场景5个- 多步骤或跨列逻辑:**
6. [ ] "把诊断日期和出院日期计算天数差,如果出院日期早于诊断日期则标记为异常" - 逻辑验证
7. [ ] "根据白细胞、中性粒细胞、淋巴细胞三个指标计算NLR中性粒细胞/淋巴细胞并按2.5分为高低两组" - 多步骤计算
8. [ ] "从病理报告列中提取TNM分期生成新列如果没有提取到则标记为'未分期'" - 文本提取(可用正则)
9. [ ] "把血压列中的'120/80'格式拆分成收缩压和舒张压两列,并判断是否高血压(收缩压>140 or 舒张压>90" - 字符串处理+逻辑判断
10. [ ] "删除重复的患者ID保留最新的一条记录根据就诊日期" - 去重+排序
**🔴 高级场景5个- 复杂分组、时间序列、医学规则:**
11. [ ] "对于每个患者,找出第一次化疗日期和最后一次化疗日期,计算化疗持续时间" - 分组聚合
12. [ ] "生成生存状态变量如果死亡日期存在则为1否则为0生成生存时间如果死亡则为死亡日期-诊断日期),否则为(随访截止日期-诊断日期)" - 复杂条件逻辑
13. [ ] "根据多个实验室指标ALT、AST、ALP、TBIL判断肝功能分级正常、轻度异常、中度异常、重度异常" - 医学规则引擎
14. [ ] "按患者ID分组对每个患者的多次随访记录计算相邻两次之间的指标变化率如肿瘤大小变化率" - 时间序列分析
15. [ ] "根据入院时间,计算患者的季节变量(春夏秋冬),然后统计不同季节的发病人数" - 时间特征提取+统计分析
**验收标准(分层要求):**
- ✅ **基础场景成功率 > 90%**5/5或4/5成功
- ✅ **中等场景成功率 > 80%**4/5成功
- ✅ **高级场景成功率 > 60%**3/5成功
- ✅ **总体成功率 > 80%**12/15场景成功
- ✅ AST能拦截`import os`等危险代码
- ❌ 如果总体成功率 < 60%9/15失败**MVP失败需要Pivot到模板库模式**
**负责人:** Node.js后端 + Python开发
---
#### Day 8代码执行 + 表格刷新
**任务清单:**
- [ ] 实现代码执行API
```python
# Python
@app.post("/api/python/execute")
async def execute_code(request: ExecuteRequest):
# 1. 获取Session
df = session_manager.get(request.sessionId)
# 2. AST检查
is_safe, error_msg = validate_code_safety(request.code)
if not is_safe:
return {"success": False, "error": error_msg}
# 3. 执行代码
try:
exec(request.code, {'df': df, 'pd': pd, 'np': np})
# 4. 返回预览数据
preview = df.head(100).to_dict('records')
return {
"success": True,
"preview": preview,
"stats": {
"totalRows": len(df),
"totalCols": len(df.columns)
}
}
except Exception as e:
return {
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}
```
- [ ] 前端执行流程
```typescript
async function executeAICode(code: string) {
// 1. 展示预操作卡片
const confirmed = await showActionCard(code);
if (!confirmed) return;
// 2. 锁定表格
setIsLocked(true);
try {
// 3. 执行代码
const response = await api.post('/api/v1/dc/tool-c/ai/execute', {
sessionId,
code
});
if (response.data.success) {
// 4. 刷新表格
setData(response.data.preview);
message.success('执行成功!');
} else {
message.error(`执行失败:${response.data.error}`);
}
} finally {
// 5. 解锁表格
setIsLocked(false);
}
}
```
**测试用例:**
- [ ] 执行成功,表格正确刷新
- [ ] 执行失败,显示错误信息
- [ ] 执行期间,表格处于锁定状态
**验收标准:**
- ✅ 代码能正确修改DataFrame
- ✅ 前端能实时看到变化
**负责人:** Python开发 + 前端
---
#### Day 9UI锁定 + 预操作卡片
**任务清单:**
- [ ] 实现AI对话UI
```typescript
function AIChatPanel({ sessionId }: Props) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const handleSend = async () => {
if (!input.trim()) return;
// 1. 添加用户消息
setMessages(prev => [...prev, {
role: 'user',
content: input
}]);
setIsProcessing(true);
// 2. 调用AI生成代码
const response = await api.post('/api/v1/dc/tool-c/ai/generate', {
sessionId,
prompt: input
});
// 3. 添加AI消息包含代码
setMessages(prev => [...prev, {
role: 'assistant',
content: response.data.summary,
code: response.data.code
}]);
setIsProcessing(false);
setInput('');
};
return (
<div className="flex flex-col h-full">
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4">
{messages.map((msg, idx) => (
<MessageBubble key={idx} message={msg} />
))}
</div>
{/* 输入框 */}
<div className="p-4 border-t">
<Input.TextArea
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入指令例如把年龄大于60的标记为老年组"
disabled={isProcessing}
/>
<Button onClick={handleSend} disabled={isProcessing}>
发送
</Button>
</div>
</div>
);
}
```
- [ ] 实现预操作卡片
```typescript
function ActionCard({ code, onConfirm, onCancel }: Props) {
return (
<Card className="mt-2 bg-slate-900 text-white">
<div className="mb-2 text-xs text-slate-400">
AI生成的代码 - 请确认后执行
</div>
<pre className="text-sm font-mono text-blue-300">
{code}
</pre>
<div className="mt-3 flex gap-2">
<Button type="primary" onClick={onConfirm}>
运行代码
</Button>
<Button onClick={onCancel}>
取消
</Button>
</div>
</Card>
);
}
```
- [ ] 实现UI锁定
```typescript
<AgGridReact
rowData={data}
suppressClickEdit={isLocked}
readOnlyEdit={isLocked}
/>
{isLocked && (
<div className="absolute inset-0 bg-black/20 flex items-center justify-center">
<Spin tip="AI正在处理中请稍候..." />
</div>
)}
```
**验收标准:**
- ✅ 用户能发送消息
- ✅ AI返回代码时展示预操作卡片
- ✅ 执行期间表格锁定
**负责人:** 前端
---
#### Day 10导出 + 端到端测试
**任务清单:**
- [ ] 实现导出API
```python
@app.get("/api/python/export/{session_id}")
async def export_excel(session_id: str):
df = session_manager.get(session_id)
# 生成Excel
output = io.BytesIO()
df.to_excel(output, index=False, engine='openpyxl')
output.seek(0)
return StreamingResponse(
output,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': f'attachment; filename=cleaned_data_{session_id}.xlsx'
}
)
```
- [ ] 前端导出按钮
```typescript
const handleExport = async () => {
const response = await api.get(`/api/v1/dc/tool-c/session/export/${sessionId}`, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(response.data);
const a = document.createElement('a');
a.href = url;
a.download = 'cleaned_data.xlsx';
a.click();
};
```
- [ ] **端到端测试(完整流程)**
```
1. 上传 test_data.xlsx (5000行 × 20列)
2. AI指令"把年龄大于60的标记为老年组"
3. 确认 → 执行 → 表格刷新(验证新增列)
4. AI指令"删除患者ID为空的行"
5. 确认 → 执行 → 表格刷新(验证行数减少)
6. 点击"导出" → 下载文件
7. 打开下载的文件,验证数据正确
```
**验收标准:**
- ✅ 整个流程 < 2分钟
- ✅ 导出的Excel数据正确
- ✅ AI代码执行成功
**负责人:** 全员
---
### Week 3优化与测试5天
#### Day 11-12错误处理 + AI自我修复
**任务清单:**
- [ ] 实现AI自我修复
```typescript
async function executeWithRetry(code: string): Promise<ExecuteResult> {
// 第一次执行
let result = await pythonService.execute(sessionId, code);
if (!result.success) {
// 失败让AI修复
const fixedCode = await aiService.fixCode(code, result.error);
// 重试1次
result = await pythonService.execute(sessionId, fixedCode);
if (!result.success) {
// 彻底失败
return {
success: false,
error: '代码执行失败,请尝试更详细地描述您的需求',
originalError: result.error
};
}
}
return result;
}
```
- [ ] 优化错误提示
```typescript
function showErrorDetail(error: string) {
const errorMap = {
"KeyError: 'age'": "列名'age'不存在,请检查列名是否正确",
"ValueError": "数据类型不匹配,请检查数据格式",
"MemoryError": "数据量过大,请减少数据行数"
};
const friendlyMsg = errorMap[error] || error;
Modal.error({
title: '执行失败',
content: friendlyMsg
});
}
```
**测试场景(边界情况):**
- [ ] 列名不存在:`df['nonexistent']`
- [ ] 语法错误:`df[df['age'] > 60 and df['age'] < 80]`
- [ ] 数据类型错误:`df['age'] + '字符串'`
- [ ] 内存溢出:处理超大数据
**验收标准:**
- ✅ 常见错误能自我修复
- ✅ 无法修复时有友好提示
**负责人:** Node.js后端
---
#### Day 13快捷模板 + 编码检测
**任务清单:**
- [ ] 实现快捷模板
```typescript
const QUICK_TEMPLATES = {
'年龄分组': {
label: '年龄分组60岁分界',
code: `df['age_group'] = pd.cut(df['age'], bins=[0,60,150], labels=['非老年','老年'])`
},
'删除空行': {
label: '删除空行',
code: `df.dropna(how='all', inplace=True)`
},
'性别编码': {
label: '性别编码男1女0',
code: `df['gender_code'] = df['gender'].map({'男':1, '女':0})`
}
};
function QuickActions() {
return (
<div className="p-4 border-t">
<div className="text-xs text-slate-500 mb-2">快捷操作</div>
{Object.entries(QUICK_TEMPLATES).map(([key, template]) => (
<Button
key={key}
size="small"
onClick={() => executeTemplate(template.code)}
>
{template.label}
</Button>
))}
</div>
);
}
```
- [ ] 实现编码检测
```python
import chardet
def detect_encoding(file_bytes: bytes) -> str:
result = chardet.detect(file_bytes)
encoding = result['encoding']
confidence = result['confidence']
if confidence < 0.8:
raise ValueError(f"无法确定文件编码(置信度{confidence:.0%}")
return encoding
@app.post("/api/python/init")
async def init_session(file: UploadFile):
content = await file.read()
# 检测编码
encoding = detect_encoding(content)
if encoding.lower() not in ['utf-8', 'utf-8-sig']:
return {
"success": False,
"error": f"文件编码为{encoding}请转换为UTF-8后重新上传",
"suggestion": "在Excel中另存为CSV UTF-8格式"
}
# 正常加载
# ...
```
**验收标准:**
- ✅ 快捷模板一键执行
- ✅ GBK文件被友好拦截
**负责人:** 前端 + Python开发
---
#### Day 14性能测试 + 压力测试
**测试场景:**
- [ ] **性能测试**
```
测试数据5000行 × 20列约5MB
场景1上传文件
- 目标:< 5秒
- 实测____秒
场景2AI生成代码
- 目标:< 5秒
- 实测____秒
场景3代码执行+刷新
- 目标:< 3秒
- 实测____秒
场景4导出Excel
- 目标:< 3秒
- 实测____秒
总计:< 16秒
```
- [ ] **压力测试**
```
场景1并发用户
- 5个用户同时上传文件
- 验证Session隔离互不影响
场景2大数据量
- 上传50000行 × 50列约10MB
- 验证:不崩溃,响应时间可接受
场景3连续操作
- 连续执行10次AI指令
- 验证:内存不泄漏
```
**性能优化(如果不达标):**
- [ ] 前端AG Grid虚拟滚动配置
- [ ] 后端DataFrame操作改为惰性计算
- [ ] Python使用Polars替代Pandas如果需要
**验收标准:**
- ✅ 单次操作 < 16秒
- ✅ 5个并发用户正常工作
- ✅ 无内存泄漏
**负责人:** 全员
---
#### Day 15文档 + 演示准备
**任务清单:**
- [ ] 编写用户文档
```markdown
# 工具C使用指南
## 快速开始
1. 点击"上传文件"选择Excel限10MB
2. 等待3-5秒表格加载完成
3. 在右侧AI助手输入指令例如"把年龄大于60的标记为老年组"
4. 点击"运行代码"确认
5. 查看表格自动刷新
6. 完成所有操作后,点击"导出"下载结果
## 常见问题
Q: 文件上传失败,提示文件过大?
A: 请将Excel文件压缩到10MB以内或删除不需要的列。
Q: 列名显示乱码?
A: 请在Excel中另存为"CSV UTF-8"格式后重新上传。
```
- [ ] 准备演示数据
```
创建 demo_data.xlsx
- 100行患者数据
- 包含patient_id, name, age, gender, admission_date, bmi
- 故意制造一些问题:空值、异常值、需要清洗的数据
```
- [ ] 准备演示脚本
```
演示流程5分钟
1. 上传demo_data.xlsx
2. AI指令"把年龄大于60的标记为老年组"
3. AI指令"删除患者ID为空的行"
4. AI指令"把性别转为数字男1女0"
5. 导出结果
6. 展示导出的Excel文件
```
**验收标准:**
- ✅ 文档清晰易懂
- ✅ 演示流畅无卡顿
- ✅ 演示数据能体现核心功能
**负责人:** 全员
---
## 五、风险应对策略
### 5.1 风险清单与应对
| 风险 | 概率 | 影响 | 应对策略 | 降级方案 |
|------|------|------|---------|---------|
| **AI代码质量低成功率<60%** | 中 | 致命 | 1. 优化Prompt工程<br>2. 增加Few-shot示例<br>3. 实现自我修复 | **Pivot改用代码模板库** |
| **Apache Arrow集成困难** | 高 | 高 | 1. MVP不用Arrow<br>2. 直接用JSON | **已降级MVP用JSON** |
| **Redis内存成本高** | 中 | 中 | 1. MVP用进程内存<br>2. 激进的Session过期 | **已降级用Map缓存** |
| **Python内存泄漏** | 中 | 高 | 1. 不做历史快照<br>2. 定期重启进程 | **SAE自动重启** |
| **中文Excel乱码** | 高 | 中 | 1. chardet自动检测<br>2. 友好报错提示 | **文档说明转UTF-8** |
| **SAE冷启动慢** | 高 | 中 | 1. 最小实例数=1<br>2. 异步初始化 | **Loading优化** |
| **前端AG Grid性能差** | 低 | 中 | 1. 只展示100行<br>2. 虚拟滚动 | **已优化** |
### 5.2 快速失败触发器
**立即停止开发的条件:**
1. **Day 7结束AI代码成功率 < 60%**
- 决策放弃AI Code Interpreter路线
- Pivot改为"代码模板库 + 参数化配置"
2. **Day 10结束端到端时间 > 30秒**
- 决策:性能无法接受
- Pivot改为批处理模式上传 → 后台处理 → 下载)
3. **Day 14结束3个用户测试都无法独立完成任务**
- 决策:交互设计失败
- Pivot重新设计UI增加新手引导
---
## 六、验收标准
### 6.1 功能验收清单
| 编号 | 验收项 | 验收标准 | 验收方式 |
|------|--------|---------|---------|
| **F-001** | 文件上传 | 能上传10MB以内的Excel | 手动测试 |
| **F-002** | 编码检测 | GBK文件被友好拦截 | 手动测试 |
| **F-003** | 表格展示 | 100行数据正确显示中文无乱码 | 手动测试 |
| **F-004** | AI对话 | 能发送消息,接收回复 | 手动测试 |
| **F-005** | 代码生成 | **10个场景8个成功80%** | 自动化测试 |
| **F-006** | AST检查 | 拦截`import os`等危险代码 | 单元测试 |
| **F-007** | 代码执行 | 能正确修改DataFrame | 集成测试 |
| **F-008** | 表格刷新 | 执行后自动更新 | 手动测试 |
| **F-009** | UI锁定 | AI处理时表格只读+遮罩 | 手动测试 |
| **F-010** | 导出Excel | 下载的文件数据正确 | 手动测试 |
### 6.2 性能验收标准
| 指标 | 目标值 | 验收方式 |
|------|--------|---------|
| 文件上传到表格显示 | < 5秒 | 性能测试 |
| AI生成代码 | < 5秒 | 性能测试 |
| 代码执行+刷新 | < 3秒 | 性能测试 |
| 导出Excel | < 3秒 | 性能测试 |
| **端到端总时间** | **< 16秒** | 性能测试 |
| 并发用户数 | ≥ 5人 | 压力测试 |
| 内存占用 | < 2GB5用户 | 压力测试 |
### 6.3 安全验收标准
| 指标 | 验收标准 | 验收方式 |
|------|---------|---------|
| 代码沙箱 | 拦截所有危险代码 | 渗透测试 |
| Session隔离 | 用户A看不到用户B数据 | 并发测试 |
| Session过期 | 10分钟后自动清理 | 时间测试 |
---
## 七、快速失败机制
### 7.1 关键检查点
```
Checkpoint 1 (Day 5结束)
├─ System Prompt能否让AI理解数据
├─ AI能生成Pandas代码吗
└─ 决策:继续 or 调整Prompt策略
Checkpoint 2 (Day 7结束)
├─ AI代码成功率 ≥ 80%
├─ 如果 < 60%:立即停止,改用模板库
└─ 如果 60-80%继续优化Prompt
Checkpoint 3 (Day 10结束)
├─ 端到端流程能跑通?
├─ 性能 < 16秒
└─ 决策:继续 or 性能优化
Checkpoint 4 (Day 14结束)
├─ 用户测试通过?
├─ 无重大Bug
└─ 决策:发布 or 修复Bug
```
### 7.2 Pivot决策树
```
如果 AI代码成功率 < 60%
├─ Pivot 1改用"代码模板库 + 参数化配置"
│ └─ 用户选择模板 → 填写参数 → 生成代码
├─ Pivot 2AI变成"辅助建议"角色
│ └─ 用户手动写代码AI提供建议
└─ Pivot 3简化为"批处理模式"
└─ 上传 → 选择预设操作 → 后台处理 → 下载
如果 性能无法接受(> 30秒
├─ 方案1引入Apache Arrow
├─ 方案2使用Polars替代Pandas
└─ 方案3改为批处理+异步通知
如果 用户无法独立完成任务:
├─ 方案1增加交互式新手引导
├─ 方案2提供示例数据集
└─ 方案3简化UI减少选项
```
---
## 八、附录
### 8.1 测试数据准备
**创建标准测试数据集:**
```python
# create_test_data.py
import pandas as pd
import numpy as np
# 生成5000行测试数据
data = {
'patient_id': [f'P{i:04d}' for i in range(1, 5001)],
'name': [f'患者{i}' for i in range(1, 5001)],
'age': np.random.randint(18, 90, 5000),
'gender': np.random.choice(['男', '女'], 5000),
'admission_date': pd.date_range('2023-01-01', periods=5000, freq='H'),
'weight': np.random.uniform(45, 95, 5000).round(1),
'height': np.random.uniform(150, 190, 5000).round(0),
'diagnosis': np.random.choice(['肺癌', '糖尿病', '高血压'], 5000)
}
df = pd.DataFrame(data)
# 故意制造问题
df.loc[np.random.choice(5000, 100, replace=False), 'age'] = None # 100个缺失
df.loc[np.random.choice(5000, 10, replace=False), 'age'] = 150 # 10个异常值
df.loc[np.random.choice(5000, 50, replace=False), 'patient_id'] = None # 50个空ID
df.to_excel('test_data.xlsx', index=False)
```
### 8.2 10个典型场景测试用例
```python
# test_ai_scenarios.py
test_cases = [
{
"id": "TC-001",
"prompt": "把年龄大于60的患者标记为老年组",
"expected_code": "df['age_group'] = df['age'].apply(lambda x: '老年组' if pd.notna(x) and x > 60 else '非老年组')",
"verify": lambda df: 'age_group' in df.columns
},
{
"id": "TC-002",
"prompt": "删除所有患者ID为空的行",
"expected_code": "df.dropna(subset=['patient_id'], inplace=True)",
"verify": lambda df: df['patient_id'].isnull().sum() == 0
},
# ... 其余8个场景
]
```
### 8.3 环境配置清单
**Node.js依赖**
```json
{
"dependencies": {
"fastify": "^4.0.0",
"axios": "^1.6.0",
"joi": "^17.11.0",
"@fastify/multipart": "^8.0.0"
}
}
```
**Python依赖**
```txt
fastapi==0.115.0
uvicorn==0.30.0
pandas==2.2.0
openpyxl==3.1.0
chardet==5.2.0
python-multipart==0.0.9
```
---
## 九、项目管理
### 9.1 团队配置
| 角色 | 人数 | 职责 |
|------|------|------|
| 前端开发 | 1人 | React + AG Grid + UI |
| Node.js后端 | 1人 | BFF + LLM集成 + Prompt工程 |
| Python开发 | 1人 | FastAPI + DataFrame + AST检查 |
| **总计** | **3人** | **3周15天** |
### 9.2 沟通机制
- **每日站会**15分钟同步进度和问题
- **关键检查点**Day 5、7、10、14
- **问题升级**阻塞问题1小时内升级
- **文档更新**:每日更新开发记录
### 9.3 代码管理
**分支策略:**
```
main (稳定版)
└── develop (开发版)
├── feature/tool-c-frontend
├── feature/tool-c-backend
└── feature/tool-c-python
```
**提交规范:**
```
feat: 新增AI代码生成功能
fix: 修复表格刷新Bug
test: 增加AST检查测试用例
docs: 更新开发文档
```
---
## 十、总结
### 核心成功要素
1. ✅ **AI代码质量**:成功率 > 80%(核心假设)
2. ✅ **性能可接受**:端到端 < 16秒
3. ✅ **交互简单**:用户无需文档就能用
### 风险控制
1. ✅ **快速失败**7天验证AI能力不行立即Pivot
2. ✅ **务实技术**JSON不用Arrow内存不用Redis
3. ✅ **降级方案**准备好模板库、批处理等Plan B
### 时间节点
- **Week 1**基础架构5天
- **Week 2**核心功能5天
- **Week 3**优化测试5天
- **总计**15个工作日
---
## 📝 文档修订记录
### V1.32025-12-06- 发现现有Python服务 ⭐ 重大发现
**修正内容**检查系统发现已有Python微服务不需要重复开发
#### 修正1复用现有Python服务不重复造轮 ⭐⭐⭐⭐⭐
- **重大发现**:系统已有 `extraction_service/`FastAPI + Pandas + openpyxl
- **现有功能**
- ✅ PDF/Docx/Txt文档提取PyMuPDF + Mammoth
- ✅ Pandas、openpyxl、chardet、langdetect已安装
- ✅ FastAPI框架已运行端口8000
- ✅ Node.js已有ExtractionClient集成
- **新增方案**
- ✅ 扩展现有服务,添加 `/api/dc/execute` 端点
- ✅ 新增 `dc_executor.py` 模块AST检查 + 代码执行)
- ✅ 复用ExtractionClient模式调用
- **避免重复**
- ❌ 不需要新建Python项目
- ❌ 不需要重新安装依赖
- ❌ 不需要重新写HTTP调用逻辑
---
### V1.22025-12-06- Python执行环境 + 复杂场景 ⚠️ 核心功能
**修正内容**根据用户反馈明确Python代码执行是核心功能
#### 修正1Python执行是核心不是可选 ⭐⭐⭐⭐⭐
- **原错误**:说"MVP阶段可能不需要Python微服务"
- **已修正****Python代码执行是工具C的核心价值必须实现**
- **新增内容**
- Day 1增加Python环境搭建
- 新增PythonExecutorServiceNode.js ↔ Python通信
- 新增executor.pyPandas代码执行器
- 技术方案Node.js child_process调用Python脚本
#### 修正2测试场景从简单到复杂 ✅
- **原问题**10个测试场景过于简单
- **已修正**15个真实医疗数据清洗场景
- 🟢 基础场景5个单步骤操作
- 🟡 中等场景5个多步骤、跨列逻辑
- 🔴 高级场景5个分组聚合、时间序列、医学规则
- **新增场景**
- 复杂条件逻辑(生存时间计算)
- 医学规则引擎(肝功能分级)
- 时间序列分析(指标变化率)
- 文本提取TNM分期
- 字符串处理(血压拆分)
#### 修正3验收标准分层 ✅
- **原标准**:总体成功率 > 80%
- **新标准**
- 基础场景成功率 > 90%
- 中等场景成功率 > 80%
- 高级场景成功率 > 60%
- 总体成功率 > 80%12/15场景
#### 修正4核心假设验证 ✅
- **新增H2**Python代码执行环境稳定可靠
- **修改H1**:从"生成代码"改为"生成代码并成功执行"
- **强调**AI生成代码 + 真实执行 + 表格刷新是差异化价值
---
### V1.12025-12-06- 架构合规性修正 ⚠️ 重要
**修正内容**:根据用户严肃提醒,修正以下违反规范的问题:
#### 修正1强制复用平台能力 ✅
- **原错误**建议自己实现Session管理、存储、日志
- **已修正**:强制使用`storage`, `logger`, `cache`, `prisma`, `LLMFactory`
- **影响章节**所有Day 1-15的代码示例
#### 修正2Session存储方式 ✅
- **原错误**:建议用`Map<sessionId, data>`内存缓存
- **已修正**Session存数据库`dc_tool_c_sessions`表)
- **原因**违反云原生规范第2条禁止内存缓存
#### 修正3文件夹结构 ✅
- **原错误**:建议创建`python-service/`、`node-service/`子文件夹
- **已修正**遵循tool-b结构`services/`, `controllers/`, `routes/`
- **参考**`backend/src/modules/dc/tool-b/`
#### 修正4技术栈说明 ✅
- **原错误**:未强调复用现有能力
- **已修正**:明确标注"已有"、"复用平台能力"
- **新增**:云原生开发规范检查清单
#### 修正5代码示例 ✅
- **原错误**Day 1-5的代码示例未体现平台服务
- **已修正**:所有代码示例都使用`import { storage } from '@/common/storage'`等
- **新增**:常见错误示例(❌ 严禁)
#### 新增章节 ✅
- **开发前必读**:强调不要重复造轮子
- **云原生规范强制要求**列出6条核心规范
- **常见错误示例**展示5个严禁的错误写法
---
**文档状态:** ✅ 已完成V1.1 架构合规性修正版)
**下一步:** 团队Review → 严格按规范开发
**负责人:** 项目经理
**创建日期:** 2025-12-06
**修订日期:** 2025-12-06架构合规性修正
**⚠️ 重要提醒**
1. 开发前必须阅读:`docs/04-开发规范/08-云原生开发规范.md`
2. 参考现有实现:`backend/src/modules/dc/tool-b/`
3. 禁止违反规范:内存缓存、本地文件存储、重复实现平台能力