From 41b17eedad16362753ffca06f8276abf2497ece0 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 13:40:21 +0800 Subject: [PATCH] =?UTF-8?q?docs(ai):=20=E5=AE=9E=E6=96=BD=E8=AE=A1?= =?UTF-8?q?=E5=88=92=20Chunk=205=20(Handler/State/Module=20+=20erp-server?= =?UTF-8?q?=20=E9=9B=86=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-04-25-erp-ai-phase1-mvp.md | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) 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 index 67e85d2..993eb65 100644 --- a/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md +++ b/docs/superpowers/plans/2026-04-25-erp-ai-phase1-mvp.md @@ -1691,3 +1691,276 @@ git commit -m "feat(ai): AnalysisService 核心编排 + PromptService + UsageSer ``` --- + +## Chunk 5: Handler + State + Module + erp-server 集成 + +### Task 9: State + Module 定义 + +**Files:** +- Create: `crates/erp-ai/src/state.rs` +- Create: `crates/erp-ai/src/module.rs` + +- [ ] **Step 1: 创建 state.rs** + +```rust +// crates/erp-ai/src/state.rs +use std::sync::Arc; + +use erp_core::EventBus; +use sea_orm::DatabaseConnection; + +use crate::provider::AiProvider; +use crate::service::analysis::AnalysisService; +use crate::service::prompt::PromptService; +use crate::service::usage::UsageService; + +#[derive(Clone)] +pub struct AiState { + pub db: DatabaseConnection, + pub event_bus: EventBus, + pub analysis: Arc, + pub prompt: Arc, + pub usage: Arc, +} +``` + +- [ ] **Step 2: 创建 module.rs — ErpModule + 路由注册** + +```rust +// crates/erp-ai/src/module.rs +use async_trait::async_trait; +use axum::Router; +use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor}; +use erp_core::AppResult; +use std::any::Any; +use uuid::Uuid; + +use crate::handler; + +pub struct AiModule; + +#[async_trait] +impl ErpModule for AiModule { + fn name(&self) -> &str { "ai" } + fn module_type(&self) -> ModuleType { ModuleType::Builtin } + fn dependencies(&self) -> Vec<&str> { vec!["health"] } + + async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> { + Ok(()) + } + + fn permissions(&self) -> Vec { + vec![ + PermissionDescriptor { + code: "ai.analysis.list".into(), + name: "查看分析历史".into(), + description: "查看 AI 分析结果历史记录".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.analysis.manage".into(), + name: "请求分析".into(), + description: "发起 AI 分析请求".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.prompt.list".into(), + name: "查看 Prompt".into(), + description: "查看 AI Prompt 模板列表".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.prompt.manage".into(), + name: "管理 Prompt".into(), + description: "创建/编辑/激活/回滚 Prompt 模板".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.usage.list".into(), + name: "查看用量".into(), + description: "查看 AI 用量统计".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.provider.manage".into(), + name: "管理提供商".into(), + description: "管理 AI 提供商配置".into(), + module: "ai".into(), + }, + ] + } + + fn as_any(&self) -> &dyn Any { self } +} + +impl AiModule { + pub fn public_routes() -> Router + where + crate::state::AiState: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, + { + Router::new() + } + + pub fn protected_routes() -> Router + where + crate::state::AiState: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, + { + Router::new() + .route("/ai/analyze/lab-report", axum::routing::post(handler::stream_lab_report::)) + .route("/ai/analyze/trends", axum::routing::post(handler::stream_trends::)) + .route("/ai/analyze/checkup-plan", axum::routing::post(handler::stream_checkup_plan::)) + .route("/ai/analyze/report-summary", axum::routing::post(handler::stream_report_summary::)) + .route("/ai/analysis/history", axum::routing::get(handler::list_analysis::)) + .route("/ai/analysis/{id}", axum::routing::get(handler::get_analysis::)) + } +} +``` + +- [ ] **Step 3: 更新 lib.rs 添加 re-export** + +```rust +pub use module::AiModule; +pub use state::AiState; +``` + +以及 `pub mod module; pub mod state;` + +- [ ] **Step 4: 验证编译 (handler 还没写,先创建 stub)** + +创建 `crates/erp-ai/src/handler/mod.rs`: + +```rust +// crates/erp-ai/src/handler/mod.rs — stub,Chunk 6 完善 +use axum::response::IntoResponse; + +pub async fn stream_lab_report() -> impl IntoResponse { "stub" } +pub async fn stream_trends() -> impl IntoResponse { "stub" } +pub async fn stream_checkup_plan() -> impl IntoResponse { "stub" } +pub async fn stream_report_summary() -> impl IntoResponse { "stub" } +pub async fn list_analysis() -> impl IntoResponse { "stub" } +pub async fn get_analysis() -> impl IntoResponse { "stub" } +``` + +```bash +cargo check -p erp-ai +``` + +- [ ] **Step 5: 提交** + +```bash +git add crates/erp-ai/src/ +git commit -m "feat(ai): AiState + AiModule (ErpModule impl + 权限 + 路由骨架)" +``` + +--- + +### Task 10: erp-server 集成 — Config + State + 路由注册 + +**Files:** +- Modify: `crates/erp-server/src/config.rs` — 添加 AiConfig +- Modify: `crates/erp-server/src/state.rs` — 添加 FromRef +- Modify: `crates/erp-server/src/main.rs` — 注册模块 + 合并路由 +- Modify: `crates/erp-server/Cargo.toml` — 添加 erp-ai 依赖 +- Modify: `crates/erp-server/config/default.toml` — 添加 [ai] 段 + +- [ ] **Step 1: erp-server/Cargo.toml 添加 erp-ai** + +```toml +erp-ai.workspace = true +``` + +- [ ] **Step 2: config.rs 添加 AiConfig** + +```rust +// 在 AppConfig 结构体中添加: +pub ai: AiConfig, + +// 新增结构体: +#[derive(Debug, Clone, Deserialize)] +pub struct AiConfig { + pub default_provider: String, + pub api_key: String, + pub base_url: Option, + pub model: String, + pub max_tokens: u32, + pub temperature: f32, + pub cache_ttl_seconds: u64, + pub rate_limit_patient_daily: u32, +} +``` + +- [ ] **Step 3: config/default.toml 添加 [ai] 段** + +```toml +[ai] +default_provider = "claude" +api_key = "" +base_url = "https://api.anthropic.com" +model = "claude-sonnet-4-6" +max_tokens = 2048 +temperature = 0.3 +cache_ttl_seconds = 604800 +rate_limit_patient_daily = 10 +``` + +- [ ] **Step 4: state.rs 添加 FromRef** + +```rust +impl FromRef for erp_ai::AiState { + fn from_ref(state: &AppState) -> Self { + // 从 config 构建 ClaudeProvider + let provider = erp_ai::provider::claude::ClaudeProvider::new( + state.config.ai.api_key.clone(), + ); + let db = state.db.clone(); + let event_bus = state.event_bus.clone(); + + let analysis = std::sync::Arc::new( + erp_ai::service::analysis::AnalysisService::new( + Box::new(provider), db.clone(), + ) + ); + let prompt = std::sync::Arc::new( + erp_ai::service::prompt::PromptService::new(db.clone()) + ); + let usage = std::sync::Arc::new( + erp_ai::service::usage::UsageService::new(db.clone()) + ); + + Self { db, event_bus, analysis, prompt, usage } + } +} +``` + +- [ ] **Step 5: main.rs 注册模块 + 路由** + +```rust +// 1. 创建 AiModule +let ai_module = erp_ai::AiModule; + +// 2. 注册到 registry +let registry = ModuleRegistry::new() + .register(auth_module) + // ...existing... + .register(ai_module); + +// 3. 合并路由 (protected_routes 中) +.merge(erp_ai::AiModule::protected_routes()) +``` + +- [ ] **Step 6: 验证全 workspace 编译** + +```bash +cargo check --workspace +``` + +- [ ] **Step 7: 提交** + +```bash +git add crates/erp-server/ +git commit -m "feat(server): erp-ai 模块集成 — Config/State/路由注册" +``` + +---