From e149a61ce6dda999faf59601861e47ff41fb1b08 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 18 May 2026 02:14:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20error=20=E7=B1=BB=E5=9E=8B=20+=20a?= =?UTF-8?q?uth=5Fservice=20=E5=B0=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/erp-auth/src/error.rs | 4 + crates/erp-auth/src/service/auth_service.rs | 10 + .../2026-05-18-ai-agent-breakthrough-plan.md | 663 ++++++++++++++++++ 4 files changed, 678 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8841f27..bb83c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,6 +1453,7 @@ dependencies = [ "base64 0.22.1", "cbc", "chrono", + "dashmap", "erp-core", "hex", "jsonwebtoken", diff --git a/crates/erp-auth/src/error.rs b/crates/erp-auth/src/error.rs index 1a7f63b..f8be07b 100644 --- a/crates/erp-auth/src/error.rs +++ b/crates/erp-auth/src/error.rs @@ -27,6 +27,9 @@ pub enum AuthError { #[error("{0}")] Validation(String), + #[error("{0}")] + Forbidden(String), + #[error("版本冲突: 数据已被其他操作修改,请刷新后重试")] VersionMismatch, } @@ -39,6 +42,7 @@ impl From for AppError { AuthError::TokenRevoked => AppError::Unauthorized, AuthError::UserDisabled(s) => AppError::Forbidden(s), AuthError::Validation(s) => AppError::Validation(s), + AuthError::Forbidden(s) => AppError::Forbidden(s), AuthError::DbError(_) => AppError::Internal(err.to_string()), AuthError::HashError(_) => AppError::Internal(err.to_string()), AuthError::JwtError(_) => AppError::Unauthorized, diff --git a/crates/erp-auth/src/service/auth_service.rs b/crates/erp-auth/src/service/auth_service.rs index 972ab5a..9728eff 100644 --- a/crates/erp-auth/src/service/auth_service.rs +++ b/crates/erp-auth/src/service/auth_service.rs @@ -113,6 +113,16 @@ impl AuthService { // 5. Get roles and permissions let roles: Vec = TokenService::get_user_roles(user_model.id, tenant_id, db).await?; + + // 纯患者角色不允许登录管理端(同时拥有医护角色则放行) + let medical_roles = ["doctor", "nurse", "admin", "health_manager", "operator"]; + let is_pure_patient = + roles.iter().all(|r| r == "patient") && roles.iter().any(|r| r == "patient"); + let has_medical_role = roles.iter().any(|r| medical_roles.contains(&r.as_str())); + if is_pure_patient && !has_medical_role { + return Err(AuthError::Forbidden("患者账号请使用小程序登录".to_string())); + } + let permissions = TokenService::get_user_permissions(user_model.id, tenant_id, db).await?; // 6. Sign tokens diff --git a/docs/superpowers/plans/2026-05-18-ai-agent-breakthrough-plan.md b/docs/superpowers/plans/2026-05-18-ai-agent-breakthrough-plan.md index 4958668..34ec6e1 100644 --- a/docs/superpowers/plans/2026-05-18-ai-agent-breakthrough-plan.md +++ b/docs/superpowers/plans/2026-05-18-ai-agent-breakthrough-plan.md @@ -987,3 +987,666 @@ git commit -m "test(ai): Phase 0 集成测试 — Agent 循环 + Tool 执行 + - [ ] 代码已提交并推送 --- + +## Chunk 2: Phase 1 — Tool 扩展 + 策略 Prompt(5-7 天) + +> 目标:覆盖全部核心 Tool,多策略对话流生效 + +### Task 1.1: 数据查询类 Tool — query_lab_reports + query_patient_profile + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/query_lab_reports.rs` +- Create: `crates/erp-ai/src/agent/tools/query_patient_profile.rs` +- Modify: `crates/erp-ai/src/agent/tools/mod.rs` (注册新模块) +- Modify: `crates/erp-ai/src/module.rs` (注册新 Tool 到 ToolRegistry) + +- [ ] **Step 1: 实现 query_lab_reports Tool** + +调用 `ctx.health_provider.get_lab_report()`,参数为 `report_id`。输出格式化的化验指标列表,标注异常项。 + +- [ ] **Step 2: 实现 query_patient_profile Tool** + +调用 `ctx.health_provider.get_patient_summary()`,返回脱敏后的患者摘要(年龄组、性别、慢性病、用药、家族史)。 + +- [ ] **Step 3: 注册到 ToolRegistry + cargo check** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs +git commit -m "feat(ai): 添加 query_lab_reports + query_patient_profile Tool" +``` + +--- + +### Task 1.2: 数据查询类 Tool — query_appointments + query_medication + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/query_appointments.rs` +- Create: `crates/erp-ai/src/agent/tools/query_medication.rs` + +- [ ] **Step 1: 实现 query_appointments Tool** + +调用 `ctx.health_provider.get_upcoming_appointments()`(Task 0.6 新增的方法),返回患者即将到来的预约列表。 + +- [ ] **Step 2: 实现 query_medication Tool** + +调用 `ctx.health_provider.get_medication_list()`(Task 0.6 新增的方法),返回患者当前用药列表。 + +- [ ] **Step 3: 注册到 ToolRegistry + cargo check + cargo test -p erp-ai** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs +git commit -m "feat(ai): 添加 query_appointments + query_medication Tool" +``` + +--- + +### Task 1.3: AI 分析类 Tool — analyze_lab_report + analyze_health_trends + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/analyze_lab_report.rs` +- Create: `crates/erp-ai/src/agent/tools/analyze_health_trends.rs` + +- [ ] **Step 1: 实现 analyze_lab_report Tool** + +调用 `AiState.analysis`(AnalysisService)的非流式分析方法。参数:`report_id`。返回化验报告的 AI 解读摘要。 + +注意:现有 `analysis_service` 使用 SSE 流式输出,Tool 内需要走同步路径。检查 `analysis.rs` 是否有 `analyze_sync()` 方法,如果没有需要添加。 + +- [ ] **Step 2: 实现 analyze_health_trends Tool** + +调用 `ctx.health_provider.get_trend_analysis_data()` 获取预计算的统计数据,再用 `AnalysisService` 做趋势解读。 + +- [ ] **Step 3: 注册到 ToolRegistry + cargo check** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs +git commit -m "feat(ai): 添加 analyze_lab_report + analyze_health_trends 分析类 Tool" +``` + +--- + +### Task 1.4: AI 分析类 Tool — get_health_insights + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/get_health_insights.rs` + +- [ ] **Step 1: 实现 get_health_insights Tool** + +调用 `AiState.insight_service`(InsightService)+ `AiState.risk_service`(RiskService),获取患者的风险洞察和 AI 建议。参数:`patient_id`(默认使用当前患者)。 + +- [ ] **Step 2: 注册到 ToolRegistry + cargo check** + +- [ ] **Step 3: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs +git commit -m "feat(ai): 添加 get_health_insights Tool — Copilot 风险洞察接入" +``` + +--- + +### Task 1.5: 知识类 Tool — search_medical_knowledge + recommend_services + check_alert_rules + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/search_medical_knowledge.rs` +- Create: `crates/erp-ai/src/agent/tools/recommend_services.rs` +- Create: `crates/erp-ai/src/agent/tools/check_alert_rules.rs` + +- [ ] **Step 1: 实现 search_medical_knowledge Tool** + +调用 `AiState` 中的 `knowledge_structured_source`,按关键词搜索医疗知识库(KDIGO 规则、科室指南、科普文章)。参数:`query`(搜索关键词)、`category`(可选分类过滤)。 + +- [ ] **Step 2: 实现 recommend_services Tool** + +基于规则 + 知识库推荐科室或服务。Phase 1 用简化规则映射(如"头晕"→"神经内科/心内科","血压高"→"心内科")。参数:`symptoms`(症状列表)。 + +- [ ] **Step 3: 实现 check_alert_rules Tool** + +调用 `AiState` 中的 `local_rules_engine`,评估当前患者数据是否触发告警阈值。参数:`patient_id`。 + +- [ ] **Step 4: 注册全部 3 个 Tool 到 ToolRegistry + cargo check** + +- [ ] **Step 5: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs +git commit -m "feat(ai): 添加知识类 Tool — medical_knowledge + recommend_services + check_alert_rules" +``` + +--- + +### Task 1.6: 多策略 System Prompt 设计 + 调优 + +**Files:** +- Modify: `crates/erp-ai/src/agent/prompt.rs` (新建或修改现有 prompt 模块) + +- [ ] **Step 1: 实现 build_system_prompt 函数** + +从 Spec §4.2 的 System Prompt 模板生成完整 prompt。函数签名: + +```rust +pub fn build_agent_system_prompt( + user_profile: Option<&UserProfileSummary>, + patient_profile: Option<&PatientSummaryDto>, +) -> String +``` + +动态注入: +- 用户画像偏好(如有长期记忆) +- 患者基本信息(如已关联患者) +- 可用 Tool 列表描述 + +- [ ] **Step 2: 更新 chat_handler 使用新 prompt** + +替换 Phase 0 中的硬编码 prompt 为 `build_agent_system_prompt()` 调用。 + +- [ ] **Step 3: cargo check + 手动对话调优** + +启动后端,用不同场景测试 Agent 策略选择是否正确: +- "我最近血压有点高"(应触发查询 → 分析 → 预警 → 推荐流程) +- "糖尿病有什么并发症"(应触发知识搜索 → 科普) +- "我很担心我的检查结果"(应先安抚 → 再查数据) + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/prompt.rs crates/erp-ai/src/handler/chat_handler.rs +git commit -m "feat(ai): 多策略 System Prompt — 安抚/科普/推荐/预警/引导到院" +``` + +--- + +### Task 1.7: 配额检查 + Token 计量 + +**Files:** +- Modify: `crates/erp-ai/src/agent/orchestrator.rs` (添加配额检查) +- Modify: `crates/erp-ai/src/handler/chat_handler.rs` (记录总 token 消耗) + +- [ ] **Step 1: 在 Orchestrator 每轮 Tool Call 前添加配额检查** + +在 `run()` 循环的 `generate_with_tools()` 调用前,检查 `QuotaService`。配额不足时直接返回提示而非调用 LLM。 + +注意:QuotaService 在 `AiState.quota` 中,需要在 ToolContext 或 Orchestrator 构造时传入。 + +- [ ] **Step 2: 在 chat_handler 中记录每轮 token 消耗到 usage_service** + +- [ ] **Step 3: cargo check + cargo test -p erp-ai** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/orchestrator.rs crates/erp-ai/src/handler/chat_handler.rs +git commit -m "feat(ai): Agent 配额检查 + Token 计量" +``` + +--- + +### Task 1.8: Phase 1 测试覆盖 + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/query_lab_reports_test.rs` (及其他 Tool 的单元测试) +- Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` (扩展集成测试) + +- [ ] **Step 1: 每个 Tool 编写单元测试** + +测试模式:mock `HealthDataProvider`(用 `MockHealthDataProvider`),验证 Tool 的参数解析、输出格式、错误处理。 + +- [ ] **Step 2: 扩展集成测试** + +新增场景: +- 发送"我最近化验报告有什么问题" → Agent 调用 query_lab_reports + analyze_lab_report +- 发送"帮我推荐个科室" → Agent 调用 recommend_services +- 配额耗尽 → Agent 返回降级提示 + +- [ ] **Step 3: cargo test --workspace** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/ crates/erp-server/tests/integration/ai_agent_test.rs +git commit -m "test(ai): Phase 1 测试覆盖 — Tool 单元测试 + 5 策略集成测试" +``` + +--- + +### Phase 1 完成标准 + +- [ ] `cargo check` + `cargo test --workspace` 全部通过 +- [ ] 模拟 5 种典型场景(安抚/科普/推荐/预警/引导到院),Agent 自主选择正确策略和 Tool +- [ ] 配额检查和 Token 计量正常工作 +- [ ] 代码已提交并推送 + +--- + +## Chunk 3: Phase 2 — 前端升级 + 流式输出(7-9 天) + +> 目标:小程序 + Web 都有完整 AI 客服体验 + +### Task 2.1: 后端 — 会话 CRUD API + +**Files:** +- Create: `crates/erp-ai/src/handler/chat_session_handler.rs` (Session CRUD) +- Create: `crates/erp-ai/src/entity/ai_chat_session.rs` (SeaORM Entity) +- Create: `crates/erp-ai/src/entity/ai_chat_message.rs` (SeaORM Entity) +- Create: `crates/erp-ai/src/service/chat_session_service.rs` (Service 层) +- Modify: `crates/erp-ai/src/module.rs` (注册新路由) +- Modify: `crates/erp-ai/src/entity/mod.rs` (导出新 Entity) +- Modify: `crates/erp-ai/src/handler/mod.rs` (导出新 handler) + +- [ ] **Step 1: 创建 SeaORM Entity** + +为 `ai_chat_sessions` 和 `ai_chat_messages` 表创建 Entity 文件。参考现有 Entity 格式(如 `entity/ai_analysis.rs`),包含所有标准字段。 + +- [ ] **Step 2: 实现 ChatSessionService** + +CRUD 方法: +- `create_session(tenant_id, user_id, patient_id?)` → 创建会话 +- `list_sessions(tenant_id, user_id)` → 列出用户会话 +- `get_session(session_id, tenant_id)` → 获取会话详情 +- `close_session(session_id, tenant_id)` → 软关闭 +- `save_message(session_id, role, content, tool_calls?, tool_call_id?)` → 保存消息 +- `list_messages(session_id, tenant_id, limit, offset)` → 分页获取消息 + +- [ ] **Step 3: 实现 Session Handler** + +4 个端点:`POST /sessions`、`GET /sessions`、`DELETE /sessions/{id}`、`GET /sessions/{id}/messages` + +每个端点添加对应权限守卫(`ai.chat.session.manage`/`.list`/`.history`)。 + +- [ ] **Step 4: 改造 chat_handler 使用会话模式** + +`POST /sessions/{id}/messages` 替代原有 `POST /ai/chat`。从 DB 加载会话历史,不再依赖前端传 history。 + +- [ ] **Step 5: 在 module.rs 注册新路由** + +```rust +// 新增会话管理路由 +let session_routes = Router::new() + .route("/", post(session_handler::create_session)) + .route("/", get(session_handler::list_sessions)) + .route("/{session_id}", delete(session_handler::close_session)) + .route("/{session_id}/messages", get(session_handler::list_messages)) + .route("/{session_id}/messages", post(chat_handler::send_message)); +``` + +保留原 `POST /ai/chat` 兼容一段时间。 + +- [ ] **Step 6: cargo check + cargo test -p erp-ai** + +- [ ] **Step 7: Commit** + +```bash +git add crates/erp-ai/src/handler/chat_session_handler.rs crates/erp-ai/src/entity/ai_chat_session.rs crates/erp-ai/src/entity/ai_chat_message.rs crates/erp-ai/src/service/chat_session_service.rs crates/erp-ai/src/module.rs +git commit -m "feat(ai): 会话 CRUD API — sessions/messages 端点 + DB 持久化" +``` + +--- + +### Task 2.2: 后端 — Agent 回复 SSE 流式输出 + +**Files:** +- Modify: `crates/erp-ai/src/handler/chat_handler.rs` (send_message 支持 SSE) + +- [ ] **Step 1: send_message 端点支持 SSE** + +当客户端请求 `Accept: text/event-stream` 时: +1. Agent Orchestrator 的 Tool Call 过程在后台执行(不在 SSE 中传输) +2. 最终回复生成后,通过 SSE 流式推送给客户端 +3. 复用现有 SSE 架构(`Sse` 模式,参考 analysis_handler) + +当客户端请求 `Accept: application/json` 时,走原有同步模式。 + +- [ ] **Step 2: cargo check + Postman 测试 SSE** + +用 Postman 发送带 `Accept: text/event-stream` 头的请求,验证流式输出。 + +- [ ] **Step 3: Commit** + +```bash +git add crates/erp-ai/src/handler/chat_handler.rs +git commit -m "feat(ai): Agent 回复 SSE 流式输出 — Tool 过程后台执行" +``` + +--- + +### Task 2.3: 小程序 — SSE 兼容层 + 会话列表页 + +**Files:** +- Rewrite: `apps/miniprogram/src/services/ai-chat.ts` (从本地 Storage 迁移到 API) +- Create: `apps/miniprogram/src/pages/ai-chat/sessions/index.tsx` (会话列表页) +- Create: `apps/miniprogram/src/pages/ai-chat/sessions/index.scss` +- Modify: `apps/miniprogram/src/pages/messages/index.tsx` (改造为使用会话 API) +- Modify: `apps/miniprogram/src/pages/messages/index.scss` +- Modify: `apps/miniprogram/src/app.config.ts` (注册新页面) + +- [ ] **Step 1: 重写 ai-chat.ts 服务层** + +```typescript +// 新 API +export async function createSession(patientId?: string): Promise +export async function listSessions(): Promise +export async function sendMessage(sessionId: string, message: string): Promise +export async function getMessageHistory(sessionId: string): Promise + +// SSE 支持 +export function sendMessageStream(sessionId: string, message: string): Promise +``` + +Taro 不支持原生 SSE,使用轮询或 `requestTask` 长连接实现。 + +- [ ] **Step 2: 改造 messages 页面** + +将现有 `messages/index.tsx` 改造为使用新 API: +- 页面加载时创建或恢复会话 +- 发送消息调用 `sendMessage(sessionId, text)` +- 消息列表从 `getMessageHistory()` 获取而非本地 Storage +- 支持富消息渲染(基于 `display_hint` 字段) + +- [ ] **Step 3: 创建会话列表页** + +新页面 `ai-chat/sessions/`:展示历史会话列表,点击进入对话。添加到 `app.config.ts` 的 subPackages 中。 + +- [ ] **Step 4: 旧数据迁移** + +首次打开新版本时,检测本地 `ai_chat_history`,如有数据则提示"历史记录已迁移到云端"并清除本地缓存。 + +- [ ] **Step 5: 编译 + 真机预览** + +Run: `cd apps/miniprogram && pnpm build` +在微信开发者工具中验证页面渲染和消息收发。 + +- [ ] **Step 6: Commit** + +```bash +git add apps/miniprogram/src/services/ai-chat.ts apps/miniprogram/src/pages/messages/ apps/miniprogram/src/pages/ai-chat/ apps/miniprogram/src/app.config.ts +git commit -m "feat(mp): AI 客服升级 — 会话 API + SSE 兼容 + 会话列表页" +``` + +--- + +### Task 2.4: Web — AI 客服页面从零构建 + +**Files:** +- Create: `apps/web/src/services/ai-chat.ts` (API 模块) +- Create: `apps/web/src/pages/ai/ChatPage.tsx` (聊天主页面) +- Create: `apps/web/src/pages/ai/ChatPage.scss` +- Create: `apps/web/src/components/ai/MessageBubble.tsx` (消息气泡组件) +- Create: `apps/web/src/components/ai/RichMessageCard.tsx` (富消息卡片) +- Modify: `apps/web/src/router/routeConfig.ts` (注册新路由) + +- [ ] **Step 1: 创建 Web 端 ai-chat.ts API 模块** + +与小程序相同的 API 接口,但使用 `fetch` + `EventSource` 实现 SSE。 + +- [ ] **Step 2: 创建 ChatPage** + +参考小程序 `messages/index.tsx` 的功能,使用 Ant Design 组件构建: +- 左侧会话列表 +- 右侧聊天区域 +- 底部输入框 +- 消息气泡区分 user/assistant + +- [ ] **Step 3: 实现富消息渲染** + +基于 `display_hint` 字段,渲染不同类型的富消息: +- `VitalCard` → ECharts 小图表 +- `LabReportCard` → 指标异常高亮 +- `ActionConfirm` → 确认/取消按钮 +- `RiskAlert` → 彩色风险等级卡片 + +- [ ] **Step 4: 注册路由** + +在 `routeConfig.ts` 中添加 `/ai/chat` 路由,权限码 `ai.chat.session.list`。 + +- [ ] **Step 5: 编译 + 浏览器验证** + +Run: `cd apps/web && pnpm build` +在浏览器中打开 `/ai/chat`,验证页面功能和 SSE 流式输出。 + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/services/ai-chat.ts apps/web/src/pages/ai/ apps/web/src/components/ai/ apps/web/src/router/routeConfig.ts +git commit -m "feat(web): AI 客服页面 — 会话列表 + 聊天界面 + 富消息渲染 + SSE" +``` + +--- + +### Task 2.5: 端到端测试 + +**Files:** +- Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` (扩展) +- Create: `apps/miniprogram/src/__tests__/ai-chat.test.ts` (小程序服务层测试) + +- [ ] **Step 1: 后端集成测试扩展** + +新增测试: +- Session CRUD 全流程 +- 发送消息 → DB 持久化验证 +- SSE 流式输出格式验证 +- 权限守卫验证(无权限返回 403) + +- [ ] **Step 2: 小程序服务层测试** + +测试 ai-chat.ts 的 API 调用逻辑(mock fetch)。 + +- [ ] **Step 3: cargo test --workspace + pnpm build** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-server/tests/integration/ai_agent_test.rs apps/miniprogram/src/__tests__/ +git commit -m "test(ai): Phase 2 端到端测试 — Session CRUD + SSE + 权限验证" +``` + +--- + +### Phase 2 完成标准 + +- [ ] `cargo check` + `cargo test --workspace` 全部通过 +- [ ] `pnpm build`(小程序 + Web)通过 +- [ ] 小程序打开 AI 客服,能自然对话,能看到数据卡片 +- [ ] Web 端打开 AI 客服,聊天界面正常,SSE 流式输出正常 +- [ ] 会话历史持久化到 DB,不再依赖本地 Storage +- [ ] 代码已提交并推送 + +--- + +## Chunk 4: Phase 3 — 行动类 Tool + 人机协作(3-5 天) + +> 目标:AI 客服能帮用户预约、转接人工 + +### Task 3.1: create_appointment Tool(带二次确认) + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/create_appointment.rs` +- Modify: `crates/erp-ai/src/agent/tools/mod.rs` + +- [ ] **Step 1: 实现 create_appointment Tool** + +参数:`department`(科室)、`preferred_date`(偏好日期)、`preferred_time`(偏好时段)。 + +逻辑: +1. 查询可用排班(调用 `HealthDataProvider`,需新增 `get_available_slots` 方法) +2. 推荐最近可用时段 +3. **不直接创建**,返回 `DisplayHint::ActionConfirm`,前端展示确认卡片 +4. 用户确认后,前端发送确认请求到独立端点 `POST /api/v1/ai/chat/sessions/{id}/confirm-action` + +注意:二次确认机制确保 LLM 不能在用户不知情的情况下创建预约。 + +- [ ] **Step 2: 新增确认端点** + +`POST /sessions/{id}/confirm-action`:接收 `action_type` + `confirm_payload`,调用 `appointment_service.create_appointment()`。 + +- [ ] **Step 3: cargo check + cargo test** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/create_appointment.rs crates/erp-ai/src/handler/ +git commit -m "feat(ai): create_appointment Tool — 二次确认机制 + 预约创建" +``` + +--- + +### Task 3.2: transfer_to_human Tool + WebSocket 通知 + +**Files:** +- Create: `crates/erp-ai/src/agent/tools/transfer_to_human.rs` +- Create: `crates/erp-ai/src/handler/chat_transfer_handler.rs` (WebSocket 通知) + +- [ ] **Step 1: 实现 transfer_to_human Tool** + +参数:`reason`(转接原因)、`urgency`(紧急程度 low/medium/high)。 + +逻辑: +1. 记录转接请求到会话 metadata +2. 通过事件总线发布 `ai.chat.transferred` 事件 +3. 返回 `DisplayHint::RiskAlert` 提示用户"正在转接" +4. 值班医护端收到通知(通过 WebSocket 或 polling) + +- [ ] **Step 2: WebSocket 通知值班医护** + +在 `chat_transfer_handler.rs` 中实现 WebSocket 端点: +- `WS /api/v1/ai/chat/notifications` — 值班医护连接此端点接收转接通知 +- 收到转接请求时推送 JSON 消息(包含 session_id、患者信息、转接原因) + +注意:WebSocket 基础设施需检查 Axum 是否已有 WebSocket 支持(项目依赖中已有 tokio-tungstenite)。 + +- [ ] **Step 3: cargo check + cargo test** + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-ai/src/agent/tools/transfer_to_human.rs crates/erp-ai/src/handler/chat_transfer_handler.rs +git commit -m "feat(ai): transfer_to_human Tool + WebSocket 通知值班医护" +``` + +--- + +### Task 3.3: 前端 — 操作确认 UI + 转接状态 + +**Files:** +- Modify: `apps/miniprogram/src/pages/messages/index.tsx` (确认卡片 + 转接状态) +- Modify: `apps/web/src/pages/ai/ChatPage.tsx` (同上) +- Modify: `apps/web/src/components/ai/RichMessageCard.tsx` (ActionConfirm + RiskAlert 渲染) + +- [ ] **Step 1: 小程序实现操作确认卡片** + +当消息包含 `display_hint.type === 'action_confirm'` 时,渲染确认按钮。用户点击后调用 `POST /sessions/{id}/confirm-action`。 + +- [ ] **Step 2: 小程序实现转接状态提示** + +当 `display_hint.type === 'risk_alert'` 且包含转接信息时,显示"正在转接值班医生"动画。 + +- [ ] **Step 3: Web 端同步实现** + +同样的确认卡片和转接状态提示。 + +- [ ] **Step 4: 编译 + 验证** + +Run: `pnpm build` (小程序 + Web) + +- [ ] **Step 5: Commit** + +```bash +git add apps/miniprogram/src/pages/messages/ apps/web/src/pages/ai/ apps/web/src/components/ai/ +git commit -m "feat: 操作确认 UI + 转接状态提示 — 小程序 + Web" +``` + +--- + +### Task 3.4: 安全边界加固 + +**Files:** +- Modify: `crates/erp-ai/src/agent/orchestrator.rs` (行动类 Tool 标记) +- Modify: `crates/erp-ai/src/agent/tool.rs` (ToolCategory 枚举) +- Modify: `crates/erp-ai/src/handler/chat_handler.rs` (审计日志增强) + +- [ ] **Step 1: 添加 ToolCategory 枚举** + +```rust +pub enum ToolCategory { + ReadOnly, // 数据查询 + Analysis, // AI 分析 + Knowledge, // 知识检索 + Action, // 写入操作(需更高权限) +} +``` + +在 `AgentTool` trait 中添加 `fn category(&self) -> ToolCategory`。 + +- [ ] **Step 2: Orchestrator 对 Action 类 Tool 额外检查** + +Action Tool 只能在用户明确意图时调用。Orchestrator 记录 Action Tool 的调用,chat_handler 写入审计日志。 + +- [ ] **Step 3: 审计日志增强** + +`ai_tool_call_logs` 表记录完整的 Tool 调用参数(已脱敏)和结果摘要,用于事后审计。 + +- [ ] **Step 4: cargo check + cargo test --workspace** + +- [ ] **Step 5: Commit** + +```bash +git add crates/erp-ai/src/agent/ crates/erp-ai/src/handler/ +git commit -m "feat(ai): 安全边界加固 — ToolCategory + Action 权限标记 + 审计日志" +``` + +--- + +### Task 3.5: 端到端验证 + +**Files:** +- Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` + +- [ ] **Step 1: 端到端测试** + +完整流程测试: +1. 用户说"帮我预约个号" → Agent 调用 query_appointments 查空档 → recommend_services 推荐科室 → create_appointment 返回确认卡片 → 用户确认 → 预约创建成功 +2. 用户说"我要找医生" → Agent 调用 transfer_to_human → WebSocket 通知发送 → 用户看到转接提示 +3. 用户说"帮我取消预约" → Agent 提示"暂不支持取消,请联系前台" + +- [ ] **Step 2: 浏览器手动验证** + +启动后端 + Web 前端,在浏览器中走完整预约流程。 + +- [ ] **Step 3: 小程序真机验证** + +在微信开发者工具中测试预约确认和转接流程。 + +- [ ] **Step 4: Commit** + +```bash +git add crates/erp-server/tests/integration/ai_agent_test.rs +git commit -m "test(ai): Phase 3 端到端测试 — 预约创建 + 转接人工 + 完整对话流" +``` + +--- + +### Phase 3 完成标准 + +- [ ] `cargo check` + `cargo test --workspace` 全部通过 +- [ ] `pnpm build`(小程序 + Web)通过 +- [ ] 用户对 AI 说"帮我预约个号",全流程跑通 +- [ ] 用户说"找医生",转接通知正常发送 +- [ ] 行动类 Tool 有权限标记和审计日志 +- [ ] 代码已提交并推送 + +--- + +## 全局完成标准 + +- [ ] `cargo check` + `cargo test --workspace` + `pnpm build` 全部通过 +- [ ] AI 客服能自然处理 5 种策略场景(安抚/科普/推荐/预警/引导) +- [ ] 小程序 + Web 两端 AI 客服功能完整 +- [ ] 所有 Tool 有单元测试,核心流程有集成测试 +- [ ] wiki 关键数字已更新(新增迁移数/实体数/路由数) +- [ ] 所有代码已提交并推送 +