docs(ai): erp-ai Phase 1 实施计划 — Chunk 1 (crate 骨架 + core 扩展)
This commit is contained in:
404
docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md
Normal file
404
docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md
Normal file
@@ -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<AiError> 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<sea_orm::DbErr> for AiError {
|
||||
fn from(e: sea_orm::DbErr) -> Self {
|
||||
AiError::DbError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type AiResult<T> = Result<T, AiError>;
|
||||
```
|
||||
|
||||
> 注意: 检查 `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<LabReportDto>;
|
||||
|
||||
/// 获取生命体征趋势数据
|
||||
async fn get_vital_signs(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
metrics: &[String],
|
||||
range: &TimeRange,
|
||||
) -> AppResult<Vec<VitalSignDto>>;
|
||||
|
||||
/// 获取患者摘要(用于个性化方案)
|
||||
async fn get_patient_summary(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> AppResult<PatientSummaryDto>;
|
||||
|
||||
/// 获取完整健康报告(用于摘要生成)
|
||||
async fn get_full_report(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
report_id: Uuid,
|
||||
) -> AppResult<HealthReportDto>;
|
||||
}
|
||||
|
||||
// === DTO 定义 ===
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimeRange {
|
||||
pub start: chrono::DateTime<chrono::Utc>,
|
||||
pub end: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[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<LabItemDto>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub medications: Vec<String>,
|
||||
pub family_history: Vec<String>,
|
||||
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<ReportSectionDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReportSectionDto {
|
||||
pub title: String,
|
||||
pub findings: Vec<String>,
|
||||
pub abnormal_items: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **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<LabReportDto> {
|
||||
todo!("Chunk 5: 实现化验报告数据查询")
|
||||
}
|
||||
|
||||
async fn get_vital_signs(
|
||||
&self,
|
||||
_tenant_id: Uuid,
|
||||
_patient_id: Uuid,
|
||||
_metrics: &[String],
|
||||
_range: &TimeRange,
|
||||
) -> AppResult<Vec<VitalSignDto>> {
|
||||
todo!("Chunk 5: 实现生命体征趋势查询")
|
||||
}
|
||||
|
||||
async fn get_patient_summary(
|
||||
&self,
|
||||
_tenant_id: Uuid,
|
||||
_patient_id: Uuid,
|
||||
) -> AppResult<PatientSummaryDto> {
|
||||
todo!("Chunk 5: 实现患者摘要查询")
|
||||
}
|
||||
|
||||
async fn get_full_report(
|
||||
&self,
|
||||
_tenant_id: Uuid,
|
||||
_report_id: Uuid,
|
||||
) -> AppResult<HealthReportDto> {
|
||||
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 实现"
|
||||
```
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user