docs(dc/tool-c): Complete Tool C MVP planning and TODO list

Summary:
- Update Tool C MVP Development Plan (V1.3)
  * Clarify Python execution as core feature
  * Add 15 real medical data cleaning scenarios (basic/medium/advanced)
  * Enhance System Prompt with 10 Few-shot examples
  * Discover existing Python service (extraction_service)
  * Update to extend existing service instead of rebuilding
- Create Tool C MVP Development TODO List
  * 3-week plan with 30 tasks (Day 1-15)
  * 4 core milestones with clear acceptance criteria
  * Daily checklist and risk management
  * Detailed task breakdown for each day

Key Changes:
- Python service: Extend existing extraction_service instead of new setup
- Test scenarios: 15 scenarios (5 basic + 5 medium + 5 advanced)
- Success criteria: Basic >90%, Medium >80%, Advanced >60%, Total >80%
- Development time: Reduced from 3 weeks to 2 weeks (reuse infrastructure)

Status: Planning complete, ready to start Day 1 development
This commit is contained in:
2025-12-06 11:00:44 +08:00
parent 8a17369138
commit 8be741cd52
42 changed files with 3581 additions and 0 deletions

View File

@@ -1242,3 +1242,4 @@ interface FulltextScreeningResult {

View File

@@ -356,3 +356,4 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export

View File

@@ -458,3 +458,4 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf'

View File

@@ -0,0 +1,114 @@
# **PRDTool C \- 科研数据编辑器 (The Research Editor)**
| 文档版本 | V6.0 (AI Code Interpreter 增强版) |
| :---- | :---- |
| **产品形态** | Web 端双核编辑器 (GUI 可视化操作 \+ LUI 自然语言交互) |
| **核心价值** | 将“Excel 的易用性”与“Python 的强大能力”结合。医生既可以通过鼠标点击完成微操,也可以通过自然语言指挥 AI 编写代码完成复杂的清洗任务(如长宽转换、多重插补)。 |
| **技术底座** | Node.js BFF \+ Python Server-side Sandbox \+ DeepSeek-V3 |
## **一、 产品核心理念 (Core Philosophy)**
### **1.1 双核驱动 (Dual-Core Interaction)**
* **左脑 (GUI):** 提供类似 Excel 的网格和工具栏,适合“直觉式、原子化”的操作(如手动修改一个值、排序、筛选)。
* **右脑 (AI Copilot):** 提供对话式代码解释器适合“逻辑性、批量化”的操作如“把年龄按60岁分箱”、“删除所有空行”、“计算生存时间”
### **1.2 可控黑盒 (Controllable Blackbox)**
AI 不直接修改数据,而是**生成 Python 代码**。系统在执行前展示\*\*“预操作卡片”\*\*,由用户确认执行,确保科研数据的严谨性。
## **二、 核心业务流程 (User Flow)**
数据导入 \-\> 双模式清洗 (点击工具栏 OR 对话 AI) \-\> 代码/操作执行 \-\> 实时预览更新 \-\> 版本快照 \-\> 导出结果
## **三、 功能模块详解 (Functional Requirements)**
### **1\. 界面布局 (The Workspace)**
* **P0:** **分栏布局 (Split View):**
* **左侧 (70%):** 超级网格 (The Grid),展示数据预览。
* **右侧 (30%):** 智能侧边栏 (Smart Sidebar),包含 **\[统计概览\]** 和 **\[AI 助手\]** 两个 Tab。
* **P0:** **全局状态指示:**
* 当 AI 正在思考或后端正在计算时,左侧网格显示 **“AI 处理中...”** 遮罩,并锁定编辑,防止双写冲突。
### **2\. 顶部扁平工具栏 (Flat Toolbar) —— GUI 核心**
*保留高频、标准化的操作入口,作为 AI 的补充。*
* **P0: 变量加工:**
* **生成新变量:** 弹窗公式构建器。
* **计算时间差:** 内置医学常数 (年=365.25天)。
* **生成哑变量:** 回归分析专用。
* **横纵表转换 (Pivot):** \* *交互升级:* 点击后不再只是纯前端计算,而是调用后端 Python 逻辑,支持处理更复杂的转换。
* **P0: 质量治理:**
* **查找重复值:** 按 ID 或全字段查重。
* **多重插补 (MICE):** 全局入口,调用后端 sklearn 或 fancyimpute 库。
* **P0: 样本筛选:** 构建入排标准。
### **3\. AI Copilot 智能助手 (The Brain) —— V6 核心升级**
*位于右侧侧边栏的 \[AI 助手\] Tab。*
#### **3.1 自然语言指令解析**
* **P0:** **意图识别:** 支持模糊指令如“洗一下数据”、“把男变成1”。
* **P0:** **上下文感知:** AI 能够读取当前的列名 (Metadata) 和前 5 行数据示例,理解数据含义。
#### **3.2 代码解释器模式 (Code Interpreter)**
* **P0:** **代码生成:** AI 针对用户需求,生成可执行的 **Python (Pandas)** 代码块。
* **P0:** **预操作卡片 (Action Card):**
* AI 不直接执行代码。
* 界面展示一个卡片:操作类型: 数据分箱 | 目标列: 年龄 | 代码预览。
* 按钮:**\[运行代码\]** | **\[取消\]**。
* **P0:** **执行反馈:**
* 执行成功:显示 ✅,左侧表格自动刷新。
* 执行失败AI 自动分析 Error Log尝试自我修正代码并建议重试。
#### **3.3 典型 AI 场景支持**
* **高级清洗:** “把所有列的异常值(\>3倍标准差替换为缺失值”。
* **复杂提取:** “从‘诊断’列中提取出由‘/’分隔的第二部分,生成新列”。
* **批量处理:** “删除所有缺失率超过 50% 的列”。
### **4\. 智能统计面板 (Insight Panel)**
*位于右侧侧边栏的 \[统计概览\] Tab。*
* **P0:** **列联动:** 点击左侧网格某一列,右侧自动显示该列的分布图(直方图/频次图)。
* **P0:** **快捷操作:** 图表下方直接提供“填补”、“分箱”、“映射”等快捷按钮。
### **5\. 导出与流转 (Export)**
* **P0:** **结果导出:** 支持 Excel (.xlsx) 和 SPSS (.sav) 格式。
* **P0:** **操作审计:** 导出的文件中,附带一份 **"清洗日志 (Cleaning Log)"**,记录了所有的 AI 代码和手动操作步骤(用于科研溯源)。
## **四、 数据与性能策略 (Data Strategy)**
### **4.1 性能准入 (Guardrails)**
* **文件大小限制:** 单个文件 **\< 20MB**。
* **行数限制:** 建议 **\< 50,000 行** 以保证前端渲染流畅度。
* *策略:* 后端 Python 可以处理百万行,但前端 AG Grid 仅加载前 100-1000 行作为预览Preview Mode导出时才生成全量文件。
### **4.2 安全与隐私**
* **P0:** **沙箱隔离:** AI 生成的 Python 代码必须在服务端的安全沙箱Docker/SAE中运行禁止访问外网和系统文件。
* **P0:** **数据脱敏:** 确保输入编辑器的数据已在前置环节(工具 B完成了 PII 脱敏。
## **五、 埋点与统计**
* **AI 采纳率:** 展示 Action Card 后,用户点击“运行”的比例。
* **代码报错率:** AI 生成的代码在后端执行失败的比例。
* **常用指令 Top 10:** 统计医生最常对 AI 说的话。
## **六、 附录AI 指令集示例 (Few-Shot Examples)**
| 用户指令 | AI 动作 (Action) | 生成代码逻辑 (Python Pandas) |
| :---- | :---- | :---- |
| "把性别转为数字" | Recode | df\['sex'\] \= df\['sex'\].map({'男':1, '女':0}) |
| "年龄按60分两组" | Binning | df\['age\_group'\] \= pd.cut(df\['age'\], bins=\[0,60,150\], labels=\['0','1'\]) |
| "删除没有ID的行" | Filter | df \= df.dropna(subset=\['patient\_id'\]) |
| "计算BMI" | Formula | df\['bmi'\] \= df\['weight'\] / (df\['height'\]/100)\*\*2 |
| "把每个人的一行变成多行" | Pivot/Melt | df \= df.melt(id\_vars=\['id'\], ...) |

View File

@@ -0,0 +1,107 @@
# **PRDTool C \- 科研数据编辑器 (MVP V1.1)**
| 文档版本 | V1.1 (工程细化版) |
| :---- | :---- |
| **产品形态** | **Web 端数据编辑器 (AG Grid \+ AI Chat)** |
| **核心策略** | **"AI-First" \+ "Server-side State"**。依靠 AI 生成 Python 代码完成清洗;以服务端 DataFrame 为单一数据源,前端负责渲染和轻量编辑。 |
| **技术底座** | React \+ AG Grid \+ Node.js BFF \+ Python Sandbox (FastAPI) \+ DeepSeek-V3 |
| **变更记录** | V1.1: 增加数据同步机制、撤销回滚策略、会话生命周期定义。 |
## **一、 MVP 核心目标 (Objectives)**
1. **验证闭环:** 跑通 “自然语言 \-\> Python 代码 \-\> 后端执行 \-\> 前端刷新” 的完整链路。
2. **数据一致性:** 确保手动编辑与 AI 操作不冲突,状态可追溯。
3. **可用性:** 解决中文乱码、会话超时等实际工程问题。
## **二、 详细功能需求 (Functional Requirements)**
### **1\. 界面框架与会话 (Shell & Session)**
* **P0: 左右分栏布局:** 左侧 AG Grid (70%),右侧 AI Chat (30%)。
* **P0: 会话初始化 (Session Init):**
* 用户上传文件 \-\> 后端开启 Python 进程/容器 \-\> 加载 df \-\> 返回 sessionId。
* **编码检测:** 后端必须尝试 utf-8 和 gbk 解码,防止中文乱码。
* **P1: 心跳与保活 (Keep-alive):**
* 前端每 30s 发送心跳。
* **超时策略:** 若超过 30min 无操作,后端释放内存。用户再次操作时,提示“会话已过期,请重新加载文件”。
### **2\. 超级网格 (The Grid) \- *前端交互***
* **P0: 数据展示 (View):**
* 通过 API 分页拉取数据(预览模式,仅取前 100-500 行)。
* **列类型推断:** 前端根据后端返回的 dtypes 渲染列头图标(数值/文本/日期)。
* **P0: 手动编辑 (Manual Edit):**
* 支持双击修改单元格。
* **关键逻辑(脏数据标记):** 用户修改后,单元格右标红,数据暂存在前端 dirtyRows 队列中。
* **P0: 状态锁 (UI Locking):**
* 当 AI 正在执行时Grid 变为 **只读 (Read-only)**,显示 Loading 遮罩。
### **3\. AI Copilot 智能助手 (The Brain) \- *后端驱动***
#### **3.1 交互流程 (Chat Loop)**
1. **用户输入:** “把年龄大于60的设为老年组”。
2. **前置同步 (Auto-Sync):** **(V1.1 新增)**
* **判定:** 前端检查是否有未保存的手动修改 (dirtyRows).
* **动作:** 如果有,先静默发送 PATCH /api/data 将手动修改同步给后端 df。确保 AI 基于最新数据操作。
3. **代码生成:** 后端调用 DeepSeek生成 Pandas 代码。
4. **预操作确认 (Action Card):**
* 展示:代码预览 \+ 摘要。
* 按钮:\[运行\] | \[取消\]。
#### **3.2 执行与回滚 (Execute & Rollback) \- (V1.1 核心)**
* **P0: 自动快照 (Auto-Checkpoint):**
* 在执行 exec(code) 之前,后端必须先对当前 df 进行内存快照(或序列化备份)。
* 记录操作日志ActionID: 101, Type: AI\_CODE, Code: "..."。
* **P0: 执行反馈:**
* 成功:返回新的预览数据 \-\> 刷新 Grid \-\> 添加一条“操作成功”消息。
* 失败:捕获 Python Traceback \-\> 让 AI 尝试自我修复 (Self-Correction) 一次 \-\> 若仍失败,向用户展示错误原因。
* **P0: 撤销操作 (Undo):**
* 聊天框每条成功记录下显示 \[撤销\] 按钮。
* 逻辑:调用后端 rollback(action\_id) \-\> 恢复到该操作前的快照 \-\> 刷新 Grid。
### **4\. 快捷指令 (Prompt Chips)**
* **P0:** 顶部工具栏按钮(生成变量、长宽转换等),点击仅作为“快捷短语”填入输入框,不触发独立 UI 逻辑,保持架构统一。
### **5\. 导出 (Export)**
* **P0:** 导出 Excel。
* 逻辑:后端直接将当前的 df (包含所有 AI 修改和手动修改) 写入 Excel 流并返回。
## **三、 异常处理规范 (Error Handling)**
| 异常场景 | 前端表现 | 后端处理 |
| :---- | :---- | :---- |
| **中文乱码** | 提示“编码格式识别失败,请手动选择” | 尝试 chardet 检测,失败则报错 |
| **AI 代码报错** | Chat 气泡变红显示“AI 正在尝试修复...” | 捕获 stderr将错误反喂给 AI 重试 (Max 1次) |
| **会话过期** | 全屏遮罩“会话已过期” | 清理 Redis/内存,返回 401/404 |
| **手动修改冲突** | 提示“正在同步数据...” | 优先处理手动 Patch再执行 AI 任务 |
## **四、 开发与测试重点 (QA Focus)**
1. **一致性测试:**
* 先手动改一个值,再让 AI 删一行,确认那个值还在不在(或者是否正确被删)。
* 先让 AI 改一列,再撤销,确认是否完全恢复。
2. **边界测试:**
* 上传空文件。
* 上传全中文列名的文件。
* 让 AI 计算一个不存在的列。
3. **性能测试:**
* 连续快速发送 5 条指令,确保后端是**串行队列**处理,而不是并发搞乱数据。
## **五、 附录:数据同步 API 定义 (简版)**
* POST /api/sync: 前端 \-\> 后端。发送手动修改的 Diff。
* Payload: \[{ rowId: "P001", col: "age", value: 66 }\]
* POST /api/run: 前端 \-\> 后端。发送 AI 代码执行请求。
* Payload: { code: "df\['new'\] \= 1", snapshot: true }
* POST /api/undo: 前端 \-\> 后端。
* Payload: { step: \-1 }
\#\#\# 给开发团队的一句话总结
\*\*“V1.1 的核心在于‘状态管理’。请务必保证后端 Python 内存里的 \`DataFrame\` 是唯一的‘真理来源 (Source of Truth)’。前端的所有操作(无论是手改还是 AI 改),本质上都是在向这个真理层发送指令和同步状态。”\*\*
现在,您可以拿着这份文档,很有底气地去和开发团队开工了。它已经堵上了最容易翻车的几个漏洞。

View File

@@ -0,0 +1,164 @@
# **技术设计文档:工具 C \- 科研数据编辑器 (V7 云端沙箱抗风险版)**
| 文档类型 | Technical Design Document (TDD) |
| :---- | :---- |
| **对应原型** | **工具C\_科研数据编辑器\_原型设计\_V6\_修复版.html** |
| **版本** | **V7.1** (完整收录架构决策 ADR 与红队风险对策) |
| **状态** | Final Standard |
| **核心目标** | 构建一个高可靠的云端 Python 数据清洗平台。在保障“数据不出域”的前提下,通过 Apache Arrow 和样式分离技术,解决服务端执行带来的延迟与格式丢失问题。 |
## **1\. 总体架构设计 (System Architecture)**
鉴于文件大小限制 (\<20MB) 和脱敏前提,采用 “Node.js BFF \+ Python Microservice” 架构。
V7 核心升级: 引入 Apache Arrow 作为前后端数据交换标准,替代低效的 Excel 文件反复读写,将单次交互延迟从 8s 降低至 0.5s。
### **1.1 架构拓扑图 (V7 优化版)**
graph TD
subgraph Client\_Layer \[用户端\]
ReactApp\[React 19 \+ AG Grid\]
ArrowClient\[Apache Arrow JS\]
end
subgraph Aliyun\_SAE \[阿里云 Serverless 应用引擎\]
BFF\[Node.js Web 服务 (Fastify)\]
PythonService\[Python 计算微服务 (FastAPI)\]
end
subgraph Cache\_Layer \[高速缓存层\]
Redis\_Session\[Redis (存 DataFrame Arrow 序列化)\]
end
subgraph AI\_PaaS \[AI 能力层\]
Dify\[Dify 编排引擎\]
DeepSeek\[DeepSeek-V3 模型\]
end
subgraph Cloud\_Infra \[持久化层\]
OSS\[对象存储 (存 Excel 底板)\]
RDS\[RDS PostgreSQL (存元数据)\]
end
%% 交互流
User\[用户\] \--\>|1. 上传| BFF
BFF \--\>|2. 存原始Excel| OSS
BFF \--\>|3. 预热 Session| PythonService
PythonService \--\>|4. 加载并转为 Arrow| Redis\_Session
User \--\>|5. AI 指令| Dify
Dify \--\>|6. Python 代码| BFF
BFF \--\>|7. 发送代码| PythonService
PythonService \--\>|8. 读取 Arrow (内存)| Redis\_Session
PythonService \--\>|9. Pandas 执行| PythonService
PythonService \--\>|10. 写回 Arrow| Redis\_Session
PythonService \--\>|11. 返回 Preview 数据 (JSON/Arrow)| ReactApp
User \--\>|12. 导出/保存| BFF
BFF \--\>|13. 触发合并| PythonService
PythonService \--\>|14. 读OSS底板 \+ 填入数据| OSS
## **2\. 关键架构决策记录 (ADR)**
本节记录了为何从“前端 Pyodide”转向“后端沙箱”的决策过程供团队参考。
### **决策点:前端运行 (WASM) vs 后端运行 (Server-side)**
| 维度 | 方案 A前端 Pyodide (WASM) | 方案 B后端 Python (本方案) | 决策结论 |
| :---- | :---- | :---- | :---- |
| **启动延迟** | **极慢 (15s+)**。需下载 \~20MB 引擎包,用户体验极差。 | **秒开**。环境在服务器预热,即开即用。 | **后端胜** |
| **交互延迟** | **极快 (\< 0.1s)**。本地内存操作,无网络开销。 | **中等 (0.5s)**。通过 Apache Arrow 优化后可接受。 | 前端胜 |
| **稳定性** | **高风险**。浏览器 Tab 内存有限,易 OOM 崩溃。 | **高稳定**。服务器内存充足,容器隔离,崩溃不影响前端。 | **后端胜** |
| **库支持** | **有限**。不支持部分 C 扩展库 (如复杂统计库)。 | **无限**。标准 Linux 环境,生态完整。 | **后端胜** |
| **开发难度** | **极高**。需处理 JS-Python 通信、内存管理。 | **低**。标准 Web API 开发。 | **后端胜** |
**结论:** 鉴于“数据已脱敏”且“文件较小”,**后端执行方案** 在稳定性、兼容性和开发成本上全面胜出。
## **3\. 技术选型与融合 (Tech Stack Fusion)**
### **3.1 核心组件更新**
| 领域 | 选型 | V7 新增理由 |
| :---- | :---- | :---- |
| **数据交换** | **Apache Arrow** | **关键升级**。用于 Python 和 Node.js/前端之间的高性能数据传输,避免 JSON 序列化开销,**解决 IO 延迟核心**。 |
| **Excel 处理** | **openpyxl** | 替代纯 Pandas。用于在导出时**保留原始 Excel 样式**(如颜色、边框),**解决格式丢失核心**。 |
| **会话缓存** | **Redis** | 用于暂存用户的 DataFrame (序列化为 Parquet/Arrow),避免每次操作都去 OSS 读文件。 |
| **计算服务** | **FastAPI \+ Celery** | 引入 Celery 处理异步任务,防止长计算阻塞 HTTP 线程。 |
## **4\. 逆向风险评估与对策 (Red Teaming Analysis)**
本节详细记录了“红队测试”中发现的潜在致命风险及其工程化解决方案。
### **风险一:交互延迟的“体感崩塌”**
* **逆向拷问:** 每次 AI 操作都走 OSS 下载 \-\> Pandas 读取 \-\> 计算 \-\> 上传,单次耗时可能超过 8秒用户无法忍受。
* **V7 解决方案:** **Session 驻留模式 (Memory-Resident)**
1. **初始化:** 用户上传 Excel 后,后端将其加载为 DataFrame并序列化为 **Arrow** 格式存入 Redis (TTL 30min)。
2. **增量交互:** 前端发送指令Python 从 Redis 读取 Arrow 数据(毫秒级),执行 Pandas 计算,将结果写回 Redis。
3. **轻量反馈:** 计算完成后,只返回 **前 100 行预览数据** 给前端 AG Grid 渲染。
4. **效果:** 耗时缩短至 **0.5s \- 1s**
### **风险二Excel 格式丢失 (The Format Loss)**
* **逆向拷问:** Pandas 的 to\_excel 会重置所有单元格样式,医生标注的颜色和批注会丢失。
* **V7 解决方案:** **底板分离策略 (Template Separation)**
1. **上传时:** 将原始 Excel 标记为 **"Style Template" (样式底板)**,永久保存在 OSS。
2. **计算时:** 只在内存/Redis 中处理纯数据 (Values),不关心样式。
3. **导出时:** 使用 openpyxl 加载“样式底板”,将内存中的新数据**填入**到底板的对应坐标中,保留未修改区域的背景色和批注。
### **风险三:状态同步的“双写冲突”**
* **逆向拷问:** 用户手动修改了第 5 行,同时 AI 删除了第 5 行,导致数据状态不一致。
* **V7 解决方案:** **UI 互斥锁 (UI Locking)**
* 当 AI 正在生成代码或后端正在计算时AG Grid 强制进入 **readOnly** 模式,并在界面显示 "AI 正在处理..." 遮罩,**物理层面上禁止并发操作**。
### **风险四:安全沙箱逃逸**
* **逆向拷问:** AI 生成了 import os; os.system('rm \-rf /')。
* **V7 解决方案:** **AST 静态分析 \+ 容器隔离**
* **预检:** 在执行 exec() 前,使用 Python ast 模块扫描代码树,检测到 import os 等关键词直接抛出异常。
* **资源限额:** 使用 Python resource 模块限制单次执行的 CPU 时间 (10s) 和 内存 (1GB)。
## **5\. 数据库设计 (元数据层)**
新增 TaskAudit 表用于记录每一次 AI 操作的上下文,便于回滚和审计。
model TaskAudit {
id String @id @default(uuid())
datasetId String
version Int // 操作后的版本号
actionType String // "AI\_CODE" or "MANUAL\_EDIT"
prompt String? // 用户的自然语言指令
code String? // AI 生成的 Python 代码
executionTime Int // 执行耗时 (ms)
status String // SUCCESS / FAILED
createdAt DateTime @default(now())
}
## **6\. API 接口定义 (V7 优化)**
* POST /api/session/init: 上传文件,初始化 Redis Session返回 sessionId。
* POST /api/session/execute:
* **Input:** { sessionId, code, version }
* **Output:** { previewData: ArrowBase64, newVersion: int, logs: string }
* **说明:** 仅返回预览数据,不生成 Excel 文件。
* POST /api/session/save:
* **Input:** { sessionId }
* **Output:** { downloadUrl }
* **说明:** 触发 openpyxl 合并逻辑,生成最终 Excel 并上传 OSS。
## **7\. 开发分工建议**
* **Python 组 (重中之重):**
* 实现 Arrow \<-\> Pandas 的序列化逻辑。
* 封装 openpyxl 的样式回填逻辑。
* 搭建 FastAPI \+ Redis 环境。
* **Node.js 组:**
* 负责 Dify 转发和鉴权。
* **前端组:**
* 集成 apache-arrow JS 库,解析后端返回的二进制流并在 AG Grid 展示。
* 实现“AI 处理中”的全屏锁定交互。

View File

@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工具C - 科研数据编辑器 V6 (修复版)</title>
<!-- 1. 引入 Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 2. 引入 React 和 ReactDOM (切换为 jsDelivr 源,更稳定,国内访问更快) -->
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<!-- 3. 引入 Babel (用于解析 JSX) -->
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.23.5/babel.min.js"></script>
<!-- 4. 引入 Lucide Icons (指定 UMD 版本,修复 Identifier 'Infinity' 报错) -->
<script src="https://cdn.jsdelivr.net/npm/lucide@0.460.0/dist/umd/lucide.min.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes zoomIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.animate-in { animation: fadeIn 0.3s ease-out forwards; }
.slide-in-right { animation: slideInRight 0.3s ease-out forwards; }
.zoom-in { animation: zoomIn 0.2s ease-out forwards; }
/* 滚动条美化 */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans antialiased overflow-hidden">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// --- 图标组件适配 (Safe Shim) ---
// 使用 ref 容器隔离 React 和 DOM 操作,修复图标渲染问题
const LucideIcon = ({ name, className, size = 16, ...props }) => {
const containerRef = useRef(null);
useEffect(() => {
// 确保 lucide 全局对象存在且 createIcons 方法可用
if (containerRef.current && typeof lucide !== 'undefined' && lucide.createIcons) {
containerRef.current.innerHTML = '';
const i = document.createElement('i');
i.setAttribute('data-lucide', name);
containerRef.current.appendChild(i);
lucide.createIcons({
root: containerRef.current,
nameAttr: 'data-lucide',
attrs: { class: className, width: size, height: size, ...props }
});
}
}, [name, className, size]);
return <span ref={containerRef} style={{ display: 'inline-flex', verticalAlign: 'middle' }}></span>;
};
// --- 模拟数据 ---
const INITIAL_DATA = [
{ id: 'P001', age: 45, gender: '男', bmi: 24.5, admission_date: '2023-01-12', lab_val: '4.5' },
{ id: 'P002', age: 62, gender: '女', bmi: 28.1, admission_date: '2023-01-15', lab_val: '5.1' },
{ id: 'P003', age: 205, gender: '男', bmi: null, admission_date: '2023-02-01', lab_val: '<0.1' },
{ id: 'P004', age: 58, gender: '女', bmi: 22.4, admission_date: '2023-02-10', lab_val: '4.8' },
{ id: 'P005', age: 34, gender: '男', bmi: 21.0, admission_date: '2023-03-05', lab_val: '5.2' },
{ id: 'P006', age: 71, gender: '女', bmi: null, admission_date: '2023-03-12', lab_val: '6.0' },
{ id: 'P007', age: null, gender: '男', bmi: null, admission_date: '2023-04-01', lab_val: '4.9' },
{ id: 'P008', age: 49, gender: '男', bmi: 26.8, admission_date: '2023-04-05', lab_val: '5.5' },
{ id: 'P009', age: 55, gender: '女', bmi: 23.9, admission_date: '2023-04-10', lab_val: '4.2' },
{ id: 'P010', age: 66, gender: '男', bmi: 29.1, admission_date: '2023-04-12', lab_val: '5.8' },
];
const INITIAL_COLUMNS = [
{ id: 'id', name: '病人ID', type: 'text', width: 100 },
{ id: 'age', name: '年龄', type: 'number', width: 80 },
{ id: 'gender', name: '性别', type: 'category', width: 80 },
{ id: 'bmi', name: 'BMI指数', type: 'number', width: 100 },
{ id: 'admission_date', name: '入院日期', type: 'date', width: 120 },
{ id: 'lab_val', name: '肌酐', type: 'text', width: 100 },
];
const ToolC_EditorV6 = () => {
// --- 核心状态 ---
const [data, setData] = useState(INITIAL_DATA);
const [columns, setColumns] = useState(INITIAL_COLUMNS);
const [selectedColId, setSelectedColId] = useState(null);
// 侧边栏状态 (默认打开 Chat)
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [sidebarTab, setSidebarTab] = useState('chat'); // 'chat' | 'insight'
// Chat 状态
const [messages, setMessages] = useState([
{ id: 1, role: 'system', content: '您好!我是您的 AI 数据分析师。我可以为您编写代码来清洗数据。试试说“把年龄大于60的设为老年组”。' }
]);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef(null);
// 滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(scrollToBottom, [messages, isTyping]);
// 处理发送消息 (模拟 AI 生成代码)
const handleSend = () => {
if (!inputValue.trim()) return;
const userText = inputValue;
setMessages(prev => [...prev, { id: Date.now(), role: 'user', content: userText }]);
setInputValue('');
setIsTyping(true);
// 模拟 AI 思考和生成代码
setTimeout(() => {
setIsTyping(false);
let aiResponse;
if (userText.includes('老年') || userText.includes('年龄')) {
aiResponse = {
id: Date.now(),
role: 'assistant',
content: '没问题。我将使用 Pandas 对 `age` 列进行分箱处理,生成新的 `age_group` 列。请检查以下代码并运行:',
code: {
lang: 'python',
content: `import pandas as pd\n\n# 定义分箱逻辑\ndef categorize_age(age):\n if pd.isna(age):\n return None\n return '老年组' if age > 60 else '非老年组'\n\n# 应用函数生成新列\ndf['age_group'] = df['age'].apply(categorize_age)`,
status: 'pending' // pending | running | success | error
}
};
} else {
aiResponse = {
id: Date.now(),
role: 'assistant',
content: '收到。这是一个数据清洗任务,但我目前只演示“年龄分组”的 Python 代码生成能力。您可以试试输入“把年龄大于60的标记为老年组”。'
};
}
setMessages(prev => [...prev, aiResponse]);
}, 1200);
};
// 执行代码 (模拟 Pyodide 运行)
const handleRunCode = (msgId) => {
// 1. 更新消息状态为 running
setMessages(prev => prev.map(m =>
m.id === msgId ? { ...m, code: { ...m.code, status: 'running' } } : m
));
// 2. 模拟执行延迟
setTimeout(() => {
// 3. 更新表格数据
const newData = data.map(row => ({
...row,
age_group: row.age && row.age > 60 ? '老年组' : (row.age ? '非老年组' : null)
}));
setData(newData);
// 检查列是否存在,不存在则添加
if (!columns.find(c => c.id === 'age_group')) {
setColumns(prev => [...prev, { id: 'age_group', name: 'age_group', type: 'category', width: 100 }]);
}
// 4. 更新消息状态为 success
setMessages(prev => prev.map(m =>
m.id === msgId ? { ...m, code: { ...m.code, status: 'success' } } : m
));
// 5. 追加成功提示
setMessages(prev => [...prev, {
id: Date.now(),
role: 'system',
content: '✅ 代码执行成功!已新增列 `age_group`。',
isSuccess: true
}]);
}, 1500);
};
// --- UI 组件 ---
const ToolbarButton = ({ iconName, label, colorClass = "text-slate-600 hover:bg-slate-100" }) => (
<button className={`flex flex-col items-center justify-center w-20 h-14 rounded-lg transition-all hover:shadow-sm ${colorClass}`}>
<LucideIcon name={iconName} className="w-5 h-5 mb-1" />
<span className="text-[10px] font-medium">{label}</span>
</button>
);
return (
<div className="h-screen w-screen bg-slate-50 font-sans text-slate-800 flex flex-col overflow-hidden">
{/* 1. Header */}
<header className="bg-white border-b border-slate-200 h-14 flex-none flex items-center justify-between px-4 z-20 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center text-white shadow-emerald-200 shadow-md">
<LucideIcon name="table-2" size={20} />
</div>
<span className="font-bold text-lg text-slate-900 tracking-tight">科研数据编辑器 <span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">Pro</span></span>
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
<span className="text-xs text-slate-500 font-mono">lung_cancer_2023.csv</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center bg-slate-100 rounded-lg p-1">
<button className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"><LucideIcon name="undo-2" size={16} /></button>
<button className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"><LucideIcon name="redo-2" size={16} /></button>
</div>
<button className="flex items-center gap-2 px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-all shadow-md">
<LucideIcon name="download" size={12} /> 导出结果
</button>
</div>
</header>
{/* 2. Main Workspace (Flex Layout) */}
<div className="flex-1 flex overflow-hidden">
{/* Left: Table Area (Flexible Width) */}
<div className="flex-1 flex flex-col min-w-0 bg-slate-100/50">
{/* Flat Toolbar */}
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
<ToolbarButton iconName="calculator" label="生成新变量" colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
<ToolbarButton iconName="calendar-clock" label="时间差" colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
<ToolbarButton iconName="arrow-left-right" label="横纵转换" colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100" />
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton iconName="file-search" label="查重" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
<ToolbarButton iconName="wand-2" label="多重插补" colorClass="text-rose-600 bg-rose-50 hover:bg-rose-100" />
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton iconName="filter" label="筛选分析集" colorClass="text-indigo-600 bg-indigo-50 hover:bg-indigo-100" />
<div className="flex-1"></div>
<div className="relative">
<LucideIcon name="search" size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20" placeholder="搜索值..." />
</div>
</div>
{/* Grid Container */}
<div className="flex-1 overflow-auto p-6 relative">
<div className="bg-white border border-slate-200 shadow-sm rounded-xl overflow-hidden min-w-[800px]">
{/* Grid Header */}
<div className="flex bg-slate-50 border-b border-slate-200 sticky top-0 z-10">
{columns.map(col => (
<div
key={col.id}
className={`h-10 px-4 flex items-center justify-between border-r border-slate-200 text-xs font-bold text-slate-600 cursor-pointer hover:bg-slate-100 transition-colors ${selectedColId === col.id ? 'bg-indigo-50 text-indigo-600' : ''}`}
style={{ width: col.width, flex: 'none' }}
onClick={() => {
setSelectedColId(col.id);
if (!isSidebarOpen) setIsSidebarOpen(true);
setSidebarTab('insight');
}}
>
<div className="flex items-center gap-1.5 truncate">
{col.type === 'number' && <LucideIcon name="hash" size={12} className="text-slate-400" />}
{col.type === 'text' && <LucideIcon name="type" size={12} className="text-slate-400" />}
{col.type === 'category' && <LucideIcon name="split" size={12} className="text-slate-400" />}
{col.type === 'date' && <LucideIcon name="calendar" size={12} className="text-slate-400" />}
{col.name}
</div>
{col.id === 'age_group' && <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>}
</div>
))}
</div>
{/* Grid Body */}
<div className="divide-y divide-slate-100 text-sm text-slate-700 bg-white">
{data.map((row, idx) => (
<div key={idx} className={`flex hover:bg-slate-50 transition-colors ${row.age_group ? 'bg-emerald-50/30' : ''}`}>
{columns.map(col => {
const val = row[col.id];
const isNull = val === null || val === '';
const isOutlier = col.id === 'age' && val > 120;
return (
<div
key={col.id}
className={`h-10 px-4 flex items-center border-r border-slate-100 truncate
${isNull ? 'bg-red-50' : ''}
${isOutlier ? 'bg-orange-100 text-orange-700 font-bold' : ''}
${selectedColId === col.id ? 'bg-indigo-50/10' : ''}
`}
style={{ width: col.width, flex: 'none' }}
>
{isNull ? <span className="text-[10px] text-red-400 italic">NULL</span> : val}
</div>
);
})}
</div>
))}
</div>
</div>
</div>
</div>
{/* Right: Smart Sidebar (Fixed Width, No Overlay) */}
{isSidebarOpen && (
<div className="w-[400px] flex-none border-l border-slate-200 bg-white flex flex-col shadow-xl z-20">
{/* Sidebar Tabs */}
<div className="flex border-b border-slate-200 bg-slate-50">
<button
onClick={() => setSidebarTab('chat')}
className={`flex-1 py-3 text-xs font-bold text-center border-b-2 transition-all flex items-center justify-center gap-2 ${sidebarTab === 'chat' ? 'border-purple-600 text-purple-700 bg-white' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
>
<LucideIcon name="sparkles" size={16} /> AI Copilot
</button>
<button
onClick={() => setSidebarTab('insight')}
className={`flex-1 py-3 text-xs font-bold text-center border-b-2 transition-all flex items-center justify-center gap-2 ${sidebarTab === 'insight' ? 'border-indigo-600 text-indigo-700 bg-white' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
>
<LucideIcon name="bar-chart-3" size={16} /> 统计概览
</button>
<button onClick={() => setIsSidebarOpen(false)} className="px-3 text-slate-400 hover:text-slate-600 hover:bg-slate-200 border-l border-slate-200 transition-colors">
<LucideIcon name="arrow-right" size={16} />
</button>
</div>
{/* Content Area */}
<div className="flex-1 flex flex-col overflow-hidden relative">
{/* === TAB 1: AI Chat (Code Interpreter Mode) === */}
{sidebarTab === 'chat' && (
<>
<div className="flex-1 overflow-y-auto p-4 space-y-5 bg-slate-50/50">
{messages.map((msg) => (
<div key={msg.id} className={`flex flex-col animate-in slide-in-right duration-300 ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
{/* 文本气泡 */}
<div className={`max-w-[90%] px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm ${
msg.role === 'user'
? 'bg-purple-600 text-white rounded-tr-none'
: msg.isSuccess
? 'bg-emerald-50 text-emerald-800 border border-emerald-100'
: 'bg-white text-slate-700 border border-slate-200 rounded-tl-none'
}`}>
{msg.role === 'assistant' && !msg.isSuccess && (
<div className="flex items-center gap-2 mb-2 text-purple-600 font-bold text-xs uppercase tracking-wider">
<LucideIcon name="bot" size={12} /> AI Analysis
</div>
)}
{msg.content}
</div>
{/* 代码块卡片 (Code Block) */}
{msg.code && (
<div className="mt-3 w-full bg-[#1e1e1e] rounded-xl overflow-hidden shadow-md border border-slate-300 animate-in zoom-in duration-300">
<div className="bg-[#2d2d2d] px-3 py-2 flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-slate-400">
<LucideIcon name="terminal" size={12} /> Python Environment
</div>
<span className="text-[10px] text-slate-500 font-mono">pandas available</span>
</div>
<div className="p-3 overflow-x-auto">
<pre className="text-xs font-mono text-blue-300 leading-5">
{msg.code.content}
</pre>
</div>
{/* 执行状态栏 */}
<div className="bg-[#252526] px-3 py-2 border-t border-[#333] flex items-center justify-between">
{msg.code.status === 'pending' && (
<button
onClick={() => handleRunCode(msg.id)}
className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs px-3 py-1.5 rounded transition-colors"
>
<LucideIcon name="play" size={12} className="fill-current" /> 运行代码
</button>
)}
{msg.code.status === 'running' && (
<span className="flex items-center gap-2 text-yellow-500 text-xs">
<LucideIcon name="loader-2" size={12} className="animate-spin" /> 执行中 (Pyodide)...
</span>
)}
{msg.code.status === 'success' && (
<span className="flex items-center gap-2 text-emerald-500 text-xs">
<LucideIcon name="check-circle-2" size={12} /> 执行成功 (0.4s)
</span>
)}
</div>
</div>
)}
</div>
))}
{isTyping && (
<div className="flex items-center gap-2 text-slate-400 text-xs ml-2">
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce"></span>
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce delay-100"></span>
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce delay-200"></span>
</div>
)}
<div ref={messagesEndRef}></div>
</div>
{/* Input */}
<div className="p-4 bg-white border-t border-slate-200 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
<div className="relative">
<textarea
className="w-full pl-4 pr-12 py-3 text-sm bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none resize-none shadow-inner"
rows={2}
placeholder="输入指令例如把年龄大于60的标记为老年组..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }}
/>
<button
className={`absolute right-2 bottom-2 p-2 rounded-lg transition-all ${inputValue.trim() ? 'bg-purple-600 text-white hover:bg-purple-700 shadow-md' : 'bg-slate-200 text-slate-400'}`}
disabled={!inputValue.trim()}
onClick={handleSend}
>
<LucideIcon name="arrow-right" size={16} />
</button>
</div>
<div className="text-[10px] text-slate-400 mt-2 text-center flex items-center justify-center gap-1">
<LucideIcon name="settings" size={12} /> 代码在本地浏览器沙箱运行数据不上传云端
</div>
</div>
</>
)}
{/* === TAB 2: Insight Panel (统计概览) === */}
{sidebarTab === 'insight' && (
<div className="flex-1 overflow-y-auto p-5 space-y-6 animate-in">
{selectedColId ? (
<>
<div className="p-4 bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="text-xs text-slate-500 uppercase tracking-wider mb-1">SELECTED COLUMN</div>
<h3 className="text-xl font-bold text-slate-900">{columns.find(c => c.id === selectedColId)?.name}</h3>
<div className="text-xs font-mono text-slate-400 mt-1">{selectedColId}</div>
</div>
{/* Mock Charts */}
<div className="space-y-3">
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<LucideIcon name="bar-chart-3" size={16} className="text-slate-500" /> 分布概览
</h4>
<div className="h-32 bg-slate-50 rounded-lg border border-slate-100 flex items-end justify-between px-2 pb-2 gap-1">
{[20, 45, 30, 60, 80, 50, 20, 10].map((h, i) => (
<div key={i} className="flex-1 bg-indigo-200 hover:bg-indigo-400 transition-colors rounded-t" style={{height: `${h}%`}}></div>
))}
</div>
</div>
<div className="space-y-2 pt-4 border-t border-slate-100">
<button className="w-full text-left px-4 py-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-indigo-300 transition-all">
<LucideIcon name="split" size={16} className="text-indigo-600" />
<div><div className="font-bold">生成分类变量</div><div className="text-[10px] text-slate-500">Binning</div></div>
</button>
<button className="w-full text-left px-4 py-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-indigo-300 transition-all">
<LucideIcon name="wand-2" size={16} className="text-rose-600" />
<div><div className="font-bold">缺失值填补</div><div className="text-[10px] text-slate-500">Imputation</div></div>
</button>
</div>
</>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 text-sm opacity-60">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
<LucideIcon name="bar-chart-3" size={32} />
</div>
<p>请点击表格列头<br/>查看详细统计</p>
</div>
)}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ToolC_EditorV6 />);
</script>
</body>
</html>

View File

@@ -0,0 +1,620 @@
# 工具C MVP开发 - To-do List
> **文档版本**v1.0
> **创建日期**2025-12-06
> **预计工期**3周15个工作日
> **参考文档**[工具C_MVP开发计划_V1.0.md](./工具C_MVP开发计划_V1.0.md)
---
## 📊 整体进度概览
| 阶段 | 任务数 | 已完成 | 进行中 | 待开始 | 完成率 |
|------|-------|-------|-------|-------|--------|
| **Week 1: 基础架构** | 12 | 0 | 0 | 12 | 0% |
| **Week 2: 核心功能** | 10 | 0 | 0 | 10 | 0% |
| **Week 3: 测试优化** | 8 | 0 | 0 | 8 | 0% |
| **总计** | **30** | **0** | **0** | **30** | **0%** |
---
## 🎯 核心里程碑(必须完成)
- [ ] **M1**Python代码执行环境搭建完成Day 1
- [ ] **M2**AI生成代码能力验证通过Day 7
- [ ] **M3**:基础场景成功率 > 90%Day 12
- [ ] **M4**:总体成功率 > 80%Day 15
---
## 📅 Week 1基础架构搭建Day 1-5
### Day 1Python服务扩展 + 环境验证 ⭐
#### Python微服务扩展
- [ ] **P0** 创建 `extraction_service/services/dc_executor.py`
- [ ] 实现 `validate_code(code)` - AST静态检查
- [ ] 实现 `execute_pandas_code(data, code)` - 代码执行
- [ ] 添加危险模块黑名单os、sys、subprocess等
- [ ] 添加超时保护30秒
- [ ] 添加异常捕获和错误消息
- [ ] **P0** 扩展 `extraction_service/main.py`
- [ ] 添加 `POST /api/dc/execute` 端点
- [ ] 添加 `POST /api/dc/validate` 端点
- [ ] 添加请求日志记录
- [ ] 添加错误处理中间件
- [ ] **P0** 测试Python服务
- [ ] 启动服务:`cd extraction_service && venv\Scripts\activate && uvicorn main:app --reload`
- [ ] 测试健康检查:`GET http://localhost:8000/api/health`
- [ ] 测试代码验证发送简单Pandas代码
- [ ] 测试代码执行:发送真实数据+代码
- [ ] 验证AST拦截发送危险代码`import os`
#### Node.js后端集成
- [ ] **P0** 创建后端文件夹结构
```
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 # 前置验证
```
- [ ] **P0** 实现 `PythonExecutorService.ts`
- [ ] 实现 `executeCode(data, code)` 方法
- [ ] 实现 `validateCode(code)` 方法
- [ ] 添加超时控制30秒
- [ ] 添加日志记录(复用 `@/common/logging`
- [ ] 添加错误处理和重试机制
- [ ] **P1** 环境变量配置
- [ ] 添加 `EXTRACTION_SERVICE_URL=http://localhost:8000` 到 `.env`
- [ ] 验证环境变量加载
#### 验收标准
- [ ] Python服务能成功执行简单Pandas代码如 `df['age'] > 60`
- [ ] AST检查能拦截危险代码如 `import os`
- [ ] Node.js能成功调用Python服务并获取结果
- [ ] 所有日志正常输出到控制台
---
### Day 2数据库 + Session管理
#### 数据库Schema设计
- [ ] **P0** 创建Prisma Schema`prisma/schema.prisma`
```prisma
// @@schema("dc")
model DcToolCSession {
id String @id @default(uuid())
userId String
fileName String
fileKey String // OSS存储key
totalRows Int
totalCols Int
columns Json // 列名数组
encoding String? // 编码格式
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("dc_tool_c_sessions")
}
```
- [ ] **P0** 执行数据库迁移
```bash
npx prisma db push
npx prisma generate
```
- [ ] **P1** 验证表创建成功
```sql
SELECT * FROM dc.dc_tool_c_sessions LIMIT 1;
```
#### SessionService实现
- [ ] **P0** 实现 `SessionService.ts`
- [ ] `createSession(userId, fileName, fileBuffer)` - 创建会话
- [ ] 上传Excel到OSS复用 `storage.uploadBuffer`
- [ ] 解析Excel到JSON使用xlsx库
- [ ] 保存元数据到数据库prisma
- [ ] `getSession(sessionId)` - 获取会话
- [ ] 从数据库读取元数据
- [ ] 从OSS下载数据如需要
- [ ] `deleteSession(sessionId)` - 删除会话
- [ ] 删除OSS文件
- [ ] 删除数据库记录
- [ ] **P1** 添加Excel解析逻辑
- [ ] 使用 `xlsx` 库读取Excel
- [ ] 转换为JSON格式数组对象
- [ ] 检测编码(中文支持)
- [ ] 提取列名和数据类型
#### 验收标准
- [ ] 能成功上传10MB以内的Excel文件
- [ ] 数据正确保存到OSS零落盘
- [ ] Session元数据正确存储到数据库
- [ ] 能通过sessionId检索到完整数据
---
### Day 3AI代码生成服务
#### AICodeService实现
- [ ] **P0** 实现 `AICodeService.ts`
- [ ] 集成LLMFactory复用 `@/common/llm`
- [ ] 实现 `generateCode(prompt, dataContext)` 方法
- [ ] 构建System Prompt包含10个Few-shot示例
- [ ] 注入数据上下文(行数、列名、样本数据)
- [ ] 调用LLM生成代码
- [ ] 提取纯代码去除Markdown格式
- [ ] 实现 `fixCode(originalCode, errorMsg, dataContext)` 方法
- [ ] AI自我修复逻辑
- [ ] 最多重试1次
- [ ] **P0** System Prompt设计
- [ ] 基础场景示例5个
- [ ] 中等场景示例3个
- [ ] 高级场景示例2个
- [ ] 安全规范说明
- [ ] 输出格式要求
- [ ] **P1** 代码提取逻辑
- [ ] 识别 ` ```python ... ``` ` 格式
- [ ] 识别纯代码格式
- [ ] 去除注释和说明
#### 验收标准
- [ ] AI能生成正确的Pandas代码基础场景
- [ ] 生成的代码符合安全规范(无危险导入)
- [ ] 能正确处理中文列名
- [ ] 代码提取准确率 > 95%
---
### Day 4前端基础框架
#### 前端文件夹结构
- [ ] **P0** 创建前端目录
```
frontend-v2/src/modules/dc/pages/tool-c/
├── index.tsx # 主页面入口
├── components/
│ ├── DataTable.tsx # AG Grid数据表格
│ ├── AICopilot.tsx # AI对话侧边栏
│ ├── FileUploader.tsx # 文件上传
│ ├── Toolbar.tsx # 顶部工具栏
│ └── ChatMessage.tsx # 对话消息组件
├── hooks/
│ ├── useSession.ts # Session管理
│ └── useAIChat.ts # AI对话
├── types.ts # TypeScript类型定义
└── api.ts # API封装
```
- [ ] **P0** 安装依赖
```bash
cd frontend-v2
npm install ag-grid-react ag-grid-community xlsx
```
#### 主页面布局
- [ ] **P0** 实现 `index.tsx`(主布局)
- [ ] 左侧数据表格区域70%宽度)
- [ ] 右侧AI Copilot侧边栏30%宽度)
- [ ] 顶部:扁平工具栏(文件上传、导出、撤销)
- [ ] 状态管理useState/useReducer
- [ ] **P1** 实现 `FileUploader.tsx`
- [ ] 拖拽上传支持
- [ ] 文件类型验证仅Excel
- [ ] 文件大小限制10MB
- [ ] 上传进度显示
#### 验收标准
- [ ] 页面布局正确左表格右AI
- [ ] 能成功上传Excel文件
- [ ] 上传后能看到加载状态
- [ ] 响应式布局最小宽度1280px
---
### Day 5数据表格实现AG Grid
#### DataTable组件
- [ ] **P0** 实现 `DataTable.tsx`
- [ ] 集成AG Grid
- [ ] 动态列定义根据Excel自动生成
- [ ] 单元格编辑功能
- [ ] 脏数据标记(黄色高亮)
- [ ] 分页支持每页100行
- [ ] **P1** 配置AG Grid主题
- [ ] 使用 `ag-theme-alpine`
- [ ] 自定义样式Ant Design风格
- [ ] 列宽自适应
- [ ] **P1** 表格功能
- [ ] 列排序
- [ ] 列筛选
- [ ] 行选择(多选)
- [ ] 导出CSVAG Grid内置
#### 验收标准
- [ ] 能正确显示Excel数据100行+
- [ ] 列宽自适应且可手动调整
- [ ] 单元格编辑后有黄色标记
- [ ] 表格性能流畅1000行不卡顿
---
## 📅 Week 2核心功能实现Day 6-10
### Day 6AI对话UI
#### AICopilot组件
- [ ] **P0** 实现 `AICopilot.tsx`
- [ ] 对话消息列表(滚动)
- [ ] 输入框多行支持Enter发送
- [ ] 发送按钮
- [ ] 加载状态AI思考中...
- [ ] **P0** 实现 `ChatMessage.tsx`
- [ ] 用户消息(右对齐,蓝色)
- [ ] AI消息左对齐灰色
- [ ] 代码块高亮(使用 `react-syntax-highlighter`
- [ ] 时间戳显示
- [ ] **P1** 消息历史管理
- [ ] 保存到localStorage
- [ ] 最多保存50条
- [ ] 清空历史按钮
#### 验收标准
- [ ] 对话界面美观(参考原型设计)
- [ ] 消息发送/接收流畅
- [ ] 代码块正确高亮
- [ ] 滚动到最新消息
---
### Day 7AI生成代码集成 ⭐
#### API集成
- [ ] **P0** 实现 `api.ts`
- [ ] `uploadFile(file)` - 上传Excel
- [ ] `sendMessage(sessionId, message)` - 发送AI消息
- [ ] `executeCode(sessionId, code)` - 执行代码
- [ ] `getSessionData(sessionId)` - 获取数据
- [ ] **P0** 后端API实现`ToolCController.ts`
- [ ] `POST /api/v1/dc/tool-c/upload` - 文件上传
- [ ] `POST /api/v1/dc/tool-c/chat` - AI对话
- [ ] `POST /api/v1/dc/tool-c/execute` - 执行代码
- [ ] `GET /api/v1/dc/tool-c/sessions/:id` - 获取会话
#### 业务逻辑实现
- [ ] **P0** 实现完整流程
1. [ ] 用户发送消息 → AICodeService生成代码
2. [ ] 前端展示生成的代码Markdown格式
3. [ ] 用户点击"执行"按钮 → 调用Python服务
4. [ ] 执行成功 → 刷新表格数据
5. [ ] 执行失败 → AI自我修复 → 重试
- [ ] **P1** 错误处理
- [ ] AST检查失败 → 提示用户
- [ ] 执行超时30秒 → 提示用户
- [ ] AI生成失败 → 重试机制
#### 验收标准
- [ ] **基础场景测试5个成功率 > 90%**
- [ ] "把年龄大于60的标记为老年组"
- [ ] "删除所有患者ID为空的行"
- [ ] "把性别转为数字男1女0"
- [ ] "计算BMI = 体重 / (身高/100)^2"
- [ ] "删除缺失率超过50%的列"
---
### Day 8UI锁定机制
#### 互斥锁实现
- [ ] **P0** 前端状态管理
- [ ] 添加 `isAIProcessing` 状态
- [ ] AI对话中 → 锁定表格编辑
- [ ] 显示友好提示:"AI正在处理请稍候..."
- [ ] **P0** 表格锁定逻辑
- [ ] `isAIProcessing=true` → AG Grid设置为只读
- [ ] 禁用工具栏按钮(导出除外)
- [ ] 显示半透明蒙层
- [ ] **P1** 视觉反馈
- [ ] 表格半透明opacity: 0.6
- [ ] 显示加载动画
- [ ] 顶部显示进度条
#### 验收标准
- [ ] AI处理时表格无法编辑
- [ ] 锁定状态有明显的视觉反馈
- [ ] AI完成后表格自动解锁
- [ ] 用户体验流畅(无卡顿)
---
### Day 9自动检查点Checkpoint
#### 数据快照管理
- [ ] **P0** 实现检查点逻辑
- [ ] 每次AI执行成功 → 自动保存快照
- [ ] 最多保存10个检查点
- [ ] 快照数据存储到OSS压缩JSON
- [ ] **P0** 回滚功能
- [ ] 工具栏添加"撤销"按钮
- [ ] 点击撤销 → 恢复到上一个检查点
- [ ] 最多支持10次撤销
- [ ] **P1** 检查点列表UI
- [ ] 侧边栏显示检查点列表
- [ ] 每个检查点显示:时间、操作描述
- [ ] 点击检查点 → 恢复到该状态
#### 验收标准
- [ ] 每次AI操作后自动保存检查点
- [ ] 撤销功能正常工作
- [ ] 快照数据正确存储到OSS
- [ ] 10个检查点后自动删除最旧的
---
### Day 10Excel导出
#### 导出功能实现
- [ ] **P0** 后端导出API
- [ ] `POST /api/v1/dc/tool-c/export/:sessionId`
- [ ] 使用 `openpyxl`Python或 `xlsx`Node.js
- [ ] 保留原始Excel格式可选MVP可跳过
- [ ] **P0** 前端导出按钮
- [ ] 工具栏添加"导出"按钮
- [ ] 点击 → 下载Excel文件
- [ ] 文件名:`原文件名_cleaned_YYYYMMDD.xlsx`
- [ ] **P1** 导出选项(可选)
- [ ] 仅导出修改的行
- [ ] 保留样式(复杂,可延后)
#### 验收标准
- [ ] 能成功导出Excel文件
- [ ] 导出的数据与表格一致
- [ ] 文件名正确
- [ ] 下载速度快(< 3秒
---
## 📅 Week 3测试优化Day 11-15
### Day 11中等场景测试 🟡
#### 测试用例执行
- [ ] **P0** 准备测试数据
- [ ] 创建测试Excel文件包含多个列
- [ ] 包含真实医疗数据示例
- [ ] **P0** 中等场景测试5个
1. [ ] "把诊断日期和出院日期计算天数差,如果出院日期早于诊断日期则标记为异常"
2. [ ] "根据白细胞、中性粒细胞、淋巴细胞三个指标计算NLR并按2.5分为高低两组"
3. [ ] "从病理报告列中提取TNM分期生成新列"
4. [ ] "把血压列的'120/80'格式拆分成收缩压和舒张压,并判断是否高血压"
5. [ ] "删除重复的患者ID保留最新的一条记录根据就诊日期"
#### 优化AI Prompt
- [ ] **P1** 根据失败案例优化Prompt
- [ ] 增加错误处理示例
- [ ] 强化中文列名处理
- [ ] 增加边界情况说明
#### 验收标准
- [ ] **中等场景成功率 > 80%4/5成功**
---
### Day 12高级场景测试 🔴
#### 高级场景测试5个
- [ ] **P0** 复杂场景测试
1. [ ] "对于每个患者,找出第一次化疗日期和最后一次化疗日期,计算化疗持续时间"
2. [ ] "生成生存状态和生存时间"(复杂条件逻辑)
3. [ ] "根据ALT、AST、ALP、TBIL判断肝功能分级"
4. [ ] "按患者ID分组计算每次随访相比上次的肿瘤大小变化率"
5. [ ] "根据入院时间,计算季节变量,统计不同季节的发病人数"
#### Prompt深度优化
- [ ] **P1** 针对失败场景优化
- [ ] 增加分组聚合示例
- [ ] 增加时间序列示例
- [ ] 增加医学规则示例
#### 验收标准
- [ ] **高级场景成功率 > 60%3/5成功**
- [ ] **总体成功率 > 80%12/15成功**
---
### Day 13性能优化
#### 性能测试
- [ ] **P1** 测试性能指标
- [ ] 文件上传速度10MB文件 < 5秒
- [ ] AI代码生成速度< 10秒
- [ ] Python执行速度< 5秒
- [ ] 表格刷新速度(< 2秒
- [ ] 端到端流程(< 20秒
#### 优化措施
- [ ] **P1** 前端优化
- [ ] AG Grid虚拟滚动
- [ ] React.memo优化渲染
- [ ] 防抖输入debounce 300ms
- [ ] **P1** 后端优化
- [ ] 数据压缩OSS上传前
- [ ] 缓存会话数据Redis可选
- [ ] 并发控制(限制同时执行数)
#### 验收标准
- [ ] 端到端操作 < 20秒
- [ ] 1000行数据表格不卡顿
- [ ] 无内存泄漏
---
### Day 14集成测试
#### 端到端测试
- [ ] **P0** 完整流程测试10次
1. [ ] 上传Excel文件
2. [ ] 查看数据表格
3. [ ] 发送AI指令
4. [ ] 执行生成的代码
5. [ ] 验证表格刷新
6. [ ] 测试撤销功能
7. [ ] 导出Excel文件
8. [ ] 验证导出数据正确
- [ ] **P1** 异常场景测试
- [ ] 上传损坏的Excel文件
- [ ] 发送空消息
- [ ] 执行危险代码(应被拦截)
- [ ] 网络断开恢复
#### Bug修复
- [ ] **P1** 修复测试中发现的问题
- [ ] 记录所有bug到GitHub Issues
- [ ] 按优先级修复P0 > P1 > P2
#### 验收标准
- [ ] 端到端流程100%通过
- [ ] 无P0级别bug
- [ ] P1级别bug < 3个
---
### Day 15MVP验收 🎉
#### 最终验收测试
- [ ] **P0** 15个场景全覆盖测试
- [ ] 基础场景5/5 ✅
- [ ] 中等场景4/5 ✅
- [ ] 高级场景3/5 ✅
- [ ] **总体成功率 > 80%**
- [ ] **P0** 非功能性验收
- [ ] 性能:端到端 < 20秒 ✅
- [ ] 安全AST拦截危险代码 ✅
- [ ] 稳定10次测试无崩溃 ✅
- [ ] 易用:用户能独立完成任务 ✅
#### 文档完善
- [ ] **P1** 更新文档
- [ ] 用户使用手册
- [ ] API接口文档
- [ ] 部署指南
- [ ] 已知问题清单
#### 验收决策
- [ ] ✅ **通过**:总体成功率 > 80%,进入下一阶段
- [ ] ❌ **失败**:总体成功率 < 60%需要Pivot到模板库模式
---
## 🔥 每日站会检查清单
### 每天工作开始前
- [ ] 查看昨日遗留问题
- [ ] 确认今日任务清单
- [ ] 检查环境是否正常Python服务、数据库、前端dev server
### 每天工作结束前
- [ ] 更新To-do List进度
- [ ] 提交代码到Git✅ 重要!防止丢失)
- [ ] 记录遇到的问题和解决方案
- [ ] 规划明日任务
---
## ⚠️ 风险提示
### 技术风险
1. **AI生成质量不稳定**
- 缓解多轮测试优化Prompt
- 备选:失败后人工编写模板
2. **Python执行安全问题**
- 缓解AST静态检查 + 超时控制
- 备选Docker沙箱隔离Phase 2
3. **性能不达标**
- 缓解:分步优化,设定性能基准
- 备选降低数据量要求10MB → 5MB
### 进度风险
1. **AI生成成功率 < 60%**
- 应对紧急会议决定是否Pivot
2. **前端开发延期**
- 应对简化UI聚焦核心功能
---
## 📊 成功标准(最终验收)
| 指标 | 目标 | 当前 | 状态 |
|------|------|------|------|
| **基础场景成功率** | > 90% | 0% | 🔴 |
| **中等场景成功率** | > 80% | 0% | 🔴 |
| **高级场景成功率** | > 60% | 0% | 🔴 |
| **总体成功率** | > 80% | 0% | 🔴 |
| **端到端性能** | < 20秒 | 0秒 | 🔴 |
| **代码安全性** | 100%拦截 | 0% | 🔴 |
---
## 🎯 下一步行动(启动开发)
### 立即开始Day 1
1. **扩展Python服务**2小时
```bash
cd AIclinicalresearch/extraction_service
# 创建dc_executor.py
# 扩展main.py添加2个端点
# 测试服务
```
2. **创建后端文件夹**1小时
```bash
cd AIclinicalresearch/backend/src/modules/dc
mkdir -p tool-c/services tool-c/controllers tool-c/routes tool-c/utils
```
3. **实现PythonExecutorService**2小时
- 编写代码
- 单元测试
- 集成测试
---
**准备好了吗让我们开始Day 1的开发** 🚀

View File

@@ -295,3 +295,4 @@ Changes:
**版本**: V1.0

View File

@@ -385,3 +385,4 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建

View File

@@ -360,3 +360,4 @@ const mockAssets: Asset[] = [
**文档版本**: V1.0

View File

@@ -344,3 +344,4 @@ frontend-v2/src/modules/dc/
**UI质量**: ⭐⭐⭐⭐⭐
**状态**: ✅ 已完成,待测试

View File

@@ -258,3 +258,4 @@ ConflictDetectionService // 冲突检测(字段级对比)
**文档创建时间:** 2025-12-03
**维护者:** DC模块开发团队

View File

@@ -307,3 +307,4 @@
**UI质量**: ⭐⭐⭐⭐⭐
**用户体验**: ⭐⭐⭐⭐⭐

View File

@@ -270,3 +270,4 @@
**测试状态**: ✅ 待用户验证
**代码质量**: ⭐⭐⭐⭐⭐

View File

@@ -334,3 +334,4 @@
**测试完成后,请更新此文档并标记所有测试点的完成状态!**

View File

@@ -422,3 +422,4 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发
*本报告将持续更新,随着测试进展补充更多测试结果*

View File

@@ -268,3 +268,4 @@
**更新人**: AI Assistant
**下次更新**: 测试完成后

View File

@@ -199,3 +199,4 @@ $ node scripts/check-dc-tables.mjs
**下次验证**: 不需要(除非重建数据库)

View File

@@ -432,3 +432,4 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}${f.desc}`).join('\n')}
**文档维护:** 每次处理技术债务时更新此文档