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:
114
docs/03-业务模块/DC-数据清洗整理/01-需求分析/PRD_工具C_科研数据编辑器_V6.md
Normal file
114
docs/03-业务模块/DC-数据清洗整理/01-需求分析/PRD_工具C_科研数据编辑器_V6.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# **PRD:Tool 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'\], ...) |
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# **PRD:Tool 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 改),本质上都是在向这个真理层发送指令和同步状态。”\*\*
|
||||
|
||||
现在,您可以拿着这份文档,很有底气地去和开发团队开工了。它已经堵上了最容易翻车的几个漏洞。
|
||||
@@ -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 处理中”的全屏锁定交互。
|
||||
479
docs/03-业务模块/DC-数据清洗整理/03-UI设计/工具C_原型设计V6 .html
Normal file
479
docs/03-业务模块/DC-数据清洗整理/03-UI设计/工具C_原型设计V6 .html
Normal 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>
|
||||
620
docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_MVP开发_TODO清单.md
Normal file
620
docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_MVP开发_TODO清单.md
Normal 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 1:Python服务扩展 + 环境验证 ⭐
|
||||
|
||||
#### 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 3:AI代码生成服务
|
||||
|
||||
#### 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** 表格功能
|
||||
- [ ] 列排序
|
||||
- [ ] 列筛选
|
||||
- [ ] 行选择(多选)
|
||||
- [ ] 导出CSV(AG Grid内置)
|
||||
|
||||
#### 验收标准
|
||||
- [ ] 能正确显示Excel数据(100行+)
|
||||
- [ ] 列宽自适应且可手动调整
|
||||
- [ ] 单元格编辑后有黄色标记
|
||||
- [ ] 表格性能流畅(1000行不卡顿)
|
||||
|
||||
---
|
||||
|
||||
## 📅 Week 2:核心功能实现(Day 6-10)
|
||||
|
||||
### Day 6:AI对话UI
|
||||
|
||||
#### AICopilot组件
|
||||
- [ ] **P0** 实现 `AICopilot.tsx`
|
||||
- [ ] 对话消息列表(滚动)
|
||||
- [ ] 输入框(多行,支持Enter发送)
|
||||
- [ ] 发送按钮
|
||||
- [ ] 加载状态(AI思考中...)
|
||||
|
||||
- [ ] **P0** 实现 `ChatMessage.tsx`
|
||||
- [ ] 用户消息(右对齐,蓝色)
|
||||
- [ ] AI消息(左对齐,灰色)
|
||||
- [ ] 代码块高亮(使用 `react-syntax-highlighter`)
|
||||
- [ ] 时间戳显示
|
||||
|
||||
- [ ] **P1** 消息历史管理
|
||||
- [ ] 保存到localStorage
|
||||
- [ ] 最多保存50条
|
||||
- [ ] 清空历史按钮
|
||||
|
||||
#### 验收标准
|
||||
- [ ] 对话界面美观(参考原型设计)
|
||||
- [ ] 消息发送/接收流畅
|
||||
- [ ] 代码块正确高亮
|
||||
- [ ] 滚动到最新消息
|
||||
|
||||
---
|
||||
|
||||
### Day 7:AI生成代码集成 ⭐
|
||||
|
||||
#### 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 8:UI锁定机制
|
||||
|
||||
#### 互斥锁实现
|
||||
- [ ] **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 10:Excel导出
|
||||
|
||||
#### 导出功能实现
|
||||
- [ ] **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 15:MVP验收 🎉
|
||||
|
||||
#### 最终验收测试
|
||||
- [ ] **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的开发!** 🚀
|
||||
|
||||
2061
docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_MVP开发计划_V1.0.md
Normal file
2061
docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_MVP开发计划_V1.0.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -295,3 +295,4 @@ Changes:
|
||||
**版本**: V1.0
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -385,3 +385,4 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -360,3 +360,4 @@ const mockAssets: Asset[] = [
|
||||
**文档版本**: V1.0
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -344,3 +344,4 @@ frontend-v2/src/modules/dc/
|
||||
**UI质量**: ⭐⭐⭐⭐⭐
|
||||
**状态**: ✅ 已完成,待测试
|
||||
|
||||
|
||||
|
||||
@@ -304,3 +304,4 @@
|
||||
**文档版本**: V1.0
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -258,3 +258,4 @@ ConflictDetectionService // 冲突检测(字段级对比)
|
||||
**文档创建时间:** 2025-12-03
|
||||
**维护者:** DC模块开发团队
|
||||
|
||||
|
||||
|
||||
@@ -307,3 +307,4 @@
|
||||
**UI质量**: ⭐⭐⭐⭐⭐
|
||||
**用户体验**: ⭐⭐⭐⭐⭐
|
||||
|
||||
|
||||
|
||||
@@ -270,3 +270,4 @@
|
||||
**测试状态**: ✅ 待用户验证
|
||||
**代码质量**: ⭐⭐⭐⭐⭐
|
||||
|
||||
|
||||
|
||||
@@ -334,3 +334,4 @@
|
||||
|
||||
**测试完成后,请更新此文档并标记所有测试点的完成状态!**
|
||||
|
||||
|
||||
|
||||
@@ -422,3 +422,4 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发
|
||||
*本报告将持续更新,随着测试进展补充更多测试结果*
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -268,3 +268,4 @@
|
||||
**更新人**: AI Assistant
|
||||
**下次更新**: 测试完成后
|
||||
|
||||
|
||||
|
||||
@@ -199,3 +199,4 @@ $ node scripts/check-dc-tables.mjs
|
||||
**下次验证**: 不需要(除非重建数据库)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -432,3 +432,4 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}:${f.desc}`).join('\n')}
|
||||
|
||||
**文档维护:** 每次处理技术债务时更新此文档
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user