From db626d27b8223d46c2a26440af7d38dc687af4a3 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 12:34:34 +0800 Subject: [PATCH] =?UTF-8?q?docs(ai):=20=E4=BF=AE=E5=A4=8D=20spec=20?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98=20=E2=80=94=20=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=A8=A1=E5=BC=8F/HealthDataProvider/AiState/?= =?UTF-8?q?=E7=BC=93=E5=AD=98/=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 2 CRITICAL + 4 HIGH + 3 MEDIUM: - CRITICAL: 路由注册改为 public_routes/protected_routes 静态方法 - CRITICAL: 补全 HealthDataProvider trait + DTO 定义 + 注入机制 - HIGH: 修复 async_trait + impl Stream 不兼容 (改用 Pin>) - HIGH: 新增 AiConfig 配置段 + config.toml 定义 - HIGH: sanitized_input 改为 AES-256 加密 + 90 天自动清理 - HIGH: 新增 futures/tokio-stream/async-stream 依赖说明 - MEDIUM: 事件改为 DomainEvent 字符串模式 - MEDIUM: 权限码改为 .list/.manage 实体命名模式 - MEDIUM: 补全 AiState + FromRef 注入模式 - MEDIUM: 补全缓存设计 (Redis/键格式/TTL/Hash算法) --- .../specs/2026-04-25-erp-ai-module-design.md | 310 ++++++++++++++++-- 1 file changed, 277 insertions(+), 33 deletions(-) diff --git a/docs/superpowers/specs/2026-04-25-erp-ai-module-design.md b/docs/superpowers/specs/2026-04-25-erp-ai-module-design.md index e6d9787..ffe81ef 100644 --- a/docs/superpowers/specs/2026-04-25-erp-ai-module-design.md +++ b/docs/superpowers/specs/2026-04-25-erp-ai-module-design.md @@ -86,9 +86,13 @@ crates/erp-ai/ ```rust /// AI 提供商 trait +/// 使用 Pin> 避免 async_trait + impl Trait 不兼容问题 #[async_trait] pub trait AiProvider: Send + Sync { - async fn stream_generate(&self, req: GenerateRequest) -> Result>; + async fn stream_generate( + &self, + req: GenerateRequest, + ) -> Result> + Send>>>; async fn generate(&self, req: GenerateRequest) -> Result; fn name(&self) -> &str; async fn health_check(&self) -> Result; @@ -96,44 +100,266 @@ pub trait AiProvider: Send + Sync { /// 分析管道 trait (Phase 1: RequestPipeline; Phase 3: + AsyncPipeline) pub trait AnalysisPipeline: Send + Sync { - fn analyze(&self, ctx: AnalysisContext) -> impl Stream; + fn analyze( + &self, + ctx: AnalysisContext, + ) -> Pin + Send>>; } -/// 数据脱敏 trait +/// 数据脱敏 trait — 输入为 HealthDataProvider 返回的 DTO,非原始 SeaORM Entity pub trait DataSanitizer: Send + Sync { - fn sanitize(&self, data: &serde_json::Value) -> Result; + fn sanitize(&self, data: &HealthAnalysisInput) -> Result; +} + +/// 分析上下文 +pub struct AnalysisContext { + pub trigger: AnalysisTrigger, + pub tenant_id: Uuid, + pub user_id: Uuid, + pub analysis_type: AnalysisType, + pub source_ref: String, +} + +pub enum AnalysisTrigger { + Request, // Phase 1: 患者主动请求 + Event, // Phase 3: 异步事件触发 (预留) } ``` -### 3.3 erp-core 扩展 +### 3.3 路由注册(遵循现有 public_routes / protected_routes 模式) ```rust -/// 新增到 erp-core,由 erp-health 实现 +/// erp-ai 模块注册(参照 erp-health 的注册方式) +pub struct AiModule; + +impl AiModule { + pub fn public_routes() -> Router + where + S: Clone + Send + Sync + 'static, + AiState: FromRef, + { + Router::new() + .nest("/api/v1/ai/analyze", analyze::public_routes::()) + } + + pub fn protected_routes() -> Router + where + S: Clone + Send + Sync + 'static, + AiState: FromRef, + { + Router::new() + .nest("/api/v1/ai/analyze", analyze::protected_routes::()) + .nest("/api/v1/ai/analysis", analysis::routes::()) + .nest("/api/v1/ai/prompts", prompt::routes::()) + .nest("/api/v1/ai/usage", usage::routes::()) + .nest("/api/v1/ai/admin", admin::routes::()) + } +} + +// erp-server/src/main.rs 中合并路由: +// let app = Router::new() +// .merge(AuthModule::public_routes()) +// // ... +// .merge(AiModule::public_routes()) +// .merge(AuthModule::protected_routes()) +// // ... +// .merge(AiModule::protected_routes()) +``` + +### 3.4 AiState 注入(遵循现有 FromRef 模式) + +```rust +/// erp-ai 的 Axum State +pub struct AiState { + pub db: DatabaseConnection, + pub event_bus: EventBus, + pub provider_registry: ProviderRegistry, + pub sanitizer: Arc, + pub prompt_manager: Arc, + pub redis: deadpool_redis::Pool, + pub config: AiConfig, +} + +// erp-server/src/state.rs 中新增: +// impl FromRef for AiState { ... } +``` + +### 3.5 HealthDataProvider trait 与数据 DTO + +```rust +// === erp-core 中新增 === + +/// 健康数据提供者 trait,由 erp-health 实现 +/// 通过 AppState 中的 Arc 注入到 erp-ai +#[async_trait] pub trait HealthDataProvider: Send + Sync { - async fn get_report_by_id(&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; + /// 获取化验报告(指标列表) + 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>; + + /// 获取患者摘要(用于个性化方案,不含 PII 的 DTO) + 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; } -/// 新增事件类型 -pub enum AiEvent { - AnalysisCompleted { analysis_id: Uuid, patient_id: Uuid, analysis_type: String }, - AnalysisFailed { analysis_id: Uuid, error: String }, +// === DTO 定义(erp-core 中,已脱去 PII 的数据传输对象)=== +// 注意: erp-health 实现此 trait 时,返回的 DTO 不包含患者姓名、身份证号等 PII +// 而是只包含 age_group / sex / 科室 / 检验指标 / 诊断编码 等医疗数据 + +pub struct LabReportDto { + pub age_group: String, // "40-50" + pub sex: String, // "male" / "female" + pub department: String, // 科室 + pub report_date: String, // 报告日期 + pub items: Vec, // 检验项目列表 } -/// 新增权限码 +pub struct LabItemDto { + pub name: String, // 指标名称 + pub value: f64, // 检测值 + pub unit: String, // 单位 + pub reference_range: String, // 参考范围 + pub is_abnormal: bool, // 是否异常 +} + +pub struct VitalSignDto { + pub metric: String, // 指标名 + pub values: Vec<(String, f64)>, // (日期, 值) 时间序列 + pub unit: String, +} + +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, +} + +pub struct HealthReportDto { + pub age_group: String, + pub sex: String, + pub department: String, + pub report_date: String, + pub sections: Vec, +} + +pub struct ReportSectionDto { + pub title: String, + pub findings: Vec, + pub abnormal_items: Vec, +} +``` + +### 3.6 事件定义(遵循现有 DomainEvent 字符串模式) + +```rust +// AI 事件使用现有 DomainEvent 结构,event_type 为字符串 +// 而非独立的 AiEvent enum + +// ai.analysis.completed 事件 payload: +// { +// "analysis_id": "uuid", +// "patient_id": "uuid", +// "analysis_type": "lab_report", +// "status": "completed", +// "model_used": "claude-sonnet-4-6" +// } + +// ai.analysis.failed 事件 payload: +// { +// "analysis_id": "uuid", +// "patient_id": "uuid", +// "analysis_type": "lab_report", +// "error": "provider timeout" +// } +``` + +### 3.7 权限码(遵循 .list / .manage 实体命名模式) + +```rust +/// 权限码遵循 CLAUDE.md 规范: 每个实体声明 .list + .manage pub const AI_PERMISSIONS: &[&str] = &[ - "ai.analysis.request", - "ai.analysis.view", - "ai.analysis.manage", - "ai.prompt.list", - "ai.prompt.manage", - "ai.usage.view", - "ai.admin.provider", + "ai.analysis.list", // 查看分析历史列表 + "ai.analysis.manage", // 请求新分析 / 管理分析结果 + "ai.prompt.list", // 查看 Prompt 列表 + "ai.prompt.manage", // 创建/编辑/激活/回滚 Prompt + "ai.usage.list", // 查看用量统计 + "ai.provider.manage", // 管理提供商配置 ]; ``` +### 3.8 配置结构(遵循现有 AppConfig 模式) + +```toml +# config/default.toml 中新增 [ai] 段 + +[ai] +default_provider = "claude" +cache_ttl_seconds = 604800 # 7 天 +rate_limit_patient_daily = 10 +rate_limit_admin_hourly = 100 +sanitized_input_retention_days = 90 # 脱敏输入保留 90 天 + +[ai.providers.claude] +api_key_env = "ANTHROPIC_API_KEY" # 从环境变量读取 +model = "claude-sonnet-4-6" +max_tokens = 2048 +temperature = 0.3 + +[ai.providers.openai] +enabled = false +api_key_env = "OPENAI_API_KEY" +model = "gpt-4o" +``` + +```rust +// erp-server/src/config.rs 新增: +#[derive(Debug, Deserialize)] +pub struct AiConfig { + pub default_provider: String, + pub cache_ttl_seconds: u64, + pub rate_limit_patient_daily: u32, + pub rate_limit_admin_hourly: u32, + pub sanitized_input_retention_days: u32, + pub providers: HashMap, +} +``` + +### 3.9 流式依赖 + +```toml +# erp-ai/Cargo.toml 新增依赖 +[dependencies] +futures = "0.3" +tokio-stream = "0.1" +async-stream = "0.3" +# axum 已内置 SSE 支持: axum::response::sse::{Event, Sse} +``` + --- ## 4. 数据流设计 @@ -211,7 +437,7 @@ data: {"analysis_id": "uuid-xxx", "status": "completed"} | prompt_version | INT | Prompt 版本号 | | model_used | VARCHAR(100) | 实际模型 | | input_data_hash | VARCHAR(64) | SHA-256(缓存键) | -| sanitized_input | JSONB | 脱敏输入快照(审计) | +| sanitized_input | JSONB | 脱敏输入快照(AES-256 加密存储,90天自动清理) | | result_content | TEXT | AI 完整输出 | | result_metadata | JSONB | tokens/耗时/模型信息 | | status | VARCHAR(20) | pending / streaming / completed / failed | @@ -250,17 +476,30 @@ data: {"analysis_id": "uuid-xxx", "status": "completed"} 2. **输入验证**: schema 校验,租户隔离校验 3. **Prompt 注入防护**: JSON 序列化注入,不做字符串拼接 4. **输出过滤**: 正则扫描处方药推荐/诊断结论,强制追加免责声明 -5. **审计日志**: sanitized_input 保留脱敏快照 +5. **审计日志**: sanitized_input AES-256 加密存储,90 天自动清理 -### 6.3 降级策略 +### 6.3 缓存设计 + +**缓存位置:** Redis +**缓存键:** `ai:cache:{tenant_id}:{analysis_type}:{input_data_hash}:{prompt_version}` +**TTL:** 7 天(可配置 `ai.cache_ttl_seconds`) +**Hash 计算:** 对 HealthDataProvider 返回的 DTO 做 canonical JSON 序列化后 SHA-256 +**失效策略:** TTL 自然过期;Prompt 版本切换后旧缓存键自然不同 ``` -缓存命中 → 直接返回 -缓存未命中 + AI 可用 → 正常 SSE -缓存未命中 + AI 不可用 + 有旧版本 → 返回旧结果 + 标注 +缓存命中 (Redis GET) → 直接返回完整 result_content (非流式,秒级) +缓存未命中 + AI 可用 → 正常 SSE 流式分析 → 完成后写入 Redis +缓存未命中 + AI 不可用 + 有旧版本 → 返回旧结果 + 标注"基于历史版本" 缓存未命中 + AI 不可用 + 无历史 → 本地规则引擎(50 条常见指标) ``` +### 6.4 sanitized_input 数据生命周期 + +- 写入时: AES-256-GCM 加密后存储为 JSONB(密钥从环境变量加载) +- 查询时: 仅管理端有权限解密查看,且需 `ai.analysis.manage` 权限 +- 清理: 后台定时任务每日扫描,超过 `sanitized_input_retention_days`(默认 90 天)的记录自动清空此字段 +- 审计: 清理操作本身记录审计日志 + --- ## 7. API 设计 @@ -367,13 +606,18 @@ data: {"analysis_id": "uuid-xxx", "status": "completed"} | 区域 | 文件 | 操作 | |------|------|------| -| erp-core | `src/trait.rs` | 新增 HealthDataProvider trait | -| erp-core | `src/event.rs` | 新增 ai.* 事件类型 | +| `Cargo.toml` (workspace root) | `[workspace].members` + `[workspace.dependencies]` | 添加 erp-ai | +| erp-core | `src/health_provider.rs` | 新增 HealthDataProvider trait + DTO 定义 | +| erp-core | `src/events.rs` | 新增 ai.analysis.* 事件类型常量 | | erp-core | `src/permission.rs` | 新增 ai.* 权限码 | -| erp-health | `src/ai_provider.rs` | 新增 HealthDataProvider impl | +| erp-health | `src/health_provider_impl.rs` | 新增 HealthDataProvider trait impl | | erp-ai | 整个 crate | 新建 | -| erp-server | `src/main.rs` | 注册 AiModule | -| erp-server/migration | 新增 migration | 3 张表 | +| erp-server | `src/main.rs` | 合并 AiModule::public/protected_routes | +| erp-server | `src/state.rs` | 新增 `impl FromRef for AiState` | +| erp-server | `src/config.rs` | 新增 AiConfig 结构 | +| erp-server | `config/default.toml` | 新增 `[ai]` 配置段 | +| erp-server/migration | 新增 migration | 3 张表 (ai_prompts, ai_analysis_results, ai_usage_logs) | +| erp-server/Cargo.toml | dependencies | 添加 erp-ai 依赖 | | apps/miniprogram | 报告详情页 + 解读页 | 新增/修改 | | apps/web | Prompt 管理页面 | Phase 2 新增 |