From c6129d68fcaccf34b3fe85842f9448542d8a8cbb Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 12:44:04 +0800 Subject: [PATCH] =?UTF-8?q?docs(ai):=20erp-ai=20Phase=201=20=E5=AE=9E?= =?UTF-8?q?=E6=96=BD=E8=AE=A1=E5=88=92=20=E2=80=94=20Chunk=201=20(crate=20?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=20+=20core=20=E6=89=A9=E5=B1=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-04-25-erp-ai-phase1-mvp.md | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md diff --git a/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md b/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md new file mode 100644 index 0000000..8070b9e --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md @@ -0,0 +1,404 @@ +# erp-ai Phase 1 MVP 实施计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 HMS 新建 erp-ai crate,实现 AI 智能分析流 SSE 端点,支持化验单解读/趋势分析/个性化方案/报告摘要 + +**Architecture:** 新建独立 erp-ai crate,通过 HealthDataProvider trait 从 erp-health 获取脱敏数据,AiProvider trait 抽象 AI 提供商(Phase 1 实现 Claude SSE),请求驱动管道 + SSE 流式返回 + +**Tech Stack:** Rust/Axum/SeaORM/PostgreSQL + futures/tokio-stream/async-stream (SSE) + serde_json/uuid/chrono/thiserror/utoipa + +**设计规格:** `docs/superpowers/specs/2026-04-25-erp-ai-module-design.md` + +--- + +## Chunk 1: Crate 骨架 + 错误类型 + erp-core 扩展 + +### Task 1: 创建 erp-ai crate 骨架 + +**Files:** +- Create: `crates/erp-ai/Cargo.toml` +- Create: `crates/erp-ai/src/lib.rs` +- Create: `crates/erp-ai/src/error.rs` +- Modify: `Cargo.toml` (workspace root) — 添加 erp-ai 到 workspace + +- [ ] **Step 1: 创建 crate 目录** + +```bash +mkdir -p crates/erp-ai/src +``` + +- [ ] **Step 2: 创建 Cargo.toml** + +```toml +# crates/erp-ai/Cargo.toml +[package] +name = "erp-ai" +version.workspace = true +edition.workspace = true + +[dependencies] +erp-core.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-stream.workspace = true +futures.workspace = true +async-stream.workspace = true +serde.workspace = true +serde_json.workspace = true +uuid.workspace = true +chrono.workspace = true +axum.workspace = true +sea-orm.workspace = true +tracing.workspace = true +thiserror.workspace = true +utoipa.workspace = true +async-trait.workspace = true +reqwest = { version = "0.12", features = ["stream", "json"] } +handlebars = "6" +sha2 = "0.10" +hex = "0.4" +``` + +> 注意: `futures`, `tokio-stream`, `async-stream`, `reqwest`, `handlebars`, `sha2` 需要加入 workspace 依赖或在此声明版本。参照 `crates/erp-health/Cargo.toml` 模式。 + +- [ ] **Step 3: 创建 error.rs** + +```rust +// crates/erp-ai/src/error.rs +use erp_core::AppError; + +#[derive(Debug, thiserror::Error)] +pub enum AiError { + #[error("验证失败: {0}")] + Validation(String), + + #[error("分析未找到: {0}")] + AnalysisNotFound(String), + + #[error("Prompt 模板未找到: {0}")] + PromptNotFound(String), + + #[error("AI 提供商不可用: {0}")] + ProviderUnavailable(String), + + #[error("AI 提供商错误: {0}")] + ProviderError(String), + + #[error("数据脱敏失败: {0}")] + SanitizationError(String), + + #[error("模板渲染失败: {0}")] + TemplateError(String), + + #[error("速率超限")] + RateLimitExceeded, + + #[error("版本不匹配")] + VersionMismatch, + + #[error("数据库错误: {0}")] + DbError(String), +} + +impl From for AppError { + fn from(e: AiError) -> Self { + match e { + AiError::Validation(msg) => AppError::Validation(msg), + AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")), + AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")), + AiError::ProviderUnavailable(p) => AppError::ServiceUnavailable(format!("AI 提供商 {p} 不可用")), + AiError::RateLimitExceeded => AppError::TooManyRequests, + AiError::VersionMismatch => AppError::VersionMismatch, + AiError::DbError(msg) => AppError::Internal(msg), + other => AppError::Internal(other.to_string()), + } + } +} + +impl From for AiError { + fn from(e: sea_orm::DbErr) -> Self { + AiError::DbError(e.to_string()) + } +} + +pub type AiResult = Result; +``` + +> 注意: 检查 `AppError` 是否有 `ServiceUnavailable` 变体。如果没有,使用 `AppError::Internal` 替代。 + +- [ ] **Step 4: 创建 lib.rs (最小骨架)** + +```rust +// crates/erp-ai/src/lib.rs +pub mod error; + +pub use error::{AiError, AiResult}; +``` + +- [ ] **Step 5: 注册到 workspace** + +在根 `Cargo.toml` 的 `[workspace] members` 数组中添加 `"crates/erp-ai"`,在 `[workspace.dependencies]` 中添加: + +```toml +erp-ai = { path = "crates/erp-ai" } +``` + +同时确认以下依赖在 workspace dependencies 中存在(如不存在则添加): + +```toml +futures = "0.3" +tokio-stream = "0.1" +async-stream = "0.3" +reqwest = { version = "0.12", features = ["stream", "json"] } +handlebars = "6" +sha2 = "0.10" +hex = "0.4" +``` + +- [ ] **Step 6: 验证编译** + +```bash +cargo check -p erp-ai +``` + +Expected: 编译通过,无错误 + +- [ ] **Step 7: 提交** + +```bash +git add crates/erp-ai/ Cargo.toml +git commit -m "feat(ai): 创建 erp-ai crate 骨架 + 错误类型" +``` + +--- + +### Task 2: erp-core 扩展 — HealthDataProvider trait + AI 权限码 + 事件类型 + +**Files:** +- Create: `crates/erp-core/src/health_provider.rs` — trait + DTO 定义 +- Modify: `crates/erp-core/src/lib.rs` — 添加 pub mod +- Modify: `crates/erp-health/src/health_provider_impl.rs` — trait 实现 (stub) +- Modify: `crates/erp-health/src/lib.rs` — 添加 pub mod +- Modify: `crates/erp-health/src/module.rs` — permissions() 中声明 AI 权限 + +> 注意: AI 权限码放在 erp-ai 模块的 permissions() 中,不在 erp-health。此处仅做 erp-core 的 trait 扩展。 + +- [ ] **Step 1: 创建 HealthDataProvider trait + DTO** + +```rust +// crates/erp-core/src/health_provider.rs +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::AppResult; + +/// 健康数据提供者 trait,由 erp-health 实现 +/// 返回的 DTO 已脱去 PII(姓名、身份证号等),只包含年龄/性别/医疗数据 +#[async_trait] +pub trait HealthDataProvider: Send + Sync { + /// 获取化验报告(指标列表) + async fn get_lab_report( + &self, + tenant_id: Uuid, + report_id: Uuid, + ) -> AppResult; + + /// 获取生命体征趋势数据 + async fn get_vital_signs( + &self, + tenant_id: Uuid, + patient_id: Uuid, + metrics: &[String], + range: &TimeRange, + ) -> AppResult>; + + /// 获取患者摘要(用于个性化方案) + async fn get_patient_summary( + &self, + tenant_id: Uuid, + patient_id: Uuid, + ) -> AppResult; + + /// 获取完整健康报告(用于摘要生成) + async fn get_full_report( + &self, + tenant_id: Uuid, + report_id: Uuid, + ) -> AppResult; +} + +// === DTO 定义 === + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimeRange { + pub start: chrono::DateTime, + pub end: chrono::DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabReportDto { + pub age_group: String, + pub sex: String, + pub department: String, + pub report_date: String, + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabItemDto { + pub name: String, + pub value: f64, + pub unit: String, + pub reference_range: String, + pub is_abnormal: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VitalSignDto { + pub metric: String, + pub values: Vec<(String, f64)>, + pub unit: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PatientSummaryDto { + pub age_group: String, + pub sex: String, + pub chronic_conditions: Vec, + pub medications: Vec, + pub family_history: Vec, + pub last_checkup_date: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthReportDto { + pub age_group: String, + pub sex: String, + pub department: String, + pub report_date: String, + pub sections: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReportSectionDto { + pub title: String, + pub findings: Vec, + pub abnormal_items: Vec, +} +``` + +- [ ] **Step 2: 在 erp-core/src/lib.rs 中添加 pub mod** + +```rust +pub mod health_provider; +``` + +并添加 re-export: + +```rust +pub use health_provider::{ + HealthDataProvider, LabItemDto, LabReportDto, PatientSummaryDto, + HealthReportDto, ReportSectionDto, TimeRange, VitalSignDto, +}; +``` + +- [ ] **Step 3: 验证 erp-core 编译** + +```bash +cargo check -p erp-core +``` + +- [ ] **Step 4: 提交 erp-core 扩展** + +```bash +git add crates/erp-core/src/health_provider.rs crates/erp-core/src/lib.rs +git commit -m "feat(core): 新增 HealthDataProvider trait + DTO 定义" +``` + +--- + +### Task 3: erp-health 实现 HealthDataProvider (stub) + +**Files:** +- Create: `crates/erp-health/src/health_provider_impl.rs` +- Modify: `crates/erp-health/src/lib.rs` + +> 注意: Phase 1 先创建 stub 实现(返回 todo! 或空数据),确保编译通过。实际数据查询在 Chunk 5 集成时完善。 + +- [ ] **Step 1: 创建 stub 实现** + +```rust +// crates/erp-health/src/health_provider_impl.rs +use async_trait::async_trait; +use erp_core::{ + AppResult, HealthDataProvider, LabReportDto, VitalSignDto, + PatientSummaryDto, HealthReportDto, TimeRange, +}; +use uuid::Uuid; + +pub struct HealthDataProviderImpl { + pub db: sea_orm::DatabaseConnection, +} + +#[async_trait] +impl HealthDataProvider for HealthDataProviderImpl { + async fn get_lab_report( + &self, + _tenant_id: Uuid, + _report_id: Uuid, + ) -> AppResult { + todo!("Chunk 5: 实现化验报告数据查询") + } + + async fn get_vital_signs( + &self, + _tenant_id: Uuid, + _patient_id: Uuid, + _metrics: &[String], + _range: &TimeRange, + ) -> AppResult> { + todo!("Chunk 5: 实现生命体征趋势查询") + } + + async fn get_patient_summary( + &self, + _tenant_id: Uuid, + _patient_id: Uuid, + ) -> AppResult { + todo!("Chunk 5: 实现患者摘要查询") + } + + async fn get_full_report( + &self, + _tenant_id: Uuid, + _report_id: Uuid, + ) -> AppResult { + todo!("Chunk 5: 实现完整报告查询") + } +} +``` + +- [ ] **Step 2: 在 erp-health/src/lib.rs 添加 pub mod** + +```rust +pub mod health_provider_impl; +pub use health_provider_impl::HealthDataProviderImpl; +``` + +- [ ] **Step 3: 验证全 workspace 编译** + +```bash +cargo check --workspace +``` + +Expected: 编译通过(stub 的 todo! 不影响编译) + +- [ ] **Step 4: 提交** + +```bash +git add crates/erp-health/src/health_provider_impl.rs crates/erp-health/src/lib.rs +git commit -m "feat(health): 添加 HealthDataProvider stub 实现" +``` + +---