feat(ssa): Complete SSA-Pro MVP development plan v1.3
Summary: - Add PRD and architecture design V4 (Brain-Hand model) - Complete 5 development guide documents - Pass 3 rounds of team review (v1.0 -> v1.3) - Add module status guide document - Update system status document Key Features: - Brain-Hand architecture: Node.js + R Docker - Statistical guardrails with auto degradation - HITL workflow: PlanCard -> ExecutionTrace -> ResultCard - Mixed data protocol: inline vs OSS - Reproducible R code delivery MVP Scope: 10 statistical tools Status: Design 100%, ready for development Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
209
docs/03-业务模块/SSA-智能统计分析/04-开发计划/00-MVP开发计划总览.md
Normal file
209
docs/03-业务模块/SSA-智能统计分析/04-开发计划/00-MVP开发计划总览.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# SSA-Pro MVP 开发计划总览
|
||||
|
||||
> **文档版本:** v1.3
|
||||
> **创建日期:** 2026-02-18
|
||||
> **最后更新:** 2026-02-18(纳入 V3.0 终极审查建议)
|
||||
> **项目代号:** SSA (Smart Statistical Analysis)
|
||||
> **MVP 目标:** 打通完整闭环,上线 10 个核心统计工具
|
||||
|
||||
---
|
||||
|
||||
## 1. MVP 范围定义
|
||||
|
||||
### 1.1 包含内容 ✅
|
||||
|
||||
| 类别 | 内容 |
|
||||
|------|------|
|
||||
| **统计工具** | 10 个高频工具(T检验、ANOVA、卡方、相关性等) |
|
||||
| **核心流程** | 上传数据 → AI规划 → 用户确认 → R执行 → 结果交付 |
|
||||
| **交互能力** | 计划确认卡片、执行路径树、结果展示、代码下载 |
|
||||
| **智能能力** | RAG工具检索、Planner规划、Critic结果解读 |
|
||||
| **数据安全** | LLM只看Schema,R服务处理真实数据 |
|
||||
|
||||
### 1.2 不包含内容 ❌
|
||||
|
||||
| 类别 | 说明 | 后续阶段 |
|
||||
|------|------|---------|
|
||||
| 50+ 工具量产 | MVP只做10个核心工具 | Phase 3 |
|
||||
| 跨模块 Skills 化 | 不实现 Global Skill Registry | V2.0 |
|
||||
| Word 报告导出 | 先实现代码下载 | Phase 3 |
|
||||
| 大文件 OSS 传输 | MVP 限制 2MB 以内 | Phase 3 |
|
||||
|
||||
### 1.3 MVP 工具清单(10个)
|
||||
|
||||
| 序号 | 工具代码 | 名称 | 类别 |
|
||||
|------|---------|------|------|
|
||||
| 1 | ST_T_TEST_IND | 独立样本 T 检验 | 假设检验 |
|
||||
| 2 | ST_T_TEST_PAIRED | 配对样本 T 检验 | 假设检验 |
|
||||
| 3 | ST_ANOVA_ONE | 单因素方差分析 | 假设检验 |
|
||||
| 4 | ST_CHI_SQUARE | 卡方检验 | 假设检验 |
|
||||
| 5 | ST_FISHER | Fisher 精确检验 | 假设检验 |
|
||||
| 6 | ST_WILCOXON | Wilcoxon 秩和检验 | 非参数检验 |
|
||||
| 7 | ST_MANN_WHITNEY | Mann-Whitney U 检验 | 非参数检验 |
|
||||
| 8 | ST_CORRELATION | Pearson/Spearman 相关 | 相关分析 |
|
||||
| 9 | ST_LINEAR_REG | 简单线性回归 | 回归分析 |
|
||||
| 10 | ST_DESCRIPTIVE | 描述性统计 | 基础统计 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 前端 (React 19) │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 数据上传 │ │ 计划卡片 │ │ 执行路径 │ │ 结果展示 │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ HTTP API
|
||||
┌────────────────────────────┴────────────────────────────────────┐
|
||||
│ Node.js 后端 (Brain) │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ SSA Orchestrator (编排服务) │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ Rewriter│→ │ RAG检索 │→ │ Planner │→ │ Critic │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ 只看 Schema,不看真实数据 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ HTTP (内网)
|
||||
┌────────────────────────────┴────────────────────────────────────┐
|
||||
│ R 统计服务 (Hand) │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Plumber API Gateway │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ 护栏检查 │→ │ 核心计算 │→ │ 代码生成 │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ 处理真实数据,网络隔离 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 里程碑与时间线
|
||||
|
||||
### Phase 1:骨架搭建(Week 1-2)
|
||||
|
||||
**目标:** 跑通 T 检验的 "Hello World"
|
||||
|
||||
| 交付物 | 验收标准 |
|
||||
|--------|---------|
|
||||
| R Docker 镜像 | 本地可运行,健康检查通过 |
|
||||
| Plumber API | POST /api/v1/skills/ST_T_TEST_IND 返回 JSON |
|
||||
| Node.js 转发 | POST /api/v1/ssa/execute 调用 R 成功 |
|
||||
| 数据库 Schema | tools_library, sessions, messages 表创建 |
|
||||
| 前端骨架 | 基础页面框架,可上传文件 |
|
||||
|
||||
### Phase 2:智能规划与交互(Week 3-4)
|
||||
|
||||
**目标:** 用户可与 AI 对话,确认后执行
|
||||
|
||||
| 交付物 | 验收标准 |
|
||||
|--------|---------|
|
||||
| RAG 检索 | 输入"两组差异"能返回 T 检验 |
|
||||
| Planner | 生成正确的参数映射 JSON |
|
||||
| 计划确认卡片 | 前端展示,用户可修改参数 |
|
||||
| 执行路径树 | 显示护栏检查步骤 |
|
||||
| 5 个工具 | T检验、配对T、ANOVA、卡方、相关性 |
|
||||
|
||||
### Phase 3:完善与联调(Week 5-6)
|
||||
|
||||
**目标:** MVP 功能完整,可演示
|
||||
|
||||
| 交付物 | 验收标准 |
|
||||
|--------|---------|
|
||||
| Critic 解读 | 生成严谨的统计结论 |
|
||||
| 代码下载 | 用户可下载 .R 文件 |
|
||||
| 10 个工具 | 全部上线并测试通过 |
|
||||
| 端到端测试 | 10 个典型场景通过 |
|
||||
| SAE 部署 | R 服务部署成功 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心 API 设计
|
||||
|
||||
```
|
||||
POST /api/v1/ssa/sessions # 创建会话
|
||||
POST /api/v1/ssa/sessions/:id/upload # 上传数据
|
||||
POST /api/v1/ssa/sessions/:id/plan # 生成计划(不执行)
|
||||
POST /api/v1/ssa/sessions/:id/execute # 确认执行
|
||||
GET /api/v1/ssa/sessions/:id/messages # 获取消息历史
|
||||
GET /api/v1/ssa/sessions/:id/download-code # 下载代码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖与集成
|
||||
|
||||
### 5.1 平台能力复用
|
||||
|
||||
| 能力 | 使用方式 | 状态 |
|
||||
|------|---------|------|
|
||||
| LLM 网关 | `LLMFactory.getAdapter('deepseek-v3')` | ✅ 可用 |
|
||||
| RAG 引擎 | `VectorSearchService` | ✅ 可用 |
|
||||
| 流式响应 | `StreamingService` (Critic) | ✅ 可用 |
|
||||
| Prompt 管理 | `capability_schema.prompt_templates` | ✅ 可用 |
|
||||
| 认证授权 | `authenticate` 中间件 | ✅ 可用 |
|
||||
| OSS 存储 | `storage.upload()` (图片/代码) | ✅ 可用 |
|
||||
|
||||
### 5.2 新增组件
|
||||
|
||||
| 组件 | 说明 |
|
||||
|------|------|
|
||||
| R Docker 镜像 | 基于 rocker/r-ver:4.3,含 Plumber + renv |
|
||||
| R 统计服务 | SAE 新应用,**VPC 内网通信** |
|
||||
| SSA 前端模块 | `frontend-v2/src/modules/ssa/` |
|
||||
| SSA 后端模块 | `backend/src/modules/ssa/` |
|
||||
|
||||
### 5.3 关键配置要求
|
||||
|
||||
| 配置项 | 要求 | 原因 |
|
||||
|-------|------|------|
|
||||
| OSS Endpoint | 使用 VPC 内网地址 `oss-cn-xxx-internal.aliyuncs.com` | R 服务网络隔离需求 |
|
||||
| SAE 扩容策略 | CPU > 60% 或并发 > 5 时自动扩容 | R 单线程,需水平扩展 |
|
||||
| R 服务出站策略 | Deny Public Internet, Allow VPC | 防止数据外泄 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与应对
|
||||
|
||||
| 风险 | 概率 | 应对策略 |
|
||||
|------|------|---------|
|
||||
| R 工具封装进度慢 | 高 | 先做 5 个核心工具,glue 模板化开发 |
|
||||
| LLM 参数映射错误 | 中 | R 服务强类型校验 + 前端允许修改 |
|
||||
| LLM 输出 JSON 格式错误 | 中 | json-repair 库修复 + Zod Schema 强校验 |
|
||||
| R 服务并发阻塞 | 中 | **SAE 固定 2 实例**(避免冷启动 30s+) |
|
||||
| 大文件导致内存溢出 | 中 | **混合协议**:< 2MB inline,2-20MB OSS |
|
||||
| R 临时文件堆积 | 中 | on.exit() 清理 + 定时 CronJob |
|
||||
| SAE 部署 R 失败 | 低 | 提前测试 Docker 镜像 |
|
||||
| OSS 网络不通 | 低 | **ENV 注入 Endpoint** + DEV_MODE Mock |
|
||||
| 大样本护栏超时 | 中 | **N > 5000 抽样检验**,避免 Shapiro-Wilk 超时 |
|
||||
| 🆕 Node.js xlsx 内存刺客 | 中 | **SAE 内存上限 2GB+** |
|
||||
| 🆕 R 服务 Segfault 崩溃 | 低 | **Liveness Probe** + 502/504 友好提示 |
|
||||
| 🆕 本地开发 OSS 不通 | 中 | **DEV_MODE** 读取本地 fixtures |
|
||||
| 🆕 用户代码缺依赖 | 高 | **模板头部自动安装脚本** |
|
||||
| 🆕 分类变量隐私泄露 | 中 | **稀有值 < 5 隐藏** |
|
||||
| 🆕 R 错误信息黑盒 | 中 | **map_r_error 友好映射** |
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准(来自 PRD)
|
||||
|
||||
1. **准确性**:非正态数据自动降级为非参数检验
|
||||
2. **性能**:2MB 数据 T 检验端到端 < 5 秒
|
||||
3. **复现性**:下载的 R 代码本地可运行
|
||||
4. **隐私**:审计日志不含真实患者数据
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档索引
|
||||
|
||||
| 文档 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务清单 | `04-开发计划/01-任务清单与进度追踪.md` | 可追踪的 TODO |
|
||||
| R 服务指南 | `04-开发计划/02-R服务开发指南.md` | R 工程师专用 |
|
||||
| 后端指南 | `04-开发计划/03-后端开发指南.md` | Node.js 工程师专用 |
|
||||
| 前端指南 | `04-开发计划/04-前端开发指南.md` | 前端工程师专用 |
|
||||
| PRD | `00-系统设计/PRD SSA-Pro 严谨型智能统计分析模块.md` | 产品需求 |
|
||||
| 架构设计 | `00-系统设计/SSA-Pro 严谨型智能统计分析架构设计方案V4.md` | 技术架构 |
|
||||
195
docs/03-业务模块/SSA-智能统计分析/04-开发计划/01-任务清单与进度追踪.md
Normal file
195
docs/03-业务模块/SSA-智能统计分析/04-开发计划/01-任务清单与进度追踪.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# SSA-Pro MVP 任务清单与进度追踪
|
||||
|
||||
> **文档版本:** v1.3
|
||||
> **创建日期:** 2026-02-18
|
||||
> **最后更新:** 2026-02-18(纳入 V3.0 终极审查建议)
|
||||
> **更新频率:** 每日站会后更新
|
||||
|
||||
---
|
||||
|
||||
## 状态图例
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| ⬜ | 未开始 |
|
||||
| 🔄 | 进行中 |
|
||||
| ✅ | 已完成 |
|
||||
| ⏸️ | 暂停/阻塞 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:骨架搭建(Week 1-2)
|
||||
|
||||
**里程碑目标:** T 检验 API 端到端跑通
|
||||
|
||||
### R 服务任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 创建 `r-statistics-service/` 目录结构 | 2h | 含 templates/, fixtures/ 目录 |
|
||||
| ⬜ | 初始化 renv 并生成 `renv.lock` | 1h | **锁定包版本** |
|
||||
| ⬜ | 编写 Dockerfile(基于 rocker/r-ver:4.3) | 2h | 使用 renv::restore() |
|
||||
| ⬜ | 🆕 Dockerfile 配置 OSS 环境变量 | 1h | **ENV 注入,非硬编码** |
|
||||
| ⬜ | 安装 glue 包,创建代码模板文件 | 2h | **替代 paste0 拼接** |
|
||||
| ⬜ | 🆕 实现 `data_loader.R`(混合协议) | 3h | **支持 inline/OSS/DEV_MODE** |
|
||||
| ⬜ | 🆕 实现 `result_formatter.R`(p_value_fmt) | 1h | **APA 格式化** |
|
||||
| ⬜ | 实现 `plumber.R` 入口文件 | 2h | 健康检查 + 动态路由 |
|
||||
| ⬜ | 🆕 plumber.R 添加 Debug 模式支持 | 1h | **保留临时文件排查** |
|
||||
| ⬜ | 定义错误码枚举(error_codes.R) | 1h | **业务/系统错误分离** |
|
||||
| ⬜ | 🆕 扩展错误码映射表(map_r_error) | 1h | **R 错误 → 用户友好提示** |
|
||||
| ⬜ | 🆕 代码模板头部添加依赖安装脚本 | 0.5h | **用户本地可运行** |
|
||||
| ⬜ | 🆕 创建 `tests/fixtures/` 标准测试数据 | 2h | **normal/skewed/missing** |
|
||||
| ⬜ | 实现 T 检验 Wrapper(ST_T_TEST_IND) | 4h | 含护栏 + glue + 大样本优化 |
|
||||
| ⬜ | 本地 Docker 测试通过 | 2h | |
|
||||
|
||||
### 后端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 创建 `backend/src/modules/ssa/` 目录结构 | 1h | |
|
||||
| ⬜ | 设计并创建数据库 Schema(Prisma) | 3h | 4张表 |
|
||||
| ⬜ | 执行 `prisma migrate dev` | 0.5h | |
|
||||
| ⬜ | 安装 json-repair 和 zod 依赖 | 0.5h | **LLM 输出容错** |
|
||||
| ⬜ | 实现 `RClientService`(调用 R 服务) | 3h | 超时 120s |
|
||||
| ⬜ | 🆕 RClientService 添加 502/504 友好处理 | 0.5h | **R 崩溃用户提示** |
|
||||
| ⬜ | 🆕 DataParserService 分类变量隐私保护 | 1h | **稀有值 < 5 隐藏** |
|
||||
| ⬜ | 实现 `POST /api/v1/ssa/execute` 存根 | 2h | 先做转发 |
|
||||
| ⬜ | 注册路由到 `index.ts` | 0.5h | |
|
||||
|
||||
### 前端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 创建 `frontend-v2/src/modules/ssa/` 目录结构 | 1h | |
|
||||
| ⬜ | 注册到 `moduleRegistry.ts` | 0.5h | |
|
||||
| ⬜ | 实现基础页面框架(SSAWorkspace) | 3h | 参考原型图 |
|
||||
| ⬜ | 实现左侧边栏组件 | 2h | |
|
||||
| ⬜ | 实现数据上传组件(DataUploader) | 3h | |
|
||||
| ⬜ | 构造 Mock 数据用于组件开发 | 1h | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:智能规划与交互(Week 3-4)
|
||||
|
||||
**里程碑目标:** 用户可与 AI 对话,确认后执行
|
||||
|
||||
### R 服务任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现配对 T 检验(ST_T_TEST_PAIRED) | 3h | |
|
||||
| ⬜ | 实现单因素 ANOVA(ST_ANOVA_ONE) | 3h | |
|
||||
| ⬜ | 实现卡方检验(ST_CHI_SQUARE) | 3h | |
|
||||
| ⬜ | 实现相关性分析(ST_CORRELATION) | 3h | |
|
||||
| ⬜ | 实现通用护栏函数(utils/guardrails.R) | 2h | |
|
||||
| ⬜ | 为 5 个工具编写元数据 YAML | 2h | |
|
||||
|
||||
### 后端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现 `ToolRetrievalService`(RAG 检索) | 4h | 复用 VectorSearchService |
|
||||
| ⬜ | 导入 5 个工具元数据到 pgvector | 2h | |
|
||||
| ⬜ | 注册 Prompt 到 capability_schema | 2h | 4 个 Prompt |
|
||||
| ⬜ | 实现 `PlannerService`(LLM 调用) | 4h | 含 json-repair + Zod 校验 |
|
||||
| ⬜ | 实现 `POST /api/v1/ssa/sessions/:id/plan` | 3h | |
|
||||
| ⬜ | 实现会话管理 API(CRUD) | 3h | |
|
||||
| ⬜ | 实现 Brain-Hand 数据隔离逻辑 | 2h | Schema 给 LLM,Data 给 R |
|
||||
| ⬜ | DataParserService 增加小样本隐私保护 | 1h | N<10 时模糊化 Min/Max |
|
||||
|
||||
### 前端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现 Chat 消息流组件 | 4h | 复用 AIStreamChat |
|
||||
| ⬜ | 实现计划确认卡片(PlanCard) | 4h | 参考原型图 |
|
||||
| ⬜ | 实现执行路径树(ExecutionTrace) | 3h | 动画效果 |
|
||||
| ⬜ | 实现 API 对接(api.ts) | 2h | |
|
||||
| ⬜ | 实现 Zustand Store | 2h | |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:完善与联调(Week 5-6)
|
||||
|
||||
**里程碑目标:** MVP 功能完整,可演示
|
||||
|
||||
### R 服务任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现 Fisher 精确检验(ST_FISHER) | 2h | |
|
||||
| ⬜ | 实现 Wilcoxon 检验(ST_WILCOXON) | 2h | |
|
||||
| ⬜ | 实现 Mann-Whitney U(ST_MANN_WHITNEY) | 2h | |
|
||||
| ⬜ | 实现简单线性回归(ST_LINEAR_REG) | 3h | |
|
||||
| ⬜ | 实现描述性统计(ST_DESCRIPTIVE) | 2h | |
|
||||
| ⬜ | 完善代码生成器(所有工具) | 3h | |
|
||||
| ⬜ | 补充错误处理(tryCatch) | 2h | |
|
||||
|
||||
### 后端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现 `CriticService`(结果解读) | 3h | 流式输出 |
|
||||
| ⬜ | 实现代码下载 API | 2h | |
|
||||
| ⬜ | 导入剩余 5 个工具元数据 | 1h | |
|
||||
| ⬜ | 实现执行日志记录(execution_logs) | 2h | |
|
||||
| ⬜ | 端到端集成测试 | 4h | |
|
||||
|
||||
### 前端任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | 实现结果展示卡片(ResultCard) | 4h | 三线表 + 图表 |
|
||||
| ⬜ | 实现代码下载功能 | 2h | |
|
||||
| ⬜ | 实现消息历史加载 | 2h | |
|
||||
| ⬜ | UI 样式精调(对齐原型图) | 3h | |
|
||||
| ⬜ | 端到端联调测试 | 4h | |
|
||||
|
||||
### 部署任务
|
||||
|
||||
| 状态 | 任务 | 预估 | 备注 |
|
||||
|------|------|------|------|
|
||||
| ⬜ | R 服务 Docker 镜像推送 ACR | 1h | |
|
||||
| ⬜ | SAE 创建 R 服务应用 | 2h | |
|
||||
| ⬜ | 🆕 **配置 SAE 固定 2 实例** | 1h | **避免冷启动 30s+ 延迟** |
|
||||
| ⬜ | 🆕 **配置 R 服务 Liveness Probe** | 0.5h | **检测僵尸进程,自动重启** |
|
||||
| ⬜ | 🆕 **配置 Node.js 内存上限 2GB+** | 0.5h | **xlsx 全量读取防 OOM** |
|
||||
| ⬜ | 🆕 **配置 OSS Endpoint 环境变量** | 0.5h | **开发公网/生产内网** |
|
||||
| ⬜ | **配置 R 服务出站策略** | 0.5h | Deny Public, Allow VPC |
|
||||
| ⬜ | 配置内网通信(Node.js → R) | 1h | |
|
||||
| ⬜ | **创建临时文件清理 CronJob** | 1h | 每日清理 /tmp |
|
||||
| ⬜ | 生产环境验证 | 2h | |
|
||||
|
||||
---
|
||||
|
||||
## 进度统计
|
||||
|
||||
| Phase | 任务总数 | 已完成 | 进度 |
|
||||
|-------|---------|--------|------|
|
||||
| Phase 1 | 21 | 0 | 0% |
|
||||
| Phase 2 | 20 | 0 | 0% |
|
||||
| Phase 3 | 21 | 0 | 0% |
|
||||
| **总计** | **62** | **0** | **0%** |
|
||||
|
||||
---
|
||||
|
||||
## 风险与阻塞项
|
||||
|
||||
| 日期 | 问题描述 | 影响 | 解决方案 | 状态 |
|
||||
|------|---------|------|---------|------|
|
||||
| | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 每日站会记录
|
||||
|
||||
### 2026-02-xx
|
||||
|
||||
**昨日完成:**
|
||||
-
|
||||
|
||||
**今日计划:**
|
||||
-
|
||||
|
||||
**阻塞问题:**
|
||||
-
|
||||
1018
docs/03-业务模块/SSA-智能统计分析/04-开发计划/02-R服务开发指南.md
Normal file
1018
docs/03-业务模块/SSA-智能统计分析/04-开发计划/02-R服务开发指南.md
Normal file
File diff suppressed because it is too large
Load Diff
825
docs/03-业务模块/SSA-智能统计分析/04-开发计划/03-后端开发指南.md
Normal file
825
docs/03-业务模块/SSA-智能统计分析/04-开发计划/03-后端开发指南.md
Normal file
@@ -0,0 +1,825 @@
|
||||
# SSA-Pro 后端开发指南
|
||||
|
||||
> **文档版本:** v1.3
|
||||
> **创建日期:** 2026-02-18
|
||||
> **最后更新:** 2026-02-18(纳入 V3.0 终极审查建议)
|
||||
> **目标读者:** Node.js 后端工程师
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块目录结构
|
||||
|
||||
```
|
||||
backend/src/modules/ssa/
|
||||
├── index.ts # 模块入口,注册路由
|
||||
├── routes/
|
||||
│ ├── session.routes.ts # 会话管理路由
|
||||
│ └── analysis.routes.ts # 分析执行路由
|
||||
├── services/
|
||||
│ ├── SessionService.ts # 会话 CRUD
|
||||
│ ├── PlannerService.ts # AI 规划(LLM 调用)
|
||||
│ ├── CriticService.ts # 结果解读(流式)
|
||||
│ ├── ToolRetrievalService.ts # RAG 工具检索
|
||||
│ ├── RClientService.ts # R 服务调用
|
||||
│ └── DataParserService.ts # 数据解析 + Schema 提取
|
||||
├── validators/
|
||||
│ └── planSchema.ts # 📌 Zod Schema 定义
|
||||
├── dto/
|
||||
│ ├── CreateSessionDto.ts
|
||||
│ ├── UploadDataDto.ts
|
||||
│ └── ExecuteAnalysisDto.ts
|
||||
└── types/
|
||||
└── index.ts # 类型定义
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库 Schema(Prisma)
|
||||
|
||||
```prisma
|
||||
// schema.prisma - SSA 模块部分
|
||||
|
||||
// 分析会话
|
||||
model SsaSession {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
title String?
|
||||
dataSchema Json? @map("data_schema") // 数据结构(LLM可见)
|
||||
dataPayload Json? @map("data_payload") // 真实数据(仅R可见)
|
||||
status String @default("active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
messages SsaMessage[]
|
||||
|
||||
@@map("ssa_sessions")
|
||||
@@schema("ssa_schema")
|
||||
}
|
||||
|
||||
// 消息记录
|
||||
model SsaMessage {
|
||||
id String @id @default(uuid())
|
||||
sessionId String @map("session_id")
|
||||
role String // user | assistant | system
|
||||
contentType String @map("content_type") // text | plan | result
|
||||
content Json
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
session SsaSession @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@map("ssa_messages")
|
||||
@@schema("ssa_schema")
|
||||
}
|
||||
|
||||
// 工具库
|
||||
model SsaTool {
|
||||
id String @id @default(uuid())
|
||||
toolCode String @unique @map("tool_code")
|
||||
name String
|
||||
version String @default("1.0.0")
|
||||
description String
|
||||
usageContext String? @map("usage_context")
|
||||
paramsSchema Json @map("params_schema")
|
||||
guardrails Json?
|
||||
searchText String @map("search_text")
|
||||
embedding Unsupported("vector(1024)")?
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("tools_library")
|
||||
@@schema("ssa_schema")
|
||||
}
|
||||
|
||||
// 执行日志
|
||||
model SsaExecutionLog {
|
||||
id String @id @default(uuid())
|
||||
sessionId String @map("session_id")
|
||||
messageId String? @map("message_id")
|
||||
toolCode String @map("tool_code")
|
||||
inputParams Json @map("input_params")
|
||||
outputStatus String @map("output_status")
|
||||
outputResult Json? @map("output_result")
|
||||
traceLog String[] @map("trace_log")
|
||||
executionMs Int? @map("execution_ms")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("execution_logs")
|
||||
@@schema("ssa_schema")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 路由设计
|
||||
|
||||
### 3.1 路由注册
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import sessionRoutes from './routes/session.routes';
|
||||
import analysisRoutes from './routes/analysis.routes';
|
||||
|
||||
export default async function ssaModule(app: FastifyInstance) {
|
||||
// 注册认证中间件
|
||||
app.addHook('preHandler', app.authenticate);
|
||||
|
||||
// 注册子路由
|
||||
app.register(sessionRoutes, { prefix: '/sessions' });
|
||||
app.register(analysisRoutes, { prefix: '/sessions' });
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 会话路由
|
||||
|
||||
```typescript
|
||||
// routes/session.routes.ts
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { SessionService } from '../services/SessionService';
|
||||
|
||||
export default async function sessionRoutes(app: FastifyInstance) {
|
||||
const sessionService = new SessionService();
|
||||
|
||||
// 创建会话
|
||||
app.post('/', async (req, reply) => {
|
||||
const userId = req.user.id;
|
||||
const session = await sessionService.create(userId);
|
||||
return reply.send(session);
|
||||
});
|
||||
|
||||
// 获取会话列表
|
||||
app.get('/', async (req, reply) => {
|
||||
const userId = req.user.id;
|
||||
const sessions = await sessionService.listByUser(userId);
|
||||
return reply.send(sessions);
|
||||
});
|
||||
|
||||
// 获取单个会话(含消息历史)
|
||||
app.get('/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = await sessionService.getById(id, req.user.id);
|
||||
return reply.send(session);
|
||||
});
|
||||
|
||||
// 上传数据
|
||||
app.post('/:id/upload', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
// 解析 Excel/CSV,提取 Schema 和 Data
|
||||
const result = await sessionService.uploadData(id, req);
|
||||
return reply.send(result);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 分析路由
|
||||
|
||||
```typescript
|
||||
// routes/analysis.routes.ts
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { PlannerService } from '../services/PlannerService';
|
||||
import { RClientService } from '../services/RClientService';
|
||||
import { CriticService } from '../services/CriticService';
|
||||
|
||||
export default async function analysisRoutes(app: FastifyInstance) {
|
||||
const plannerService = new PlannerService();
|
||||
const rClientService = new RClientService();
|
||||
const criticService = new CriticService();
|
||||
|
||||
// 生成分析计划(不执行)
|
||||
app.post('/:id/plan', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { query } = req.body as { query: string };
|
||||
|
||||
// 1. RAG 检索工具
|
||||
// 2. LLM 生成计划
|
||||
const plan = await plannerService.generatePlan(id, query);
|
||||
|
||||
return reply.send({
|
||||
type: 'plan',
|
||||
plan
|
||||
});
|
||||
});
|
||||
|
||||
// 确认执行
|
||||
app.post('/:id/execute', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { plan } = req.body as { plan: object };
|
||||
|
||||
// 1. 调用 R 服务执行
|
||||
const result = await rClientService.execute(id, plan);
|
||||
|
||||
// 2. 保存执行日志
|
||||
// 3. 保存结果到消息
|
||||
|
||||
return reply.send({
|
||||
type: 'result',
|
||||
result
|
||||
});
|
||||
});
|
||||
|
||||
// 获取结果解读(流式)
|
||||
app.get('/:id/interpret/:messageId', async (req, reply) => {
|
||||
const { id, messageId } = req.params as { id: string; messageId: string };
|
||||
|
||||
// 流式返回 Critic 解读
|
||||
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
||||
|
||||
await criticService.streamInterpret(id, messageId, reply.raw);
|
||||
});
|
||||
|
||||
// 下载代码
|
||||
app.get('/:id/download-code/:messageId', async (req, reply) => {
|
||||
const { id, messageId } = req.params as { id: string; messageId: string };
|
||||
|
||||
const code = await sessionService.getReproducibleCode(messageId);
|
||||
|
||||
reply.header('Content-Type', 'text/plain');
|
||||
reply.header('Content-Disposition', 'attachment; filename="analysis.R"');
|
||||
return reply.send(code);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心服务实现
|
||||
|
||||
### 4.1 RClientService(调用 R 服务)
|
||||
|
||||
```typescript
|
||||
// services/RClientService.ts
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { prisma } from '@/common/db';
|
||||
import { logger } from '@/common/logging';
|
||||
|
||||
export class RClientService {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: process.env.R_SERVICE_URL || 'http://localhost:8080',
|
||||
timeout: 120000, // 📌 120s 超时(应对复杂计算)
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
async execute(sessionId: string, plan: {
|
||||
tool_code: string;
|
||||
params: Record<string, any>;
|
||||
guardrails: Record<string, boolean>;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 1. 获取会话的真实数据
|
||||
const session = await prisma.ssaSession.findUniqueOrThrow({
|
||||
where: { id: sessionId }
|
||||
});
|
||||
|
||||
// 🆕 2. 构造 R 服务请求(混合数据协议)
|
||||
const dataSource = this.buildDataSource(session);
|
||||
const requestBody = {
|
||||
data_source: dataSource, // 🆕 统一数据源字段
|
||||
params: plan.params,
|
||||
guardrails: plan.guardrails
|
||||
};
|
||||
|
||||
/**
|
||||
* 🆕 根据数据大小选择传输方式
|
||||
* - < 2MB: inline JSON
|
||||
* - >= 2MB: OSS key
|
||||
*/
|
||||
private buildDataSource(session: any): { type: string; data?: any; oss_key?: string } {
|
||||
const payload = session.dataPayload;
|
||||
const payloadSize = JSON.stringify(payload).length;
|
||||
|
||||
const SIZE_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (payloadSize < SIZE_THRESHOLD) {
|
||||
// 小数据:直接内联
|
||||
return {
|
||||
type: 'inline',
|
||||
data: payload
|
||||
};
|
||||
} else {
|
||||
// 大数据:上传 OSS,传递 key
|
||||
// 注意:此处假设 session 创建时已上传 OSS
|
||||
const ossKey = session.dataOssKey || `sessions/${session.id}/data.json`;
|
||||
return {
|
||||
type: 'oss',
|
||||
oss_key: ossKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 调用 R 服务
|
||||
try {
|
||||
const response = await this.client.post(
|
||||
`/api/v1/skills/${plan.tool_code}`,
|
||||
requestBody
|
||||
);
|
||||
|
||||
const executionMs = Date.now() - startTime;
|
||||
|
||||
// 4. 记录执行日志(不含真实数据)
|
||||
await prisma.ssaExecutionLog.create({
|
||||
data: {
|
||||
sessionId,
|
||||
toolCode: plan.tool_code,
|
||||
inputParams: plan.params, // 只记录参数,不记录数据
|
||||
outputStatus: response.data.status,
|
||||
outputResult: response.data.results,
|
||||
traceLog: response.data.trace_log || [],
|
||||
executionMs
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('R service call failed', { sessionId, toolCode: plan.tool_code, error });
|
||||
|
||||
// 🆕 502/504 特殊处理(R 服务崩溃或超时)
|
||||
const statusCode = error.response?.status;
|
||||
if (statusCode === 502 || statusCode === 504) {
|
||||
throw new Error('统计服务繁忙或数据异常,请稍后重试');
|
||||
}
|
||||
|
||||
// 🆕 提取 R 服务返回的用户友好提示
|
||||
const userHint = error.response?.data?.user_hint;
|
||||
if (userHint) {
|
||||
throw new Error(userHint);
|
||||
}
|
||||
|
||||
throw new Error(`R service error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.client.get('/health');
|
||||
return res.data.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 ToolRetrievalService(RAG 检索)
|
||||
|
||||
```typescript
|
||||
// services/ToolRetrievalService.ts
|
||||
import { VectorSearchService } from '@/common/rag';
|
||||
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
|
||||
import { prisma } from '@/common/db';
|
||||
|
||||
export class ToolRetrievalService {
|
||||
private vectorSearch: VectorSearchService;
|
||||
|
||||
constructor() {
|
||||
this.vectorSearch = new VectorSearchService({
|
||||
schema: 'ssa_schema',
|
||||
table: 'tools_library',
|
||||
embeddingColumn: 'embedding',
|
||||
textColumn: 'search_text'
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveTools(query: string, dataSchema: object, topK = 5) {
|
||||
// 1. Query Rewrite(可选,提升召回)
|
||||
const rewriter = LLMFactory.getAdapter('deepseek-v3');
|
||||
const rewritePrompt = `
|
||||
将用户的统计分析需求改写为更适合检索统计工具的查询:
|
||||
用户需求: ${query}
|
||||
数据结构: ${JSON.stringify(dataSchema)}
|
||||
|
||||
输出改写后的查询(一句话):
|
||||
`.trim();
|
||||
|
||||
const rewrittenQuery = await rewriter.chat([
|
||||
{ role: 'user', content: rewritePrompt }
|
||||
]);
|
||||
|
||||
// 2. 向量检索
|
||||
const vectorResults = await this.vectorSearch.search(rewrittenQuery, topK);
|
||||
|
||||
// 3. 关键词检索 (pg_bigm)
|
||||
const keywordResults = await prisma.$queryRaw`
|
||||
SELECT id, tool_code, name, description, params_schema, guardrails
|
||||
FROM ssa_schema.tools_library
|
||||
WHERE search_text LIKE '%' || ${query} || '%'
|
||||
AND is_active = true
|
||||
LIMIT 5
|
||||
`;
|
||||
|
||||
// 4. RRF 融合
|
||||
const merged = this.rrfMerge(vectorResults, keywordResults);
|
||||
|
||||
// 5. Rerank(可选)
|
||||
// const reranked = await this.rerank(merged, query);
|
||||
|
||||
return merged.slice(0, topK);
|
||||
}
|
||||
|
||||
private rrfMerge(vectorResults: any[], keywordResults: any[], k = 60) {
|
||||
const scores = new Map<string, number>();
|
||||
|
||||
vectorResults.forEach((item, idx) => {
|
||||
const rrf = 1 / (k + idx + 1);
|
||||
scores.set(item.id, (scores.get(item.id) || 0) + rrf);
|
||||
});
|
||||
|
||||
keywordResults.forEach((item, idx) => {
|
||||
const rrf = 1 / (k + idx + 1);
|
||||
scores.set(item.id, (scores.get(item.id) || 0) + rrf);
|
||||
});
|
||||
|
||||
// 合并并排序
|
||||
const allItems = [...vectorResults, ...keywordResults];
|
||||
const unique = [...new Map(allItems.map(i => [i.id, i])).values()];
|
||||
|
||||
return unique.sort((a, b) =>
|
||||
(scores.get(b.id) || 0) - (scores.get(a.id) || 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 PlannerService(AI 规划 + JSON 容错)
|
||||
|
||||
```typescript
|
||||
// services/PlannerService.ts
|
||||
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
|
||||
import { PromptService } from '@/common/prompts';
|
||||
import { ToolRetrievalService } from './ToolRetrievalService';
|
||||
import { prisma } from '@/common/db';
|
||||
import { jsonrepair } from 'jsonrepair'; // 📌 JSON 修复库
|
||||
import { planSchema } from '../validators/planSchema'; // 📌 Zod Schema
|
||||
|
||||
export class PlannerService {
|
||||
private retrieval: ToolRetrievalService;
|
||||
|
||||
constructor() {
|
||||
this.retrieval = new ToolRetrievalService();
|
||||
}
|
||||
|
||||
async generatePlan(sessionId: string, userQuery: string) {
|
||||
// 1. 获取会话的数据 Schema(不含真实数据)
|
||||
const session = await prisma.ssaSession.findUniqueOrThrow({
|
||||
where: { id: sessionId },
|
||||
select: { dataSchema: true }
|
||||
});
|
||||
|
||||
// 2. RAG 检索候选工具
|
||||
const candidateTools = await this.retrieval.retrieveTools(
|
||||
userQuery,
|
||||
session.dataSchema,
|
||||
5
|
||||
);
|
||||
|
||||
// 3. 获取 Planner Prompt
|
||||
const promptTemplate = await PromptService.get('SSA_PLANNER');
|
||||
|
||||
// 4. 构造 Prompt
|
||||
const systemPrompt = promptTemplate
|
||||
.replace('{{data_schema_json}}', JSON.stringify(session.dataSchema, null, 2))
|
||||
.replace('{{candidate_tools_json}}', JSON.stringify(candidateTools, null, 2));
|
||||
|
||||
// 5. 调用 LLM
|
||||
const llm = LLMFactory.getAdapter('deepseek-v3');
|
||||
const response = await llm.chat([
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userQuery }
|
||||
]);
|
||||
|
||||
// 6. 📌 解析 + 修复 + 校验 JSON
|
||||
const plan = this.parseAndValidateJson(response, candidateTools);
|
||||
|
||||
// 7. 保存用户消息和计划消息
|
||||
await prisma.ssaMessage.createMany({
|
||||
data: [
|
||||
{
|
||||
sessionId,
|
||||
role: 'user',
|
||||
contentType: 'text',
|
||||
content: { text: userQuery }
|
||||
},
|
||||
{
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
contentType: 'plan',
|
||||
content: plan
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// 📌 增强的 JSON 解析(含修复和校验)
|
||||
private parseAndValidateJson(text: string, candidateTools: any[]): object {
|
||||
// Step 1: 提取 JSON 块
|
||||
const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) ||
|
||||
text.match(/\{[\s\S]*\}/);
|
||||
|
||||
if (!jsonMatch) {
|
||||
throw new Error('LLM response does not contain valid JSON');
|
||||
}
|
||||
|
||||
let jsonStr = jsonMatch[1] || jsonMatch[0];
|
||||
|
||||
// Step 2: 使用 jsonrepair 修复常见问题(末尾逗号、缺少引号等)
|
||||
try {
|
||||
jsonStr = jsonrepair(jsonStr);
|
||||
} catch (repairError) {
|
||||
// 修复失败,继续尝试原始解析
|
||||
}
|
||||
|
||||
// Step 3: 解析 JSON
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(jsonStr);
|
||||
} catch (parseError) {
|
||||
throw new Error(`JSON parse failed: ${parseError.message}`);
|
||||
}
|
||||
|
||||
// Step 4: 使用 Zod 校验结构
|
||||
const validatedPlan = planSchema.safeParse(parsed);
|
||||
|
||||
if (!validatedPlan.success) {
|
||||
throw new Error(`Plan validation failed: ${validatedPlan.error.message}`);
|
||||
}
|
||||
|
||||
// Step 5: 校验 tool_code 是否在候选列表中
|
||||
const validToolCodes = candidateTools.map(t => t.tool_code);
|
||||
if (!validToolCodes.includes(validatedPlan.data.tool_code)) {
|
||||
throw new Error(`Invalid tool_code: ${validatedPlan.data.tool_code}`);
|
||||
}
|
||||
|
||||
return validatedPlan.data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Zod Schema 定义
|
||||
|
||||
```typescript
|
||||
// validators/planSchema.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const planSchema = z.object({
|
||||
tool_code: z.string().min(1),
|
||||
reasoning: z.string().optional(),
|
||||
params: z.record(z.any()),
|
||||
guardrails: z.object({
|
||||
check_normality: z.boolean().optional(),
|
||||
check_homogeneity: z.boolean().optional(),
|
||||
auto_fix: z.boolean().optional()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
export type PlanType = z.infer<typeof planSchema>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Brain-Hand 数据隔离
|
||||
|
||||
**核心原则:LLM 只看 Schema,R 服务处理真实数据**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据上传流程 │
|
||||
│ │
|
||||
│ Excel/CSV ──────┬────────────────────────────────────────│
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ 数据解析器 │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────┴─────────┐ │
|
||||
│ │ │ │
|
||||
│ dataSchema dataPayload │
|
||||
│ (结构/类型/统计) (真实数据) │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ LLM (Planner) R (Executor) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.1 数据解析实现
|
||||
|
||||
```typescript
|
||||
// services/DataParserService.ts
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
export class DataParserService {
|
||||
|
||||
static parse(buffer: Buffer, filename: string) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
|
||||
// 转为 JSON 数组
|
||||
const data = XLSX.utils.sheet_to_json(sheet);
|
||||
|
||||
// 提取 Schema
|
||||
const schema = this.extractSchema(data);
|
||||
|
||||
return {
|
||||
dataSchema: schema, // 给 LLM
|
||||
dataPayload: data // 给 R
|
||||
};
|
||||
}
|
||||
|
||||
private static extractSchema(data: any[]) {
|
||||
if (data.length === 0) return { columns: [], rowCount: 0 };
|
||||
|
||||
const columns = Object.keys(data[0]).map(colName => {
|
||||
const values = data.map(row => row[colName]).filter(v => v != null);
|
||||
const type = this.inferType(values);
|
||||
|
||||
return {
|
||||
name: colName,
|
||||
type,
|
||||
...this.computeStats(values, type, data.length) // 📌 传入行数用于隐私保护
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rowCount: data.length,
|
||||
columns
|
||||
};
|
||||
}
|
||||
|
||||
private static inferType(values: any[]): 'numeric' | 'categorical' | 'datetime' {
|
||||
const sample = values.slice(0, 100);
|
||||
const numericCount = sample.filter(v => typeof v === 'number' || !isNaN(Number(v))).length;
|
||||
|
||||
if (numericCount / sample.length > 0.9) return 'numeric';
|
||||
return 'categorical';
|
||||
}
|
||||
|
||||
private static computeStats(values: any[], type: string, rowCount: number) {
|
||||
if (type === 'numeric') {
|
||||
const nums = values.map(Number).filter(n => !isNaN(n));
|
||||
let min = Math.min(...nums);
|
||||
let max = Math.max(...nums);
|
||||
|
||||
// 📌 小样本隐私保护:N < 10 时模糊化极值
|
||||
if (rowCount < 10) {
|
||||
min = Math.floor(min / 10) * 10; // 向下取整到十位
|
||||
max = Math.ceil(max / 10) * 10; // 向上取整到十位
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
mean: nums.reduce((a, b) => a + b, 0) / nums.length,
|
||||
missing: values.length - nums.length,
|
||||
privacyProtected: rowCount < 10 // 📌 标记是否已模糊化
|
||||
};
|
||||
}
|
||||
|
||||
// categorical
|
||||
const counts = new Map<string, number>();
|
||||
values.forEach(v => {
|
||||
const key = String(v);
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
});
|
||||
|
||||
// 🆕 分类变量隐私保护:
|
||||
// 如果某个取值的计数 < 5 且总行数 > 10,则隐藏具体值
|
||||
const uniqueValues: string[] = [];
|
||||
let maskedCount = 0;
|
||||
|
||||
for (const [value, count] of counts.entries()) {
|
||||
if (count < 5 && rowCount > 10) {
|
||||
maskedCount++;
|
||||
} else {
|
||||
uniqueValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// 最多展示 10 个非敏感值
|
||||
const safeValues = uniqueValues.slice(0, 10);
|
||||
if (maskedCount > 0) {
|
||||
safeValues.push(`[${maskedCount} 个稀有值已隐藏]`);
|
||||
}
|
||||
|
||||
return {
|
||||
uniqueValues: safeValues,
|
||||
uniqueCount: counts.size,
|
||||
missing: values.filter(v => v == null || v === '').length,
|
||||
privacyProtected: maskedCount > 0 // 🆕 标记
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Prompt 注册
|
||||
|
||||
```sql
|
||||
-- 注册 Planner Prompt
|
||||
INSERT INTO capability_schema.prompt_templates (code, name, content, model, temperature)
|
||||
VALUES (
|
||||
'SSA_PLANNER',
|
||||
'SSA 统计规划器',
|
||||
'你是一名资深的生物统计学家。你面前有一份数据摘要(Metadata)和一组可用的统计工具箱。
|
||||
请根据用户的需求,选择最合适的一个工具,并生成详细的执行计划(SAP)。
|
||||
|
||||
### 数据摘要
|
||||
{{data_schema_json}}
|
||||
|
||||
### 可用工具箱 (Candidates)
|
||||
{{candidate_tools_json}}
|
||||
|
||||
### 决策规则 (Guardrails)
|
||||
1. **类型匹配**:严格检查变量类型。不要把分类变量填入要求数值型的参数中。
|
||||
2. **工具匹配**:如果用户要做 "预测",优先选 "回归" 类工具;如果做 "差异",选 "检验" 类工具。
|
||||
3. **护栏配置**:对于 T 检验、ANOVA 等参数检验,必须开启 check_normality。
|
||||
|
||||
### 输出要求
|
||||
请先在 <thinking> 标签中进行推理,分析变量类型和工具适用性。
|
||||
然后输出纯 JSON,格式如下:
|
||||
{
|
||||
"tool_code": "选中工具的CODE",
|
||||
"reasoning": "一句话解释为什么选这个工具",
|
||||
"params": { ...根据工具定义的 params_schema 填写... },
|
||||
"guardrails": { "check_normality": true, "auto_fix": true }
|
||||
}',
|
||||
'deepseek-v3',
|
||||
0.3
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 与主应用集成
|
||||
|
||||
```typescript
|
||||
// backend/src/index.ts
|
||||
import ssaModule from './modules/ssa';
|
||||
|
||||
// 在 Fastify 注册
|
||||
app.register(ssaModule, { prefix: '/api/v1/ssa' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 环境变量
|
||||
|
||||
```env
|
||||
# .env
|
||||
|
||||
# R 服务配置
|
||||
R_SERVICE_URL=http://ssa-r-service:8080 # SAE VPC 内网地址
|
||||
R_SERVICE_TIMEOUT=120000 # 📌 超时 120s
|
||||
|
||||
# 📌 OSS 配置(必须使用 VPC 内网 Endpoint)
|
||||
OSS_ENDPOINT=oss-cn-beijing-internal.aliyuncs.com # 内网地址
|
||||
OSS_BUCKET=ssa-data-bucket
|
||||
OSS_ACCESS_KEY_ID=your-access-key
|
||||
OSS_ACCESS_KEY_SECRET=your-secret
|
||||
|
||||
# LLM 配置
|
||||
LLM_DEFAULT_MODEL=deepseek-v3
|
||||
```
|
||||
|
||||
> **重要**:OSS Endpoint 必须使用 `-internal` 后缀的 VPC 内网地址,否则 R 服务的网络隔离策略会导致文件下载失败。
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试检查清单
|
||||
|
||||
| 测试场景 | 预期结果 |
|
||||
|----------|---------|
|
||||
| POST /sessions 创建会话 | 返回 sessionId |
|
||||
| POST /sessions/:id/upload (CSV) | 返回 dataSchema |
|
||||
| POST /sessions/:id/upload (N<10) | dataSchema.privacyProtected = true |
|
||||
| POST /sessions/:id/plan (T检验意图) | 返回包含 tool_code 的 plan |
|
||||
| POST /sessions/:id/plan (LLM 返回格式错误 JSON) | json-repair 修复成功 |
|
||||
| POST /sessions/:id/plan (参数不合法) | Zod 校验失败,返回错误 |
|
||||
| POST /sessions/:id/execute | R 服务返回 success |
|
||||
| POST /sessions/:id/execute (超过 60s) | 不超时,等待 120s |
|
||||
| GET /sessions/:id/download-code | 下载 .R 文件 |
|
||||
| R 服务宕机时 execute | 返回友好错误 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 依赖包清单
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"jsonrepair": "^3.6.0",
|
||||
"zod": "^3.22.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"axios": "^1.6.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
897
docs/03-业务模块/SSA-智能统计分析/04-开发计划/04-前端开发指南.md
Normal file
897
docs/03-业务模块/SSA-智能统计分析/04-开发计划/04-前端开发指南.md
Normal file
@@ -0,0 +1,897 @@
|
||||
# SSA-Pro 前端开发指南
|
||||
|
||||
> **文档版本:** v1.3
|
||||
> **创建日期:** 2026-02-18
|
||||
> **最后更新:** 2026-02-18(纳入 V3.0 终极审查建议)
|
||||
> **目标读者:** 前端工程师
|
||||
> **原型参考:** `03-UI设计/智能统计分析V2.html`
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块目录结构
|
||||
|
||||
```
|
||||
frontend-v2/src/modules/ssa/
|
||||
├── index.ts # 模块入口,导出路由
|
||||
├── pages/
|
||||
│ └── SSAWorkspace.tsx # 主页面(工作区)
|
||||
├── components/
|
||||
│ ├── layout/
|
||||
│ │ ├── SSASidebar.tsx # 左侧边栏
|
||||
│ │ ├── SSAHeader.tsx # 顶部标题栏
|
||||
│ │ └── SSAInputArea.tsx # 底部输入区
|
||||
│ ├── chat/
|
||||
│ │ ├── MessageList.tsx # 消息流容器
|
||||
│ │ ├── SystemMessage.tsx # 系统消息气泡
|
||||
│ │ ├── UserMessage.tsx # 用户消息气泡
|
||||
│ │ └── AssistantMessage.tsx # AI 消息(含卡片)
|
||||
│ ├── cards/
|
||||
│ │ ├── DataUploader.tsx # 数据上传区
|
||||
│ │ ├── DataStatus.tsx # 数据集状态卡片
|
||||
│ │ ├── PlanCard.tsx # 分析计划确认卡片 ⭐
|
||||
│ │ ├── ExecutionTrace.tsx # 执行路径树 ⭐
|
||||
│ │ ├── ExecutionProgress.tsx# 📌 执行进度动画 ⭐
|
||||
│ │ └── ResultCard.tsx # 结果报告卡片 ⭐
|
||||
│ └── common/
|
||||
│ ├── APATable.tsx # 三线表组件
|
||||
│ └── PlotViewer.tsx # 图表查看器
|
||||
├── hooks/
|
||||
│ ├── useSSASession.ts # 会话管理 Hook
|
||||
│ └── useSSAExecution.ts # 执行控制 Hook
|
||||
├── store/
|
||||
│ └── ssaStore.ts # Zustand Store
|
||||
├── api/
|
||||
│ └── ssaApi.ts # API 封装
|
||||
├── types/
|
||||
│ └── index.ts # 类型定义
|
||||
└── styles/
|
||||
└── ssa.css # 模块样式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 原型图核心元素解析
|
||||
|
||||
根据 `智能统计分析V2.html` 原型,需实现以下核心 UI:
|
||||
|
||||
### 2.1 整体布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ┌───────────┐ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ │ │ Header (会话标题) │ │
|
||||
│ │ Sidebar │ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - 导入数据│ │ Chat Flow (消息流) │ │
|
||||
│ │ - 新会话 │ │ │ │
|
||||
│ │ - 历史 │ │ - SystemMessage (欢迎/上传引导) │ │
|
||||
│ │ │ │ - UserMessage (用户输入) │ │
|
||||
│ │ │ │ - PlanCard (计划确认) │ │
|
||||
│ │ ─────── │ │ - ExecutionTrace (执行路径) │ │
|
||||
│ │ 数据状态 │ │ - ResultCard (结果报告) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ │ InputArea (输入框 + 发送按钮) │ │
|
||||
│ └───────────┘ └─────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心组件设计规范
|
||||
|
||||
| 组件 | 原型特征 | 实现要点 |
|
||||
|------|---------|---------|
|
||||
| **SSASidebar** | 宽 256px,白色背景,阴影分隔 | Logo + 按钮 + 历史列表 + 数据状态 |
|
||||
| **PlanCard** | 圆角卡片,分组/检验变量展示,护栏警告 | 支持参数编辑、确认/修改按钮 |
|
||||
| **ExecutionTrace** | 竖向树状结构,带状态图标和连接线 | 动画展开,步骤状态(成功/警告/进行中) |
|
||||
| **ResultCard** | 多区块:三线表 + 图表 + 解读 + 下载 | APA 格式表格,Base64 图片渲染 |
|
||||
| **APATable** | 顶线(2px) + 表头下线(1px) + 底线(2px) | 数字右对齐,等宽字体 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件实现
|
||||
|
||||
### 3.1 PlanCard(计划确认卡片)
|
||||
|
||||
```tsx
|
||||
// components/cards/PlanCard.tsx
|
||||
import React from 'react';
|
||||
import { Card, Button, Tag, Alert, Space, Descriptions } from 'antd';
|
||||
import { PlayCircleOutlined, EditOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PlanCardProps {
|
||||
plan: {
|
||||
tool_code: string;
|
||||
tool_name: string;
|
||||
reasoning: string;
|
||||
params: Record<string, any>;
|
||||
guardrails: {
|
||||
check_normality?: boolean;
|
||||
auto_fix?: boolean;
|
||||
};
|
||||
};
|
||||
dataSchema: {
|
||||
columns: Array<{ name: string; type: string; uniqueValues?: string[] }>;
|
||||
};
|
||||
onConfirm: () => void;
|
||||
onEdit: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const PlanCard: React.FC<PlanCardProps> = ({
|
||||
plan,
|
||||
dataSchema,
|
||||
onConfirm,
|
||||
onEdit,
|
||||
loading = false
|
||||
}) => {
|
||||
// 查找变量类型信息
|
||||
const getColumnInfo = (colName: string) => {
|
||||
const col = dataSchema.columns.find(c => c.name === colName);
|
||||
if (!col) return '';
|
||||
if (col.type === 'categorical' && col.uniqueValues) {
|
||||
return `(分类: ${col.uniqueValues.slice(0, 3).join('/')})`;
|
||||
}
|
||||
return `(${col.type === 'numeric' ? '数值型' : '分类型'})`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="plan-card"
|
||||
title={
|
||||
<Space>
|
||||
<span>分析方案确认</span>
|
||||
<Tag color="blue">{plan.tool_name}</Tag>
|
||||
</Space>
|
||||
}
|
||||
styles={{ header: { background: '#f8fafc' } }}
|
||||
>
|
||||
{/* 变量映射 */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
{Object.entries(plan.params).map(([key, value]) => (
|
||||
<div key={key} className="bg-slate-50 p-3 rounded border border-slate-100">
|
||||
<div className="text-xs text-slate-400 uppercase font-bold mb-1">
|
||||
{key.replace(/_/g, ' ')}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-800">
|
||||
{String(value)}
|
||||
<span className="text-xs text-slate-400 font-normal ml-1">
|
||||
{getColumnInfo(String(value))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 护栏提示 */}
|
||||
{plan.guardrails.check_normality && (
|
||||
<Alert
|
||||
type="warning"
|
||||
icon={<SafetyOutlined />}
|
||||
showIcon
|
||||
message="统计护栏 (自动执行)"
|
||||
description={
|
||||
<ul className="list-disc list-inside text-xs mt-1 space-y-1">
|
||||
<li>Shapiro-Wilk 正态性检验</li>
|
||||
<li>Levene 方差齐性检验</li>
|
||||
{plan.guardrails.auto_fix && (
|
||||
<li className="font-medium">
|
||||
⚠️ 若正态性检验失败,将自动降级为 Wilcoxon 秩和检验
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end gap-3 pt-3 border-t border-slate-100">
|
||||
<Button icon={<EditOutlined />} onClick={onEdit}>
|
||||
修改参数
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={onConfirm}
|
||||
loading={loading}
|
||||
>
|
||||
确认并执行
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 ExecutionTrace(执行路径树)
|
||||
|
||||
```tsx
|
||||
// components/cards/ExecutionTrace.tsx
|
||||
import React from 'react';
|
||||
import { CheckCircleFilled, ExclamationCircleFilled,
|
||||
SwapOutlined, CalculatorOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
interface TraceStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: 'success' | 'warning' | 'error' | 'running' | 'pending';
|
||||
detail?: string;
|
||||
subLabel?: string;
|
||||
}
|
||||
|
||||
interface ExecutionTraceProps {
|
||||
steps: TraceStep[];
|
||||
}
|
||||
|
||||
export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({ steps }) => {
|
||||
const getIcon = (status: TraceStep['status']) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircleFilled className="text-green-500" />;
|
||||
case 'warning':
|
||||
return <ExclamationCircleFilled className="text-amber-500" />;
|
||||
case 'error':
|
||||
return <ExclamationCircleFilled className="text-red-500" />;
|
||||
case 'running':
|
||||
return <LoadingOutlined className="text-blue-500" spin />;
|
||||
default:
|
||||
return <div className="w-4 h-4 rounded-full bg-slate-200" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm">
|
||||
<div className="text-xs font-bold text-slate-400 uppercase mb-3 tracking-wider flex justify-between">
|
||||
<span>执行路径</span>
|
||||
{steps.every(s => s.status === 'success') && (
|
||||
<span className="text-green-600">
|
||||
<CheckCircleFilled /> 完成
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 font-mono text-xs relative pl-2">
|
||||
{/* 连接线 */}
|
||||
<div className="absolute left-[19px] top-2 bottom-4 w-px bg-slate-200" />
|
||||
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step.id} className="flex items-start gap-3 relative z-10">
|
||||
<div className="w-4 h-4 flex items-center justify-center mt-0.5">
|
||||
{getIcon(step.status)}
|
||||
</div>
|
||||
<div>
|
||||
<span className={`
|
||||
${step.status === 'warning' ? 'text-amber-700 font-medium' : ''}
|
||||
${step.status === 'success' && step.detail ? 'text-slate-800 font-medium' : 'text-slate-600'}
|
||||
`}>
|
||||
{step.label}
|
||||
</span>
|
||||
{step.detail && (
|
||||
<div className="mt-1">
|
||||
<span className={`
|
||||
px-1.5 py-0.5 rounded border font-bold
|
||||
${step.status === 'error'
|
||||
? 'bg-red-50 text-red-600 border-red-100'
|
||||
: 'bg-green-50 text-green-600 border-green-100'}
|
||||
`}>
|
||||
{step.detail}
|
||||
</span>
|
||||
{step.subLabel && (
|
||||
<span className="ml-2 text-slate-400">{step.subLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 使用示例
|
||||
const mockSteps: TraceStep[] = [
|
||||
{ id: '1', label: '加载数据 (n=150)', status: 'success' },
|
||||
{
|
||||
id: '2',
|
||||
label: '正态性检验 (Shapiro-Wilk)',
|
||||
status: 'error',
|
||||
detail: 'P = 0.002 (< 0.05) ❌',
|
||||
subLabel: '-> 拒绝正态假设'
|
||||
},
|
||||
{ id: '3', label: '策略切换: T-Test -> Wilcoxon Test', status: 'warning' },
|
||||
{ id: '4', label: '计算完成', status: 'success' },
|
||||
];
|
||||
```
|
||||
|
||||
### 3.3 ExecutionProgress(📌 执行进度动画)
|
||||
|
||||
```tsx
|
||||
// components/cards/ExecutionProgress.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { LoadingOutlined, CheckCircleFilled } from '@ant-design/icons';
|
||||
import { Progress, Typography } from 'antd';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ExecutionProgressProps {
|
||||
isExecuting: boolean;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
// 📌 模拟进度文案(缓解用户等待焦虑)
|
||||
const PROGRESS_MESSAGES = [
|
||||
'正在加载数据...',
|
||||
'执行统计护栏检验...',
|
||||
'进行核心计算...',
|
||||
'生成可视化图表...',
|
||||
'格式化结果...',
|
||||
'即将完成...'
|
||||
];
|
||||
|
||||
export const ExecutionProgress: React.FC<ExecutionProgressProps> = ({
|
||||
isExecuting,
|
||||
onComplete
|
||||
}) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [messageIndex, setMessageIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExecuting) {
|
||||
setProgress(0);
|
||||
setMessageIndex(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 📌 模拟进度(实际进度由后端控制)
|
||||
const progressInterval = setInterval(() => {
|
||||
setProgress(prev => {
|
||||
if (prev >= 90) return prev; // 卡在 90%,等待真正完成
|
||||
return prev + Math.random() * 10;
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const messageInterval = setInterval(() => {
|
||||
setMessageIndex(prev =>
|
||||
prev < PROGRESS_MESSAGES.length - 1 ? prev + 1 : prev
|
||||
);
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(messageInterval);
|
||||
};
|
||||
}, [isExecuting]);
|
||||
|
||||
if (!isExecuting) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-blue-200 rounded-xl p-6 shadow-sm animate-pulse">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LoadingOutlined className="text-blue-500 text-xl" spin />
|
||||
<Text strong className="text-blue-700">正在执行统计分析</Text>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
percent={Math.round(progress)}
|
||||
status="active"
|
||||
strokeColor={{
|
||||
'0%': '#3b82f6',
|
||||
'100%': '#10b981'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text type="secondary" className="text-sm mt-2 block">
|
||||
{PROGRESS_MESSAGES[messageIndex]}
|
||||
</Text>
|
||||
|
||||
<Text type="secondary" className="text-xs mt-4 block opacity-60">
|
||||
复杂计算可能需要 10-30 秒,请耐心等待...
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3.4 ResultCard(结果报告卡片)
|
||||
|
||||
```tsx
|
||||
// components/cards/ResultCard.tsx
|
||||
import React from 'react';
|
||||
import { Button, Divider, Space, Typography } from 'antd';
|
||||
import { DownloadOutlined, FileWordOutlined } from '@ant-design/icons';
|
||||
import { APATable } from '../common/APATable';
|
||||
import { PlotViewer } from '../common/PlotViewer';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface ResultCardProps {
|
||||
result: {
|
||||
method: string;
|
||||
statistic: number;
|
||||
p_value: number;
|
||||
p_value_fmt: string; // 🆕 R 服务返回的格式化 p 值
|
||||
group_stats: Array<{
|
||||
group: string;
|
||||
n: number;
|
||||
mean?: number;
|
||||
median?: number;
|
||||
sd?: number;
|
||||
iqr?: [number, number];
|
||||
}>;
|
||||
};
|
||||
plots: string[]; // Base64 图片
|
||||
interpretation?: string; // Critic 解读
|
||||
reproducibleCode: string;
|
||||
onDownloadCode: () => void;
|
||||
}
|
||||
|
||||
export const ResultCard: React.FC<ResultCardProps> = ({
|
||||
result,
|
||||
plots,
|
||||
interpretation,
|
||||
reproducibleCode,
|
||||
onDownloadCode
|
||||
}) => {
|
||||
// 构造表格数据
|
||||
const tableData = result.group_stats.map(g => ({
|
||||
group: g.group,
|
||||
n: g.n,
|
||||
value: g.median
|
||||
? `${g.median.toFixed(2)} [${g.iqr?.[0].toFixed(2)} - ${g.iqr?.[1].toFixed(2)}]`
|
||||
: `${g.mean?.toFixed(2)} ± ${g.sd?.toFixed(2)}`
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{ key: 'group', title: 'Group', width: 120 },
|
||||
{ key: 'n', title: 'N', width: 60, align: 'right' as const },
|
||||
{ key: 'value', title: result.method.includes('Wilcoxon') ? 'Median [IQR]' : 'Mean ± SD' },
|
||||
{
|
||||
key: 'statistic',
|
||||
title: 'Statistic',
|
||||
render: () => result.statistic.toFixed(2),
|
||||
rowSpan: tableData.length
|
||||
},
|
||||
{
|
||||
key: 'pValue',
|
||||
title: 'P-Value',
|
||||
render: () => (
|
||||
<Text strong>
|
||||
{/* 🆕 直接使用 R 服务返回的格式化值 */}
|
||||
{result.p_value_fmt}
|
||||
{result.p_value < 0.01 ? ' **' : result.p_value < 0.05 ? ' *' : ''}
|
||||
</Text>
|
||||
),
|
||||
rowSpan: tableData.length
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-md overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200">
|
||||
<Title level={5} className="mb-0">分析结果报告</Title>
|
||||
<Text type="secondary" className="text-xs">
|
||||
基于 {result.method}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 1. 统计表格 */}
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-1 h-4 bg-blue-600 rounded-full" />
|
||||
<Text strong>表 1. 组间差异比较 (三线表)</Text>
|
||||
</div>
|
||||
|
||||
<APATable columns={columns} data={tableData} />
|
||||
|
||||
<Text type="secondary" className="text-xs mt-2 block italic">
|
||||
Note: {result.method.includes('Wilcoxon') ? 'IQR = Interquartile Range' : 'SD = Standard Deviation'};
|
||||
* P < 0.05, ** P < 0.01.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 2. 图表 */}
|
||||
{plots.length > 0 && (
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-1 h-4 bg-blue-600 rounded-full" />
|
||||
<Text strong>图 1. 可视化结果</Text>
|
||||
</div>
|
||||
<PlotViewer src={plots[0]} alt="Statistical Plot" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. 方法与解读 */}
|
||||
{interpretation && (
|
||||
<div className="p-6 bg-slate-50/50 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-1 h-4 bg-indigo-600 rounded-full" />
|
||||
<Text strong>方法与结果解读</Text>
|
||||
</div>
|
||||
<div
|
||||
className="prose prose-sm max-w-none text-slate-600"
|
||||
dangerouslySetInnerHTML={{ __html: interpretation }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. 资产交付 */}
|
||||
<div className="bg-slate-100 p-4 flex items-center justify-between">
|
||||
<Text type="secondary" className="text-xs font-semibold uppercase tracking-wide">
|
||||
资产交付
|
||||
</Text>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onDownloadCode}
|
||||
>
|
||||
下载 R 代码
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<FileWordOutlined />}
|
||||
disabled // MVP 阶段禁用
|
||||
>
|
||||
导出分析报告 (Word)
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3.5 APATable(三线表)
|
||||
|
||||
```tsx
|
||||
// components/common/APATable.tsx
|
||||
import React from 'react';
|
||||
import './APATable.css';
|
||||
|
||||
interface Column {
|
||||
key: string;
|
||||
title: string;
|
||||
width?: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
render?: (value: any, record: any, index: number) => React.ReactNode;
|
||||
rowSpan?: number;
|
||||
}
|
||||
|
||||
interface APATableProps {
|
||||
columns: Column[];
|
||||
data: Record<string, any>[];
|
||||
}
|
||||
|
||||
export const APATable: React.FC<APATableProps> = ({ columns, data }) => {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="apa-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th
|
||||
key={col.key}
|
||||
style={{ width: col.width, textAlign: col.align || 'left' }}
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIdx) => (
|
||||
<tr key={rowIdx}>
|
||||
{columns.map((col, colIdx) => {
|
||||
// 处理 rowSpan
|
||||
if (col.rowSpan && rowIdx > 0) return null;
|
||||
|
||||
const value = col.render
|
||||
? col.render(row[col.key], row, rowIdx)
|
||||
: row[col.key];
|
||||
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
rowSpan={col.rowSpan}
|
||||
style={{ textAlign: col.align || 'left' }}
|
||||
className={col.rowSpan ? 'align-middle border-l border-slate-100' : ''}
|
||||
>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```css
|
||||
/* components/common/APATable.css */
|
||||
.apa-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.apa-table thead th {
|
||||
border-top: 2px solid #1e293b;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.apa-table tbody td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.apa-table tbody tr:last-child td {
|
||||
border-bottom: 2px solid #1e293b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Zustand Store
|
||||
|
||||
```typescript
|
||||
// store/ssaStore.ts
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
contentType: 'text' | 'plan' | 'result' | 'trace';
|
||||
content: any;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface SSAState {
|
||||
// 会话
|
||||
sessionId: string | null;
|
||||
sessionTitle: string;
|
||||
|
||||
// 数据
|
||||
dataLoaded: boolean;
|
||||
dataSchema: object | null;
|
||||
dataFileName: string;
|
||||
dataRowCount: number;
|
||||
|
||||
// 消息
|
||||
messages: Message[];
|
||||
|
||||
// 执行状态
|
||||
isPlanning: boolean;
|
||||
isExecuting: boolean;
|
||||
currentPlan: object | null;
|
||||
|
||||
// Actions
|
||||
setSession: (id: string, title?: string) => void;
|
||||
setDataLoaded: (schema: object, fileName: string, rowCount: number) => void;
|
||||
addMessage: (message: Omit<Message, 'id' | 'createdAt'>) => void;
|
||||
setPlanning: (planning: boolean) => void;
|
||||
setExecuting: (executing: boolean) => void;
|
||||
setCurrentPlan: (plan: object | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useSSAStore = create<SSAState>((set, get) => ({
|
||||
sessionId: null,
|
||||
sessionTitle: '新会话',
|
||||
dataLoaded: false,
|
||||
dataSchema: null,
|
||||
dataFileName: '',
|
||||
dataRowCount: 0,
|
||||
messages: [],
|
||||
isPlanning: false,
|
||||
isExecuting: false,
|
||||
currentPlan: null,
|
||||
|
||||
setSession: (id, title = '新会话') => set({ sessionId: id, sessionTitle: title }),
|
||||
|
||||
setDataLoaded: (schema, fileName, rowCount) => set({
|
||||
dataLoaded: true,
|
||||
dataSchema: schema,
|
||||
dataFileName: fileName,
|
||||
dataRowCount: rowCount
|
||||
}),
|
||||
|
||||
addMessage: (message) => set(state => ({
|
||||
messages: [
|
||||
...state.messages,
|
||||
{
|
||||
...message,
|
||||
id: crypto.randomUUID(),
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
})),
|
||||
|
||||
setPlanning: (planning) => set({ isPlanning: planning }),
|
||||
setExecuting: (executing) => set({ isExecuting: executing }),
|
||||
setCurrentPlan: (plan) => set({ currentPlan: plan }),
|
||||
|
||||
reset: () => set({
|
||||
sessionId: null,
|
||||
sessionTitle: '新会话',
|
||||
dataLoaded: false,
|
||||
dataSchema: null,
|
||||
dataFileName: '',
|
||||
dataRowCount: 0,
|
||||
messages: [],
|
||||
isPlanning: false,
|
||||
isExecuting: false,
|
||||
currentPlan: null
|
||||
})
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 封装
|
||||
|
||||
```typescript
|
||||
// api/ssaApi.ts
|
||||
import { apiClient } from '@/common/api/client';
|
||||
|
||||
const BASE = '/api/v1/ssa';
|
||||
|
||||
export const ssaApi = {
|
||||
// 会话
|
||||
createSession: () =>
|
||||
apiClient.post<{ id: string }>(`${BASE}/sessions`),
|
||||
|
||||
getSession: (id: string) =>
|
||||
apiClient.get(`${BASE}/sessions/${id}`),
|
||||
|
||||
listSessions: () =>
|
||||
apiClient.get(`${BASE}/sessions`),
|
||||
|
||||
// 数据上传
|
||||
uploadData: (sessionId: string, file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return apiClient.post(`${BASE}/sessions/${sessionId}/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
},
|
||||
|
||||
// 生成计划
|
||||
generatePlan: (sessionId: string, query: string) =>
|
||||
apiClient.post(`${BASE}/sessions/${sessionId}/plan`, { query }),
|
||||
|
||||
// 执行分析(📌 超时 120s)
|
||||
executeAnalysis: (sessionId: string, plan: object) =>
|
||||
apiClient.post(`${BASE}/sessions/${sessionId}/execute`, { plan }, {
|
||||
timeout: 120000 // 📌 120s 超时,应对复杂计算
|
||||
}),
|
||||
|
||||
// 下载代码
|
||||
downloadCode: (sessionId: string, messageId: string) =>
|
||||
apiClient.get(`${BASE}/sessions/${sessionId}/download-code/${messageId}`, {
|
||||
responseType: 'blob'
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 模块注册
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import { lazy } from 'react';
|
||||
|
||||
const SSAWorkspace = lazy(() => import('./pages/SSAWorkspace'));
|
||||
|
||||
export const ssaRoutes = [
|
||||
{
|
||||
path: '/ssa',
|
||||
element: <SSAWorkspace />,
|
||||
meta: {
|
||||
title: '智能统计分析',
|
||||
icon: 'BarChartOutlined',
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 在 moduleRegistry.ts 中注册
|
||||
// import { ssaRoutes } from './modules/ssa';
|
||||
// registerModule('ssa', ssaRoutes);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 样式规范
|
||||
|
||||
### 7.1 颜色系统(与原型对齐)
|
||||
|
||||
```css
|
||||
/* styles/ssa.css */
|
||||
:root {
|
||||
--ssa-primary: #3b82f6; /* blue-500 */
|
||||
--ssa-primary-hover: #2563eb; /* blue-600 */
|
||||
--ssa-bg: #f8fafc; /* slate-50 */
|
||||
--ssa-card-bg: #ffffff;
|
||||
--ssa-border: #e2e8f0; /* slate-200 */
|
||||
--ssa-text: #334155; /* slate-700 */
|
||||
--ssa-text-muted: #94a3b8; /* slate-400 */
|
||||
--ssa-success: #22c55e; /* green-500 */
|
||||
--ssa-warning: #f59e0b; /* amber-500 */
|
||||
--ssa-error: #ef4444; /* red-500 */
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 动画
|
||||
|
||||
```css
|
||||
/* 渐入动画 */
|
||||
.ssa-fade-in {
|
||||
animation: ssaFadeIn 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 上滑动画 */
|
||||
.ssa-slide-up {
|
||||
animation: ssaSlideUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
@keyframes ssaFadeIn {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes ssaSlideUp {
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 开发检查清单
|
||||
|
||||
| 组件 | 功能 | 状态 |
|
||||
|------|------|------|
|
||||
| SSASidebar | 导入数据、新建会话、历史列表、数据状态 | ⬜ |
|
||||
| DataUploader | 拖拽/点击上传,进度显示 | ⬜ |
|
||||
| MessageList | 消息流滚动,自动滚底 | ⬜ |
|
||||
| PlanCard | 参数展示、护栏提示、确认/修改按钮 | ⬜ |
|
||||
| ExecutionTrace | 步骤树、状态图标、连接线 | ⬜ |
|
||||
| **ExecutionProgress** | **📌 执行中进度动画,缓解等待焦虑** | ⬜ |
|
||||
| ResultCard | 三线表、图表、解读、下载按钮 | ⬜ |
|
||||
| APATable | APA 格式表格样式 | ⬜ |
|
||||
| Zustand Store | 状态管理 | ⬜ |
|
||||
| API 对接 | 所有接口联调,**超时 120s** | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 9. 关键配置
|
||||
|
||||
### 9.1 Axios 全局超时配置
|
||||
|
||||
```typescript
|
||||
// api/client.ts
|
||||
import axios from 'axios';
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 60000, // 默认 60s
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 📌 对于 SSA 执行接口,单独设置 120s 超时
|
||||
// 见 ssaApi.executeAnalysis
|
||||
```
|
||||
Reference in New Issue
Block a user