From 9223ceca50e6399770826ca0b4fc20ccffc2563e Mon Sep 17 00:00:00 2001 From: ShaoHua <345265198@qqcom> Date: Tue, 16 Jun 2026 01:15:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E6=96=B0=E5=A2=9E=20MCP=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=8C?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=A7=84=E5=88=99=E6=96=87=E4=BB=B6=E5=BA=8F?= =?UTF-8?q?=E5=8F=B7=EF=BC=8C=E6=96=B0=E5=A2=9E=20v1.3.0=20=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 规则重组:全局/ 下 8 个规则合并为 6 个(01+02→01,05+06→04),序号顺延 - 新增项目规则 05-多入口功能同步规范(UI/语音入口覆盖检查) - 新增 MCP 服务基础设施:Mcp/ 目录(DI 注册、端点扩展、动态工具描述符)、单元测试 - v1.3.0 工单文档:03 系列(会议任务拆分)、04(富文本描述与附件管理) - MCP 接口与前端集成指南:docs/manual/08、09 --- ...{01-智能体记忆.md => 01-记忆与存储规范.md} | 49 +- .../全局/{03-注释规范.md => 02-注释规范.md} | 0 .trae/rules/全局/02-记忆存储规范.md | 33 -- ...{04-文档同步规范.md => 03-文档同步规范.md} | 2 +- .trae/rules/全局/04-研发工单全流程规范.md | 141 +++++ ...窗口冲突规约.md => 05-并行窗口冲突规约.md} | 2 +- .trae/rules/全局/05-研发工单规则.md | 129 ---- ...AI沟通记录规范.md => 06-AI沟通记录规范.md} | 0 .trae/rules/全局/06-研发工单拆分规范.md | 57 -- .trae/rules/项目/02-业务命名规范.md | 2 +- .trae/rules/项目/04-即时状态记忆.md | 24 +- .trae/rules/项目/05-多入口功能同步规范.md | 85 +++ .trae/索引.md | 23 +- docs/manual/08-MCP服务接口文档.md | 389 ++++++++++++ docs/manual/09-MCP前端集成指南.md | 210 +++++++ docs/project/研发工单-v1.3.0/00-工单总览.md | 139 ++++- .../研发工单-v1.3.0/02-语音通话与语音控制.md | 473 ++++++++++++--- .../03-01-会议数据模型与API.md | 241 ++++++++ .../研发工单-v1.3.0/03-02-音频录制与转写.md | 199 +++++++ .../研发工单-v1.3.0/03-03-AI任务拆分服务.md | 281 +++++++++ .../研发工单-v1.3.0/03-04-任务建议与确认UI.md | 295 ++++++++++ .../研发工单-v1.3.0/03-会议任务拆分.md | 341 +++++++++++ .../04-富文本描述与附件管理.md | 554 ++++++++++++++++++ .../Hua.Todo.Application.csproj | 11 + .../Mcp/DynamicMcpToolExtensions.cs | 169 ++++++ .../Mcp/McpEndpointExtensions.cs | 24 + .../Mcp/McpServiceCollectionExtensions.cs | 41 ++ src/Hua.Todo.Host/Program.cs | 7 + .../DynamicMcpToolExtensionsTests.cs | 324 ++++++++++ src/Hua.Todo.Tests/Hua.Todo.Tests.csproj | 8 + 30 files changed, 3907 insertions(+), 346 deletions(-) rename .trae/rules/全局/{01-智能体记忆.md => 01-记忆与存储规范.md} (57%) rename .trae/rules/全局/{03-注释规范.md => 02-注释规范.md} (100%) delete mode 100644 .trae/rules/全局/02-记忆存储规范.md rename .trae/rules/全局/{04-文档同步规范.md => 03-文档同步规范.md} (96%) create mode 100644 .trae/rules/全局/04-研发工单全流程规范.md rename .trae/rules/全局/{07-并行窗口冲突规约.md => 05-并行窗口冲突规约.md} (98%) delete mode 100644 .trae/rules/全局/05-研发工单规则.md rename .trae/rules/全局/{08-AI沟通记录规范.md => 06-AI沟通记录规范.md} (100%) delete mode 100644 .trae/rules/全局/06-研发工单拆分规范.md create mode 100644 .trae/rules/项目/05-多入口功能同步规范.md create mode 100644 docs/manual/08-MCP服务接口文档.md create mode 100644 docs/manual/09-MCP前端集成指南.md create mode 100644 docs/project/研发工单-v1.3.0/03-01-会议数据模型与API.md create mode 100644 docs/project/研发工单-v1.3.0/03-02-音频录制与转写.md create mode 100644 docs/project/研发工单-v1.3.0/03-03-AI任务拆分服务.md create mode 100644 docs/project/研发工单-v1.3.0/03-04-任务建议与确认UI.md create mode 100644 docs/project/研发工单-v1.3.0/03-会议任务拆分.md create mode 100644 docs/project/研发工单-v1.3.0/04-富文本描述与附件管理.md create mode 100644 src/Hua.Todo.Application/Mcp/DynamicMcpToolExtensions.cs create mode 100644 src/Hua.Todo.Application/Mcp/McpEndpointExtensions.cs create mode 100644 src/Hua.Todo.Application/Mcp/McpServiceCollectionExtensions.cs create mode 100644 src/Hua.Todo.Tests/DynamicMcpToolExtensionsTests.cs diff --git a/.trae/rules/全局/01-智能体记忆.md b/.trae/rules/全局/01-记忆与存储规范.md similarity index 57% rename from .trae/rules/全局/01-智能体记忆.md rename to .trae/rules/全局/01-记忆与存储规范.md index 58f7708..9800935 100644 --- a/.trae/rules/全局/01-智能体记忆.md +++ b/.trae/rules/全局/01-记忆与存储规范.md @@ -1,17 +1,46 @@ -# 智能体记忆与规范同步规则(必须遵守) +# 智能体记忆与存储规范(必须遵守) -> 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆与同步规范,与具体项目业务无关。 +> 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆、存储与同步规范,与具体项目业务无关。 > > `.trae/` 整体目录结构与各子目录职责见 [.trae/索引.md](../../索引.md),本文件不再重复描述。 -## 记忆存储 +## 一、记忆存储 + +### 1.1 存储位置与组织 - 智能体的记忆必须存放在 `.trae/memory` 文件夹中 - 记忆应按对话日期或主题进行组织,便于后续查询和参考 - 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息 - **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.trae/rules/项目/04-即时状态记忆.md`,便于其他智能体或开发者快速对齐 -## 规范同步 +### 1.2 内容规范 + +- 记忆文件应保持简洁明了,重点记录重要的开发决策和规范变更 +- 避免存储冗余信息,只记录对项目有价值的内容 +- 定期清理过时的记忆文件,保持存储空间的合理使用 + +### 1.3 与"项目即时状态"的边界 + +- `.trae/memory/` 偏向**长期保留**的对话产物与决策记录 +- `.trae/rules/项目/04-即时状态记忆.md` 偏向**当前快照**(实现进度、未完结事项),更新频率高 +- 二者不要重复存放同一份信息;以"是否需要长期沉淀"为判定标准 + +### 1.4 memory/ 生命周期 + +研发工单验收完成后,对 `memory/` 的处理遵循以下原则: + +- **追加而非覆盖**:将本次工单中产生的、值得**长期沉淀**的内容(架构决策、关键避坑经验、引入的新依赖与版本)追加到对应文件 +- **不存放过程信息**:实现进度、待办勾选、临时决策这些短期信息应留在 [04-即时状态记忆.md](../项目/04-即时状态记忆.md),不进 `memory/` +- **不删长期内容**:除非内容已被证伪或过时,否则不删除既有条目;过时内容用"已废弃 / 已被 XX 取代"的形式保留语义而非物理删除 +- **新增文件序号化**:当主题足够独立时新建 `NN-名称.md`(序号紧接当前最大值),并在 [.trae/索引.md](../../索引.md) 的 `memory/` 章节同步追加链接 + +### 1.5 访问权限 + +- 记忆文件仅供开发团队内部参考使用 +- 确保记忆文件中的敏感信息得到适当保护 +- 遵循项目的版本控制和代码管理规范 + +## 二、规范同步 - 每次对话中涉及到的语法或规范相关内容,必须同步整理到 `.trae/rules` 目录下的对应文件中 - **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/` @@ -19,11 +48,11 @@ - 若涉及到新的规范或规则,应创建新的规则文件进行记录 - 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践 -## 文件命名规则(强制) +## 三、文件命名规则(强制) `.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规: -- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `08-XXX规范.md` +- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `06-XXX规范.md` - **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复 - **入口/索引文件例外**:`.trae/索引.md` 这类目录入口文件不带序号 - **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾 @@ -34,18 +63,18 @@ | 子目录 | 当前最大序号 | 下一个可用 | |---|---|---| -| `rules/全局/` | 08 | 09 | -| `rules/项目/` | 04 | 05 | +| `rules/全局/` | 06 | 07 | +| `rules/项目/` | 05 | 06 | | `memory/` | 01 | 02 | | `coordination/` | 02 | 03 | -## 实现要求 +## 四、实现要求 - 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致 - 当发现规范冲突或需要调整时,应及时记录并通知相关人员 - 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程 -## 路径规范 +## 五、路径规范 - 所有 Markdown 文档中不应使用绝对路径,应使用相对路径 - 相对路径应以项目根目录为基准,例如 `.trae/memory` 而非绝对路径 diff --git a/.trae/rules/全局/03-注释规范.md b/.trae/rules/全局/02-注释规范.md similarity index 100% rename from .trae/rules/全局/03-注释规范.md rename to .trae/rules/全局/02-注释规范.md diff --git a/.trae/rules/全局/02-记忆存储规范.md b/.trae/rules/全局/02-记忆存储规范.md deleted file mode 100644 index 6480b6a..0000000 --- a/.trae/rules/全局/02-记忆存储规范.md +++ /dev/null @@ -1,33 +0,0 @@ -# 记忆存储规范 - -> 适用范围:本规则属于 **全局规则**(跨项目通用),约束 `.trae/memory/` 的使用方式。 - -## 存储结构 -- 记忆文件应存放在 `.trae/memory` 文件夹中 -- 避免使用与日期相关的文件名,使用通用的描述性文件名 -- 文件命名采用 `NN-名称.md` 序号格式 -- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息 - -## 内容规范 -- 记忆文件应保持简洁明了,重点记录重要的开发决策和规范变更 -- 避免存储冗余信息,只记录对项目有价值的内容 -- 定期清理过时的记忆文件,保持存储空间的合理使用 - -## 与"项目即时状态"的边界 -- `.trae/memory/` 偏向**长期保留**的对话产物与决策记录 -- `.trae/rules/项目/04-即时状态记忆.md` 偏向**当前快照**(实现进度、未完结事项),更新频率高 -- 二者不要重复存放同一份信息;以"是否需要长期沉淀"为判定标准 - -## memory/ 生命周期 - -研发工单验收完成后,对 `memory/` 的处理遵循以下原则: - -- **追加而非覆盖**:将本次工单中产生的、值得**长期沉淀**的内容(架构决策、关键避坑经验、引入的新依赖与版本)追加到对应文件 -- **不存放过程信息**:实现进度、待办勾选、临时决策这些短期信息应留在 [04-即时状态记忆.md](../项目/04-即时状态记忆.md),不进 `memory/` -- **不删长期内容**:除非内容已被证伪或过时,否则不删除既有条目;过时内容用"已废弃 / 已被 XX 取代"的形式保留语义而非物理删除 -- **新增文件序号化**:当主题足够独立时新建 `NN-名称.md`(序号紧接当前最大值),并在 [.trae/索引.md](../../索引.md) 的 `memory/` 章节同步追加链接 - -## 访问权限 -- 记忆文件仅供开发团队内部参考使用 -- 确保记忆文件中的敏感信息得到适当保护 -- 遵循项目的版本控制和代码管理规范 diff --git a/.trae/rules/全局/04-文档同步规范.md b/.trae/rules/全局/03-文档同步规范.md similarity index 96% rename from .trae/rules/全局/04-文档同步规范.md rename to .trae/rules/全局/03-文档同步规范.md index ddd34f7..6da5e44 100644 --- a/.trae/rules/全局/04-文档同步规范.md +++ b/.trae/rules/全局/03-文档同步规范.md @@ -14,7 +14,7 @@ description: 强制文档同步规范:每次变更代码(如新增功能、 - **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。 - **协作友好(局部修改)**:当并行处理多个研发工单/需求时,更新文档应尽量只修改与本工单直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。 -> 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [05-研发工单规则.md](./05-研发工单规则.md)。 +> 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。 ## 更新范围 diff --git a/.trae/rules/全局/04-研发工单全流程规范.md b/.trae/rules/全局/04-研发工单全流程规范.md new file mode 100644 index 0000000..e28e360 --- /dev/null +++ b/.trae/rules/全局/04-研发工单全流程规范.md @@ -0,0 +1,141 @@ +# 研发工单全流程规范(必须遵守) + +> 适用范围:本规则属于 **全局规则**(跨项目通用),覆盖研发工单从术语定义、拆分输出到新增约束的全流程。 + +> ⚠️ 术语澄清(必读) +> +> 本项目存在两类"任务"概念,必须严格区分: +> +> | 术语 | 含义 | 适用范围 | +> |---|---|---| +> | **研发工单(Dev Work Item)** | 智能体 / 开发者执行的**编码工作项** | 本规则的全部内容 | +> | **Todo 待办项(Todo Item)** | Hua.Todo 项目**业务领域**中用户创建的待办事项 | 业务代码、产品文档 | +> +> 在代码、文档与对话中,凡涉及编码侧拆分时,**必须使用"研发工单"或"工单"**,禁止使用"任务"二字。 +> 业务侧 `Task` / `SubTask` 等代码标识符**保持不变**(已固化于 API、DB、UI)。 + +--- + +## 一、研发工单拆分规范 + +### 1.1 适用时机 + +- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出**研发工单拆分文档** + +### 1.2 输出要求 + +1. **先读完所有相关文档**:包括 `docs/`、`docs/project/` 下与本次需求相关的内容 +2. **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据 +3. **新增专属文件夹**:在 `docs/project` 下新建 `研发工单-<主题>-<日期或版本>` 文件夹 +4. **可并行工单拆分**:能同步执行的工单必须拆到不同 Markdown 文件中 +5. **文件带序号**:按执行顺序编号(`01-xxx.md`、`02-xxx.md`) + +### 1.3 每个研发工单文件必须包含 + +- 目标 / 范围(做什么、不做什么) +- 前置条件(依赖哪些结论 / 接口 / 文档) +- 验收标准(可执行的验证点) +- 风险与回滚(如有) + +### 1.4 子工单完成标记要求 + +- 子工单完成后,必须在 `00-工单总览.md` 中标注"已完成" +- 维护"待验证表",记录每个子工单的"待验证 / 已验证"状态 + +### 1.5 并行冲突规避要求 + +当工单会被分发到多个 solo 窗口并行推进时,每个研发工单文件必须额外包含 Touch List、共享文件策略与编译绿线策略。详细规约见 [05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md)。 + +### 1.6 推荐结构 + +``` +docs/project/研发工单-<主题>-<版本>/ +├── 00-工单总览.md # 背景、目标、关键决策、并行分组、待验证表 +├── 01-并行工单A.md +├── 02-并行工单B.md +└── 03-串行工单C.md +``` + +--- + +## 二、新增工单约束 + +> 核心原则:**新增工单时,不得修改、覆盖、重排、删除任何已有工单文件。** + +### 2.1 已有文件不可触碰 + +| 操作 | 是否允许 | 说明 | +|---|---|---| +| 修改已有工单的 `.md` 内容 | ❌ 禁止 | 即使发现格式、措辞可优化 | +| 重命名已有工单文件 | ❌ 禁止 | | +| 删除已有工单文件 | ❌ 禁止 | | +| 重排已有工单的序号 | ❌ 禁止 | 除非用户明确要求整体重构 | +| 修改 `00-工单总览.md` 中已有条目 | ❌ 禁止 | 只能追加新条目 | +| 在 `00-工单总览.md` 中追加新条目 | ✅ 允许 | | +| 修改 `.trae/rules/项目/04-即时状态记忆.md` 中已有快照行 | ❌ 禁止 | 只能追加新版本行 | +| 新增章节到 `04-即时状态记忆.md` | ✅ 允许 | | + +### 2.2 子工单拆分格式 + +``` +NN-NN-标题.md +``` + +- 前两位:主工单序号,后两位:子工单序号 +- 示例:`03-01-会议数据模型与API.md` + +### 2.3 新增工单的序号确定 + +1. 列出目标文件夹中已有文件 +2. 找出最大主序号 +3. 新增工单的主序号 = 最大主序号 + 1 +4. 子工单的子序号从 `01` 开始递增 + +### 2.4 可追加修改的文件(例外) + +| 文件 | 允许 | 不允许 | +|---|---|---| +| `00-工单总览.md` | 追加新条目 | 改写已有条目 | +| `.trae/rules/项目/04-即时状态记忆.md` | 新增版本章节 | 修改已有快照行 | +| `.trae/索引.md` | 追加新规则链接 | 修改已有条目 | + +--- + +## 三、与业务侧 Todo 待办项的边界 + +- 代码、注释、提交信息中描述**编码工作**时:使用"研发工单 / 工单 / 子工单" +- 代码、注释、提交信息中描述**业务功能**时:使用"Todo 待办项 / Todo Item / 父子任务(业务实体)" +- 文档命名前缀: + - 编码侧:`研发工单-<主题>-<版本>/` + - 业务侧:遵循 `docs/` 既有命名习惯 +- 提交信息示例: + - ✅ `feat(todo): 新增 Todo 待办项截止日期字段(研发工单 02-后端模型)` + - ❌ `feat: 完成任务 02`("任务"歧义) + +--- + +## 四、检查清单 + +### 研发工单拆分检查 + +1. [ ] 是否已阅读完所有相关文档? +2. [ ] 是否在 `docs/project` 下新建了专属文件夹(命名以"研发工单-"开头)? +3. [ ] 是否产出 `00-工单总览.md`? +4. [ ] 是否将可并行工单拆分为不同 md 文件? +5. [ ] 是否所有 md 文件都带有连续序号? +6. [ ] 子工单完成后是否在总览中标注"已完成"并更新待验证表? + +### 新增工单检查 + +1. [ ] 新增工单的序号是否为当前最大主序号 + 1? +2. [ ] 子工单是否使用了 `NN-NN-标题.md` 格式? +3. [ ] 是否**未修改**任何已有工单文件的内容? +4. [ ] 是否**未修改** `00-工单总览.md` 和 `04-即时状态记忆.md` 中已有条目? + +### 并行冲突检查 + +> 详见 [05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md#最小检查清单)。 + +--- + +**关联规则**:[05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md) diff --git a/.trae/rules/全局/07-并行窗口冲突规约.md b/.trae/rules/全局/05-并行窗口冲突规约.md similarity index 98% rename from .trae/rules/全局/07-并行窗口冲突规约.md rename to .trae/rules/全局/05-并行窗口冲突规约.md index e64a8d0..541408a 100644 --- a/.trae/rules/全局/07-并行窗口冲突规约.md +++ b/.trae/rules/全局/05-并行窗口冲突规约.md @@ -7,7 +7,7 @@ description: > 适用范围:本规则属于 **全局规则**(跨项目通用)。 > ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。 -> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。 +> 详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。 > 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。 ## 适用范围 diff --git a/.trae/rules/全局/05-研发工单规则.md b/.trae/rules/全局/05-研发工单规则.md deleted file mode 100644 index 418f659..0000000 --- a/.trae/rules/全局/05-研发工单规则.md +++ /dev/null @@ -1,129 +0,0 @@ -# 研发工单同步规则汇总(Dev Work Item Rules) - -> 适用范围:本规则属于 **全局规则**(跨项目通用),关注智能体如何拆分编码工作。 - -> ⚠️ 术语澄清(必读) -> -> 本项目存在两类"任务"概念,必须严格区分,避免命名混淆: -> -> | 术语 | 含义 | 适用范围 | -> |---|---|---| -> | **研发工单(Dev Work Item)** | 智能体 / 开发者执行的**编码工作项**(拆分需求、并行开发、集成等) | 本规则文档的全部内容 | -> | **Todo 待办项(Todo Item)** | Hua.Todo 项目**业务领域**中用户创建的待办事项(数据库实体、API 资源、UI 列表项) | 业务代码、产品需求文档、技术设计文档 | -> -> 本文档中所有"研发工单 / 工单 / 子工单"均指**编码工作项**,与业务侧的 Todo 待办项无关。 -> 在代码、文档与对话中,凡涉及编码侧拆分时,**必须使用"研发工单"或"工单"**,禁止再使用"任务"二字以避免与 Todo 待办项混淆。 -> -> 业务侧由于历史原因仍保留 `Task` / `SubTask` 等代码标识符(API、实体、UI),这些属于 Todo 待办项语义,**不在本规范替换范围内**。 -> -> 本汇总文件是 [06-研发工单拆分规范.md](./06-研发工单拆分规范.md) 与 [07-并行窗口冲突规约.md](./07-并行窗口冲突规约.md) 的对外索引,详细规则以这两份源文件为准。 - ---- - -## 一、研发工单拆分规范 - -### 适用时机 -- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出**研发工单拆分文档** - -### 输出要求 -1. **先读完所有相关文档**:包括 `docs/`、`docs/project/` 下与本次需求相关的内容 -2. **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据 -3. **新增专属文件夹**:在 `docs/project` 下新建 `研发工单-<主题>-<日期或版本>` 文件夹 -4. **可并行工单拆分**:能同步执行的工单必须拆到不同 Markdown 文件中 -5. **文件带序号**:按执行顺序编号(`01-xxx.md`、`02-xxx.md`) - -### 每个研发工单文件必须包含 -- 目标 / 范围(做什么、不做什么) -- 前置条件(依赖哪些结论 / 接口 / 文档) -- 验收标准(可执行的验证点) -- 风险与回滚(如有) - -### 子工单完成标记要求 -- 子工单完成后,必须在 `00-工单总览.md` 中标注"已完成" -- 维护"待验证表",记录每个子工单的"待验证 / 已验证"状态 - ---- - -## 二、并行窗口冲突规约 - -### 核心原则 -1. **先声明后修改**:修改前先声明 Touch List 与共享文件策略 -2. **文件所有权唯一**:同一时段内一个文件只能由一个窗口修改 -3. **共享文件单点修改**:高耦合 / 共享入口的改动集中到集成窗口完成 -4. **绿线优先**:任何可落盘的变更必须保持可编译 - -### Touch List 要求 -- 精确到文件路径 -- 标注修改类型(新增 / 小改 / 重构 / 接口变更 / 配置变更) -- 标注是否为共享文件 -- 使用相对路径:`src\\` - -### 共享文件判定标准(满足其一即为共享) -- 项目入口 / 启动逻辑、依赖注入注册、全局路由 -- 公共配置、公共协议与 DTO、公共组件 / 样式 -- 解决方案文件(`.sln`、`.csproj`)、锁文件、全局配置 - -### Writer 约束 -- 非 Writer 窗口不得编辑共享文件 -- 非 Writer 只能提供"差异建议"给 Writer 落盘 - -### 协调目录 -固定目录:`.trae\coordination\` -- `01-ownership.md`:文件所有权登记表 -- `02-shared-files.md`:共享文件清单(集成窗口维护) -- `handoff\`:差异建议 / 交接说明 -- `wip\`:编译中途状态说明 - -### 编译绿线规则 -- 不得提交破坏编译的变更 -- 临时隔离手段(按优先级): - 1. 新功能先放在新文件中,不在入口路径启用 - 2. 通过显式开关控制,默认关闭 - 3. 通过依赖注入分支或特性开关隔离 -- 接口演进采用"双写 / 兼容期"策略 - ---- - -## 三、推荐文档结构 - -``` -docs/project/研发工单-<主题>-<版本>/ -├── 00-工单总览.md # 背景、目标、关键决策、并行分组、待验证表 -├── 01-并行工单A.md -├── 02-并行工单B.md -└── 03-串行工单C.md -``` - -> 注意:上述目录与文件名中的"工单"指**研发工单**,与业务侧 Todo 待办项无关。 - ---- - -## 四、检查清单 - -### 研发工单拆分检查 -1. [ ] 是否已阅读完所有相关文档? -2. [ ] 是否在 `docs/project` 下新建了专属文件夹(命名以"研发工单-"开头)? -3. [ ] 是否产出 `00-工单总览.md`? -4. [ ] 是否将可并行工单拆分为不同 md 文件? -5. [ ] 是否所有 md 文件都带有连续序号? -6. [ ] 子工单完成后是否在总览中标注"已完成"并更新待验证表? - -### 并行冲突检查 -1. [ ] 每个研发工单 md 是否已写 Touch List(精确到文件)? -2. [ ] Touch List 中的共享文件是否指定了唯一 Writer? -3. [ ] 是否避免了对共享文件的无意义格式化 / 重排? -4. [ ] 当前改动是否保持可编译(绿线)? -5. [ ] 若涉及接口演进,是否采用兼容期策略? - ---- - -## 五、与业务侧 Todo 待办项的边界 - -- 代码、注释、提交信息中描述**编码工作**时:使用"研发工单 / 工单 / 子工单" -- 代码、注释、提交信息中描述**业务功能**时:使用"Todo 待办项 / Todo Item / 父子任务(业务实体)" -- 文档命名前缀: - - 编码侧:`研发工单-<主题>-<版本>/` - - 业务侧(如有):遵循 `docs/` 既有命名习惯,禁止使用"研发工单"前缀 -- 提交信息示例: - - ✅ `feat(todo): 新增 Todo 待办项截止日期字段(研发工单 02-后端模型)` - - ❌ `feat: 完成任务 02`("任务"歧义,禁用) diff --git a/.trae/rules/全局/08-AI沟通记录规范.md b/.trae/rules/全局/06-AI沟通记录规范.md similarity index 100% rename from .trae/rules/全局/08-AI沟通记录规范.md rename to .trae/rules/全局/06-AI沟通记录规范.md diff --git a/.trae/rules/全局/06-研发工单拆分规范.md b/.trae/rules/全局/06-研发工单拆分规范.md deleted file mode 100644 index cc0684d..0000000 --- a/.trae/rules/全局/06-研发工单拆分规范.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -alwaysApply: false ---- -# 研发工单拆分输出规范(必须遵守) - -> 适用范围:本规则属于 **全局规则**(跨项目通用)。 - -> ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。 -> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。 -> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。 - -## 适用时机 - -- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出研发工单拆分文档,再开始写代码或改配置。 - -## 输出要求 - -- **先读完所有相关文档**:包括但不限于 `docs/`、`docs/project/` 下与本次需求相关的内容。 -- **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据。 -- **新增一个专属文件夹**:在 `docs/project` 下新建一个文件夹存放本次研发工单拆分文档。 - - 文件夹命名建议:`研发工单-<主题>-<日期或版本>`(保持可检索、避免与既有文档冲突)。 -- **可并行的工单要拆成不同 md**:能同步执行(相互无依赖/弱依赖)的工单,必须拆到不同的 Markdown 文件中,便于并行推进与分工。 -- **文件必须带序号**:同一文件夹下的 md 文件按执行顺序编号,序号从小到大。 - - 文件名建议:`01-xxx.md`、`02-xxx.md`、`03-xxx.md`。 -- **每个研发工单文件至少包含**: - - 目标/范围(做什么、不做什么) - - 前置条件(依赖哪些结论/接口/文档) - - 验收标准(怎么判断完成,包含可执行的验证点) - - 风险与回滚(如有) -- **子工单完成后的标记要求**: - - 当任一子工单(例如 `01-*`/`02-*`/`03-*`)完成实现后,必须在对应版本的 `00-工单总览.md` 中同步标注"已完成"。 - - 同时必须维护一张"待验证表"(可用 Markdown 表格),对每个子工单给出"待验证/已验证"状态,避免实现完成但验收未闭环。 -- **并行冲突规避要求**:当工单会被分发到多个 solo 窗口并行推进时,每个研发工单文件必须额外包含: - - 触碰文件清单(Touch List,精确到文件) - - 共享文件策略(哪些是共享文件、唯一 Writer 是谁、如何与集成窗口对接) - - 编译绿线策略(如何确保阶段性交付不破坏编译) - -## 推荐结构(模板) - -- `00-工单总览.md` - - 背景与目标 - - 关键决策与约束 - - 并行分组说明(哪些文件可同步做) - - 待验证表(每个子工单的"待验证/已验证"状态) -- `01-<并行工单A>.md` -- `02-<并行工单B>.md` -- `03-<串行工单C>.md` - -## 最小检查清单 - -1. [ ] 是否确认已阅读完所有相关文档? -2. [ ] 是否在 `docs/project` 下新建了本次专属文件夹(命名以"研发工单-"开头)? -3. [ ] 是否产出 `00-工单总览.md`(或等价总览文件)? -4. [ ] 是否将可并行工单拆分为不同 md 文件? -5. [ ] 是否所有 md 文件都带有连续序号? -6. [ ] 并行工单是否为每个研发工单文件补充了 Touch List/共享文件策略/编译绿线策略? -7. [ ] 子工单完成后,是否在对应版本的 `00-工单总览.md` 标注"已完成",并在"待验证表"里更新状态? diff --git a/.trae/rules/项目/02-业务命名规范.md b/.trae/rules/项目/02-业务命名规范.md index 719ab1c..25ecb31 100644 --- a/.trae/rules/项目/02-业务命名规范.md +++ b/.trae/rules/项目/02-业务命名规范.md @@ -3,7 +3,7 @@ > 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。 > > 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。 -> 全局术语规则参见 [.trae/rules/全局/05-研发工单规则.md](../全局/05-研发工单规则.md)。 +> 全局术语规则参见 [.trae/rules/全局/04-研发工单全流程规范.md](../全局/04-研发工单全流程规范.md)。 ## 一、术语对照(核心) diff --git a/.trae/rules/项目/04-即时状态记忆.md b/.trae/rules/项目/04-即时状态记忆.md index ebb3990..34c4f1d 100644 --- a/.trae/rules/项目/04-即时状态记忆.md +++ b/.trae/rules/项目/04-即时状态记忆.md @@ -11,8 +11,9 @@ ## 一、当前活跃版本 -- **进行中版本**:v1.2.0 -- **研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md) +- **进行中版本**:v1.2.0(收尾中)、v1.3.0(规划中) +- **v1.2.0 研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md) +- **v1.3.0 研发工单总览**:[docs/project/研发工单-v1.3.0/00-工单总览.md](../../../docs/project/研发工单-v1.3.0/00-工单总览.md) - **PRD**:[docs/project/产品需求文档-1.2.0.md](../../../docs/project/产品需求文档-1.2.0.md) ## 二、v1.2.0 工单状态快照 @@ -31,14 +32,27 @@ | 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` | | 09 - CloudSync 同步策略改进 | 已实现 | 待验证 | TaskEntity 继承 ABP 基类;软删除修复(SaveChangesAsync);前端类型和 cloudSync.ts 已更新;新增 guid.ts | -## 三、关键临时决策 +## 三、v1.3.0 工单状态快照 + +| 子工单 | 实现状态 | 验证状态 | 简要说明 | +|---|---|---|---| +| 01 - HTTP 服务转换 MCP 服务 | 进行中 | 待验证 | 将现有 HTTP API 映射为 MCP 工具描述符 | +| 02 - 语音控制与 AI 辅助 | 待开始 | 待验证 | STT/TTS + 语音指令解析 + AI 辅助任务拆分 | +| 03 - 会议任务拆分 | 待开始 | 待验证 | 会议类型入口 + 录音/文字输入 + AI 拆分建议 + 确认批量创建 | +| 03-01 - 会议数据模型与 API | 待开始 | 待验证 | TaskType 枚举、MeetingNotes/AudioDuration 字段、MeetingController | +| 03-02 - 音频录制与转写 | 待开始 | 待验证 | 前端 MediaRecorder 录音 + 后端 STT 转写 | +| 03-03 - AI 任务拆分服务 | 待开始 | 待验证 | 会议专用 LLM prompt + 批量创建子任务 | +| 03-04 | 任务建议与确认 UI | 待开始 | 待验证 | 录音/纪要/审阅对话框 + Meeting 类型条件渲染 | +| 04 | 富文本描述、附件与外部链接 | 待开始 | 待验证 | 多行描述 + 附件上传下载删除 + 外部链接 + 桌面端 Process.Start/xdg-open | + +## 四、关键临时决策 - **MAUI 端不暴露云同步端点**:`MauiProgram.cs` 仅注册 `AddApplicationServices()`,不调 `AddCloudSyncServer()`。云同步端点只在 `Hua.Todo.Host` 暴露。 - **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。 - **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。 - **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。 -## 四、已知未完结事项 / 待办 +## 五、已知未完结事项 / 待办 - [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证 - [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现 @@ -46,7 +60,7 @@ - [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证") - [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试) -## 五、最近一次重大重构(如有) +## 六、最近一次重大重构(如有) - **术语统一与目录中文化**(2026-06): - `.trae/rules/` 全部中文文件名 + 拆分为 `全局/` 和 `项目/` 两个子目录 diff --git a/.trae/rules/项目/05-多入口功能同步规范.md b/.trae/rules/项目/05-多入口功能同步规范.md new file mode 100644 index 0000000..35ef452 --- /dev/null +++ b/.trae/rules/项目/05-多入口功能同步规范.md @@ -0,0 +1,85 @@ +# 多入口功能同步规范(Hua.Todo 专属) + +> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。 + +--- + +## 一、背景 + +Hua.Todo 存在两个功能入口: + +| 入口 | 位置 | 方式 | +|---|---|---| +| **UI 入口** | WebView / 前端界面(Hua.Todo.Web) | 键盘/鼠标/触控交互 | +| **语音控制入口** | 语音指令(平台原生 STT + LLM 意图解析) | 语音输入 → 指令执行 | + +v1.3.0 之前,功能开发只关注 UI 入口。v1.3.0 工单 02 落地后语音控制入口正式就绪,此后**所有新增功能必须在需求阶段同步确认两个入口的覆盖情况**。 + +--- + +## 二、核心规则 + +### 2.1 功能入口检查(强制) + +每次新增功能(含新工单、新特性),智能体必须在需求讨论或工单拆分阶段执行以下检查: + +| 检查项 | 说明 | +|---|---| +| **UI 入口** | 当前功能是否已有 UI 入口规划?入口在哪个页面/组件?交互方式是什么? | +| **语音控制入口** | 当前功能是否已有语音指令规划?对应哪个意图?参数是什么?LLM prompt 是否需要更新? | + +### 2.2 缺失时为通知用户,不自行决策 + +- 任一入口缺失时,**智能体必须主动告知用户**,由用户决定: + 1. 本次就做(补上缺失入口) + 2. 本次不做(记录为已知缺口,后续版本补) + 3. 不需要(该入口不适合此功能,如选项过多需可视化交互的操作) +- **智能体不得在用户未确认的情况下自行跳过或自行补充入口设计** + +### 2.3 输出格式(检查表) + +智能体在需求讨论阶段,必须输出以下检查表: + +```markdown +## 功能入口覆盖检查 + +| 入口 | 已规划 | 方案 | +|---|---|---| +| UI | ✅ / ❌ | [描述 UI 入口] | +| 语音 | ✅ / ❌ | [描述语音指令与意图] | + +> 不可覆盖的入口说明:[如"语音控制暂不支持 XX 操作(原因)"] +``` + +--- + +## 三、适用场景 + +### 3.1 必须检查的场景 + +- 新增业务功能(如新增双因素认证、新增标签系统) +- 新增 API 端点(如新增导出/导入) +- 新增 UI 页面/组件(如新增设置页、新增弹窗) +- 新工单拆分阶段 + +### 3.2 无需检查的场景 + +- 纯 bug 修复 +- 纯性能优化(不改变用户可感知功能) +- 纯基础设施调整(如 CI/CD、打包脚本) +- 依赖升级 + +--- + +## 四、检查清单 + +1. [ ] 本次新增功能是否已确认 UI 入口? +2. [ ] 本次新增功能是否已确认语音控制入口? +3. [ ] 缺失入口是否已通知用户并记录决策? +4. [ ] LLM intent parser 的 prompt 是否需要同步更新(新增意图/新增参数)? + +--- + +## 五、与工单 02 的关系 + +本规范的语音入口覆盖检查表来源于 [docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md](../../../docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md) 的第七章"与其他工单的语音入口衔接"。 diff --git a/.trae/索引.md b/.trae/索引.md index 8a2cf30..4aa64b6 100644 --- a/.trae/索引.md +++ b/.trae/索引.md @@ -8,9 +8,9 @@ > > 文件命名约定:每个子目录内文件以 `NN-名称.md` 格式编号(两位数字),编号反映**阅读优先级 / 依赖顺序**。索引文件本身不带序号。 > -> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-智能体记忆.md](./rules/全局/01-智能体记忆.md#文件命名规则强制)。 +> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#三文件命名规则强制)。 > -> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md#memory-生命周期)。 +> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#14-memory-生命周期)。 --- @@ -32,14 +32,12 @@ | 文件 | 职责 | |---|---| -| [01-智能体记忆.md](./rules/全局/01-智能体记忆.md) | 智能体记忆/规范同步机制(**不含目录总览,由本索引文件承担**) | -| [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md) | `.trae/memory/` 的使用方式 | -| [03-注释规范.md](./rules/全局/03-注释规范.md) | C# / TypeScript / Vue 代码注释要求 | -| [04-文档同步规范.md](./rules/全局/04-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 | -| [05-研发工单规则.md](./rules/全局/05-研发工单规则.md) | 研发工单术语与边界(编码工作项 vs Todo 待办项) | -| [06-研发工单拆分规范.md](./rules/全局/06-研发工单拆分规范.md) | 工单拆分目录与文件结构 | -| [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 | -| [08-AI沟通记录规范.md](./rules/全局/08-AI沟通记录规范.md) | 用户与智能体沟通记录的存储目录、序号管理与内容规范 | +| [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md) | 智能体记忆/存储/规范同步机制 & `.trae/memory/` 使用方式 & 文件命名规则(含序号速查表) | +| [02-注释规范.md](./rules/全局/02-注释规范.md) | C# / TypeScript / Vue 代码注释要求 | +| [03-文档同步规范.md](./rules/全局/03-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 | +| [04-研发工单全流程规范.md](./rules/全局/04-研发工单全流程规范.md) | 研发工单术语定义 + 拆分输出规范 + 新增工单约束(合并原 05/06/09) | +| [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 | +| [06-AI沟通记录规范.md](./rules/全局/06-AI沟通记录规范.md) | 用户与智能体沟通记录的存储目录、序号管理与内容规范 | --- @@ -51,6 +49,7 @@ | [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 | | [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 | | [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 | +| [05-多入口功能同步规范.md](./rules/项目/05-多入口功能同步规范.md) | 新增功能时必须同步确认 UI 入口与语音控制入口的覆盖情况 | --- @@ -58,7 +57,7 @@ - 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照 - 与"项目即时状态"的边界:[04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) 偏当前快照、更新频率高;`memory/` 偏长期保留 -- 维护规约:详见 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md) +- 维护规约:详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md) - 文件: - [01-project_memory.md](./memory/01-project_memory.md):依赖小版本号、历史决策、变更时间轴 @@ -67,7 +66,7 @@ ## coordination/ — 多窗口协作运行时目录 - 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**) -- 协作协议详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) +- 协作协议详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md) - 文件: - [00-README.md](./coordination/00-README.md):目录约定 - [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记 diff --git a/docs/manual/08-MCP服务接口文档.md b/docs/manual/08-MCP服务接口文档.md new file mode 100644 index 0000000..d1ad917 --- /dev/null +++ b/docs/manual/08-MCP服务接口文档.md @@ -0,0 +1,389 @@ +# MCP 服务接口文档 + +> Hua.Todo MCP Server 接口规范,供外部系统(AI 客户端、第三方集成)通过 MCP 协议接入 Hua.Todo 待办项管理能力。 + +## 1. 概述 + +Hua.Todo 提供了基于 **Model Context Protocol (MCP)** 的服务端,允许外部 MCP 客户端通过 Streamable HTTP 传输协议发现并调用待办项管理工具。 + +### 1.1 协议与传输 + +| 项目 | 说明 | +|---|---| +| 协议版本 | MCP 2025-03-26(Streamable HTTP) | +| 传输方式 | Streamable HTTP(无状态模式) | +| 内容格式 | JSON-RPC 2.0 | +| 端点路径 | `/mcp` | +| 认证 | 暂无(本地模式);生产环境建议通过反向代理添加认证 | + +### 1.2 服务端信息 + +```json +{ + "name": "Hua.Todo MCP Server", + "version": "1.0.0" +} +``` + +### 1.3 连接地址 + +| 运行模式 | 默认地址 | +|---|---| +| Hua.Todo.Host(独立服务端) | `http://localhost:5173/mcp` | +| MAUI / Avalonia(嵌入式) | `http://localhost:5057/mcp` | + +> 实际端口以部署配置为准。 + +--- + +## 2. 接入方式 + +### 2.1 MCP 客户端配置示例 + +**Claude Desktop / Cursor / 其他 MCP 客户端** 配置文件(`mcp_servers.json` 或等效): + +```json +{ + "mcpServers": { + "hua-todo": { + "url": "http://localhost:5173/mcp", + "transport": "streamable-http" + } + } +} +``` + +### 2.2 手动调用示例(curl) + +MCP 协议基于 JSON-RPC 2.0,可通过 HTTP POST 手动调用: + +#### 初始化连接 + +```bash +curl -X POST http://localhost:5173/mcp \ + -H "Content-Type: application/json" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { "name": "my-client", "version": "1.0.0" } + } + }' +``` + +#### 列出可用工具 + +```bash +curl -X POST http://localhost:5173/mcp \ + -H "Content-Type: application/json" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + }' +``` + +#### 调用工具 + +```bash +curl -X POST http://localhost:5173/mcp \ + -H "Content-Type: application/json" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "CreateTodo", + "arguments": { + "title": "完成项目报告", + "priority": "High" + } + } + }' +``` + +--- + +## 3. 工具清单 + +### 3.1 查询类工具 + +#### ListAllTodos + +获取所有待办项列表(含已完成和未完成)。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 无 | - | - | - | + +**返回示例**: + +``` +所有待办项(共 3 项): +[1] 完成项目报告 | 优先级:High | 进行中 +[2] 购买办公用品 | 优先级:Medium | 已完成 +[3] 整理会议纪要 | 优先级:Low | 进行中 +``` + +--- + +#### ListActiveTodos + +获取未完成的待办项列表。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 无 | - | - | - | + +**返回示例**: + +``` +未完成待办项(共 2 项): +[1] 完成项目报告 | 优先级:High | 进行中 +[3] 整理会议纪要 | 优先级:Low | 进行中 +``` + +--- + +#### ListCompletedTodos + +获取已完成的待办项列表。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| 无 | - | - | - | + +--- + +#### GetTodoById + +根据 ID 获取单个待办项详情(含子任务)。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `int` | 是 | 待办项 ID | + +**返回示例**: + +```json +{ + "id": 1, + "title": "完成项目报告", + "priority": "High", + "isCompleted": false, + "createdAt": "2026-06-16T08:30:00Z", + "updatedAt": "2026-06-16T08:30:00Z", + "parentTaskId": null, + "subTasks": [ + { + "id": 4, + "title": "收集数据", + "priority": "Medium", + "isCompleted": true, + "createdAt": "2026-06-16T08:35:00Z", + "updatedAt": "2026-06-16T10:00:00Z", + "parentTaskId": 1, + "subTasks": [] + } + ] +} +``` + +**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"` + +--- + +#### ListSubTodos + +获取指定父待办项下的所有子待办项。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `parentTaskId` | `int` | 是 | 父待办项 ID | + +--- + +### 3.2 写入类工具 + +#### CreateTodo + +创建新的待办项。 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|---|---|---|---|---| +| `title` | `string` | 是 | - | 待办项标题 | +| `priority` | `string` | 否 | `"Medium"` | 优先级:`Low` / `Medium` / `High` | +| `parentTaskId` | `int` | 否 | `null` | 父待办项 ID(创建子任务时传入) | + +**返回示例**: + +``` +已创建待办项: +{ + "id": 5, + "title": "完成项目报告", + "priority": "High", + "isCompleted": false, + "createdAt": "2026-06-16T10:00:00Z", + "updatedAt": "2026-06-16T10:00:00Z", + "parentTaskId": null, + "subTasks": [] +} +``` + +**优先级无效时**:返回文本 `"无效的优先级 'xxx',有效值为:Low、Medium、High"` + +--- + +#### UpdateTodo + +更新已有待办项的标题或优先级。 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|---|---|---|---|---| +| `id` | `int` | 是 | - | 待办项 ID | +| `title` | `string` | 否 | `null` | 新标题 | +| `priority` | `string` | 否 | `null` | 新优先级:`Low` / `Medium` / `High` | + +**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"` + +--- + +#### ToggleTodoComplete + +切换待办项的完成状态(已完成 ↔ 未完成)。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `int` | 是 | 待办项 ID | + +**返回示例**: + +``` +已切换完成状态: +{ + "id": 1, + "title": "完成项目报告", + "priority": "High", + "isCompleted": true, + ... +} +``` + +--- + +#### DeleteTodo + +删除指定待办项。 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `int` | 是 | 待办项 ID | + +**成功时**:返回文本 `"已删除待办项 {id}"` + +**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"` + +--- + +## 4. 数据类型定义 + +### 4.1 TaskDto(待办项) + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | `int` | 唯一标识符 | +| `title` | `string` | 标题 | +| `priority` | `string` | 优先级枚举值:`"Low"` / `"Medium"` / `"High"` | +| `isCompleted` | `bool` | 是否已完成 | +| `createdAt` | `string` | 创建时间(ISO 8601 UTC) | +| `updatedAt` | `string` | 更新时间(ISO 8601 UTC) | +| `parentTaskId` | `int?` | 父待办项 ID(顶层任务为 null) | +| `subTasks` | `TaskDto[]` | 子任务列表(递归结构) | + +### 4.2 优先级枚举 + +| 值 | 说明 | +|---|---| +| `"Low"` | 低优先级 | +| `"Medium"` | 中优先级(默认) | +| `"High"` | 高优先级 | + +--- + +## 5. 错误处理 + +MCP 工具调用中的错误通过返回文本内容表达(而非 MCP 协议级错误),常见情况: + +| 场景 | 返回内容 | +|---|---| +| 待办项不存在 | `"未找到 ID 为 {id} 的待办项"` | +| 优先级值无效 | `"无效的优先级 'xxx',有效值为:Low、Medium、High"` | +| 列表为空 | `"没有{label}"` | + +MCP 协议级错误(如方法不存在、参数格式错误)遵循 JSON-RPC 2.0 标准: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32600, + "message": "Invalid Request" + } +} +``` + +--- + +## 6. 与 HTTP API 的对照 + +MCP 工具与原有 HTTP Dynamic API 的对应关系: + +| MCP 工具 | HTTP API | 说明 | +|---|---|---| +| `ListAllTodos` | `GET /api/task` | 获取全部 | +| `ListActiveTodos` | `GET /api/task/active` | 获取未完成 | +| `ListCompletedTodos` | `GET /api/task/completed` | 获取已完成 | +| `GetTodoById` | `GET /api/task/{id}` | 按 ID 查询 | +| `CreateTodo` | `POST /api/task` | 创建 | +| `UpdateTodo` | `PUT /api/task` | 更新 | +| `ToggleTodoComplete` | `PATCH /api/task/{id}/toggle` | 切换完成 | +| `DeleteTodo` | `DELETE /api/task/{id}` | 删除 | +| `ListSubTodos` | `GET /api/task/{parentTaskId}/subtasks` | 子任务 | + +> 两套 API 共享同一 `ITaskService` 实现,数据完全一致,可根据场景选择使用。 + +--- + +## 7. 安全建议(生产部署) + +1. **网络隔离**:MCP 端点默认无认证,建议仅在可信网络内暴露,或通过反向代理添加 API Key / Bearer Token 认证 +2. **CORS 限制**:生产环境应将 CORS 策略从 `AllowAll` 改为指定来源 +3. **HTTPS**:生产环境必须启用 HTTPS +4. **速率限制**:建议对 MCP 端点添加请求速率限制 + +--- + +## 8. 常见问题 + +### Q: MCP 端点和 HTTP API 可以同时使用吗? +可以。两者共享相同的数据源和服务层,不存在冲突。 + +### Q: Streamable HTTP 无状态模式下支持服务端通知吗? +不支持。无状态模式下服务端无法向客户端主动推送通知。如需通知能力,需改为有状态模式(`Stateless = false`),但需要会话亲和。 + +### Q: 如何验证 MCP 服务是否正常运行? +发送 `initialize` 请求(见 2.2 节),若返回服务端信息则表示正常。 + +--- + +**文档版本**:1.0.0 +**最后更新**:2026-06-16 diff --git a/docs/manual/09-MCP前端集成指南.md b/docs/manual/09-MCP前端集成指南.md new file mode 100644 index 0000000..28f8176 --- /dev/null +++ b/docs/manual/09-MCP前端集成指南.md @@ -0,0 +1,210 @@ +# MCP 前端集成指南(Hua.Todo 内部使用) + +> 适用范围:Hua.Todo 前端(Vue / TypeScript)团队,通过 MCP 协议与后端交互。 +> 外部系统接入请参阅 [08-MCP服务接口文档.md](./08-MCP服务接口文档.md)。 + +--- + +## 一、背景 + +v1.3.0 起,Hua.Todo 后端在原有 Dynamic API(`/api/*`)基础上,新增了 MCP(Model Context Protocol)服务端点。前端可根据场景选择: + +| 通道 | 端点 | 适用场景 | +|---|---|---| +| Dynamic API | `/api/task/*` | WebView 内常规 CRUD、已有逻辑兼容 | +| MCP | `/mcp` | AI 辅助、语音指令、外部工具集成 | + +两套通道共享同一 `ITaskService` 业务层,数据一致。 + +--- + +## 二、连接方式 + +### 2.1 嵌入式模式(MAUI / Avalonia + WebView) + +``` +MCP 端点:http://localhost:5057/mcp +``` + +当前嵌入式宿主**仅暴露 Dynamic API**,MCP 端点在嵌入式模式下同样可用(`AddMcpServerServices` 已注册)。 + +### 2.2 Host 模式(独立服务端) + +``` +MCP 端点:http://:5173/mcp +``` + +开发环境通过 Vite proxy 可直接访问 `/mcp`。 + +--- + +## 三、前端 MCP 客户端选型 + +### 3.1 推荐方案:`@modelcontextprotocol/sdk` + +官方 TypeScript MCP SDK,支持 Streamable HTTP 传输。 + +```bash +npm install @modelcontextprotocol/sdk +``` + +### 3.2 连接示例 + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js"; + +const transport = new StreamableHTTPClientTransport( + new URL("http://localhost:5057/mcp") +); + +const client = new Client({ + name: "hua-todo-frontend", + version: "1.3.0", +}); + +await client.connect(transport); + +// 列出所有可用工具 +const tools = await client.listTools(); +console.log("可用工具:", tools); + +// 调用工具 +const result = await client.callTool({ + name: "ListActiveTodos", + arguments: {}, +}); +console.log("未完成待办项:", result); +``` + +### 3.3 封装建议 + +在 `src/Hua.Todo.Web/src/api/` 下新增 `mcpClient.ts`: + +```typescript +// src/api/mcpClient.ts +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js"; + +const MCP_BASE_URL = window.__API_BASE_URL__ + ? window.__API_BASE_URL__.replace("/api", "/mcp") + : "/mcp"; + +let client: Client | null = null; + +/** 获取或创建 MCP 客户端单例 */ +export async function getMcpClient(): Promise { + if (!client) { + const transport = new StreamableHTTPClientTransport( + new URL(MCP_BASE_URL, window.location.origin) + ); + client = new Client({ + name: "hua-todo-frontend", + version: "1.3.0", + }); + await client.connect(transport); + } + return client; +} + +/** 断开 MCP 连接 */ +export async function disconnectMcp(): Promise { + if (client) { + await client.close(); + client = null; + } +} +``` + +--- + +## 四、工具调用映射(MCP vs Dynamic API) + +| 业务操作 | Dynamic API | MCP Tool | MCP 参数 | +|---|---|---|---| +| 获取所有待办项 | `GET /api/task` | `ListAllTodos` | 无 | +| 获取未完成待办项 | `GET /api/task/active` | `ListActiveTodos` | 无 | +| 获取已完成待办项 | `GET /api/task/completed` | `ListCompletedTodos` | 无 | +| 获取单个待办项 | `GET /api/task/{id}` | `GetTodoById` | `id: number` | +| 创建待办项 | `POST /api/task` | `CreateTodo` | `title, priority?, parentTaskId?` | +| 更新待办项 | `PUT /api/task` | `UpdateTodo` | `id, title?, priority?` | +| 切换完成状态 | `PATCH /api/task/{id}/toggle` | `ToggleTodoComplete` | `id: number` | +| 删除待办项 | `DELETE /api/task/{id}` | `DeleteTodo` | `id: number` | +| 获取子待办项 | `GET /api/task/{pid}/subtasks` | `ListSubTodos` | `parentTaskId: number` | + +--- + +## 五、返回格式差异 + +### Dynamic API 返回 + +```json +{ + "success": true, + "data": [{ "id": 1, "title": "...", ... }], + "message": "", + "errors": [] +} +``` + +### MCP Tool 返回 + +MCP 工具返回 `string` 类型,分两种格式: + +**列表类**(可读文本): +``` +未完成待办项(共 2 项): +[1] 完成报告 | 优先级:High | 进行中 +[3] 买菜 | 优先级:Low | 进行中 | 父ID:2 +``` + +**单条/创建/更新类**(JSON): +```json +{ + "id": 1, + "title": "完成报告", + "priority": "High", + "isCompleted": false, + "createdAt": "2026-06-16T08:00:00Z", + "updatedAt": "2026-06-16T08:00:00Z", + "parentTaskId": null, + "subTasks": [] +} +``` + +> 前端如果需要结构化数据,建议仍使用 Dynamic API;MCP 通道主要用于 AI 场景和文本交互。 + +--- + +## 六、典型场景 + +### 6.1 AI 对话式操作 + +用户通过 AI 助手(接入 MCP 的 LLM 客户端)用自然语言操作待办项: + +``` +用户:帮我看看还有哪些事没做完 +AI:→ 调用 ListActiveTodos +AI:您有 2 项未完成的待办:[1] 完成报告 [3] 买菜 + +用户:把"完成报告"标为已完成 +AI:→ 调用 ToggleTodoComplete(id=1) +AI:已将"完成报告"标记为完成 +``` + +### 6.2 语音指令 + +语音 → STT → 文本 → LLM 解析意图 → MCP Tool 调用 → TTS 播报结果。 + +### 6.3 外部工具集成 + +IDE 插件、自动化脚本等通过 MCP 协议直接操作待办项,无需理解 HTTP API 细节。 + +--- + +## 七、注意事项 + +1. **MCP 无状态模式**:当前配置为 Stateless,不支持服务端→客户端通知;如需实时推送仍走 Dynamic API 或 WebSocket +2. **CORS**:开发环境 `AllowAll` 策略已覆盖 `/mcp`;生产环境需按需配置 +3. **认证**:当前 MCP 端点未接入认证中间件;如需鉴权,需在 `MapMcpServer` 后追加 `.RequireAuthorization()` +4. **生命周期**:MCP 客户端连接为长连接,建议在组件 `onUnmounted` 时调用 `disconnectMcp()` diff --git a/docs/project/研发工单-v1.3.0/00-工单总览.md b/docs/project/研发工单-v1.3.0/00-工单总览.md index 3ca3626..591d4c9 100644 --- a/docs/project/研发工单-v1.3.0/00-工单总览.md +++ b/docs/project/研发工单-v1.3.0/00-工单总览.md @@ -6,12 +6,13 @@ ## 一、背景与目标 -Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: +Hua.Todo v1.3.0 版本聚焦于三个核心能力的升级: | 序号 | 能力 | 描述 | |---|---|---| | 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCP(Model Context Protocol)服务,提升服务调用效率与可扩展性 | -| 2 | **语音交互** | 实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项 | +| 2 | **语音控制与 AI 辅助** | 通过语音指令操作 Todo 待办项(CRUD + 子任务),通过 LLM 提供 AI 辅助任务拆分建议 | +| 3 | **会议任务拆分** | 以"会议"为入口记录会议内容(录音/文字),通过 AI 分析自动生成待办项拆分建议,用户确认后批量创建 | --- @@ -20,11 +21,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: ### 2.1 并行工单(可同步执行) | 工单编号 | 标题 | 负责人 | 状态 | -|---|---|---|---| -| 01 | HTTP 服务转换为 MCP 服务 | - | 待开始 | -| 02 | 语音通话与语音控制功能 | - | 待开始 | +|---|---|---|---|---|---|---| +| 01 | HTTP 服务转换为 MCP 服务 | - | 进行中 | +| 02 | 语音控制与 AI 辅助 | - | 待开始 | +| 03 | 会议任务拆分 | - | 待开始 | +| 04 | 富文本描述、附件与外部链接 | - | 待开始 | -### 2.2 串行工单(依赖前置工单完成) +### 2.2 03 子工单拆分 + +| 子工单 | 标题 | 依赖 | 状态 | +|---|---|---|---| +| 03-01 | 会议数据模型与 API | 无 | 待开始 | +| 03-02 | 音频录制与转写 | 03-01 | 待开始 | +| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 待开始 | +| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 待开始 | + +### 2.3 串行工单(依赖前置工单完成) 当前版本暂无串行工单依赖。 @@ -45,19 +57,71 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: - MCP 服务可正常对外提供接口 - 所有原有 HTTP API 功能在 MCP 服务中可正常使用 -### 3.2 工单 02 - 语音通话与语音控制功能 +### 3.2 工单 02 - 语音控制与 AI 辅助 -**目标**:实现语音通话数据传输与语音控制功能 +**目标**:实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能 **核心需求**: -- 语音通话数据传输能力 -- 语音指令识别与解析 -- 语音控制 Todo 待办项操作(创建、编辑、删除、完成等) -- 与现有业务系统对接 +- 语音输入(STT):平台原生语音识别 → 文字 +- 语音播报(TTS):执行结果语音反馈 +- 语音指令解析与执行(CRUD + 子任务 + 歧义处理) +- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建) + +**不包含**: +- 语音通话功能(场景不明确,本期不做) **验收标准**: -- 语音通话功能可正常使用 -- 语音控制可准确执行 Todo 业务操作 +- 各平台 STT/TTS 可正常工作 +- 语音指令可准确执行 Todo 业务操作 +- 歧义场景返回候选列表而非直接执行 +- AI 拆分建议需用户确认后才创建子任务 + +### 3.3 工单 03 - 会议任务拆分 + +**目标**:以"会议"为入口,通过录音/文字记录会议内容,AI 自动提取行动项生成待办项建议 + +**核心需求**: +- 新增"会议"类型标记(TaskType.Meeting) +- 录音:前端 MediaRecorder → 后端 STT 转写 +- 文字:直接输入/粘贴会议纪要 +- AI 会议拆分(LLM 分析会议内容 → 待办项建议列表) +- 建议审阅与确认 UI(勾选、编辑、批量创建子任务) + +**不包含**: +- 实时语音转写(本期不做) +- 音频持久存储(转写完成后删除音频) + +**验收标准**: +- 可通过"会议"类型创建待办项,显示会议图标 +- 录音可正常录制并提交转写 +- 文字纪要可保存/编辑 +- AI 拆分返回 3-10 条结构化建议,含优先级和原因 +- 用户可审阅、勾选、编辑建议后批量创建为子任务 +- 离线模式拒绝 AI 拆分(提示降级) + +### 3.4 工单 04 - 富文本描述、附件与外部链接 + +**目标**:为 Todo 待办项新增多行描述、文件附件管理与外部程序/链接启动能力(仅桌面端) + +**核心需求**: +- `TaskEntity` 新增 `Description` 多行描述字段 +- 新增 `AttachmentEntity` 数据模型,支持本地文件上传、下载、删除 +- 支持外部链接(URL)作为附件,点击在默认浏览器打开 +- 桌面端通过系统关联程序打开本地附件(`Process.Start` / `xdg-open`) +- 前端编辑对话框扩展描述 textarea + 附件管理区域 + +**不包含**: +- 移动端附件管理(本期仅 Windows/Linux 桌面端) +- 附件云同步(后续版本规划) +- 附件预览(如图片缩略图,本期不做) +- 富文本编辑器(本期仅纯文本) + +**验收标准**: +- 描述字段可正常编辑和保存 +- 附件可上传、下载、删除,文件完整性校验 +- 外部链接可添加并在浏览器中打开 +- 本地附件可通过系统关联程序打开 +- 附件数量(20个)和大小(50MB)限制生效 --- @@ -68,9 +132,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: | 01 | MCP 服务契约文档生成 | 待验证 | - | | 01 | MCP 服务可用性测试 | 待验证 | - | | 01 | 原有 API 功能兼容性 | 待验证 | - | -| 02 | 语音通话连接测试 | 待验证 | - | +| 02 | STT/TTS 平台适配 | 待验证 | Windows 优先,其他平台后续 | | 02 | 语音指令识别准确率 | 待验证 | - | -| 02 | Todo 业务操作覆盖度 | 待验证 | - | +| 02 | 歧义处理正确性 | 待验证 | - | +| 02 | AI 拆分建议质量 | 待验证 | - | +| 02 | Todo 业务操作覆盖度 | 待验证 | CRUD + 子任务 + 取消完成 | +| 03 | 会议数据模型迁移 | 待验证 | TaskType 字段 + DB 迁移 | +| 03 | 录音与转写链路 | 待验证 | 录制 → 上传 → 转写 → 保存 | +| 03 | AI 会议拆分质量 | 待验证 | 建议含标题+优先级+原因 | +| 03 | 建议审阅与批量创建 | 待验证 | 勾选/编辑/确认后创建子任务 | +| 04 | 描述字段编辑与保存 | 待验证 | - | +| 04 | 附件上传/下载/删除 | 待验证 | - | +| 04 | 外部链接添加与打开 | 待验证 | - | +| 04 | 本地文件通过系统程序打开 | 待验证 | Process.Start / xdg-open | +| 04 | 附件数量/大小限制 | 待验证 | 20 个 / 50MB | +| 04 | 待办项删除时附件级联清理 | 待验证 | - | +| 04 | 跨平台编辑安全(移动端不覆盖描述/附件) | 待验证 | 桌面设值 → 移动端改标题 → 桌面验证不丢失 | --- @@ -79,8 +156,21 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: | 决策点 | 结论 | |---|---| | MCP 框架选择 | 使用 TRAE 平台内置的 MCP 服务框架 | -| 语音识别方案 | 集成平台语音识别能力 | -| 服务注册方式 | 遵循平台标准注册流程 | +| STT/TTS 分层 | 接口在 Core,实现在各平台目录(同全局快捷键模式) | +| 语音指令解析 | A+C 混合方案:在线走 LLM 意图解析(LlmIntentParser),离线降级到规则匹配(RuleIntentParser),双策略通过 HybridVoiceIntentParser 自动切换 | +| 歧义处理策略 | 目标不唯一时返回候选列表;LLM confidence < 0.8 时触发确认;confidence < 0.5 按 UNKNOWN 处理 | +| AI 拆分安全性 | LLM 调用在 Host 端,建议需用户确认后才创建 | +| LLM 复用 | 意图解析与 AI 拆分共用同一 LLM 基础设施(LlmClientService) | +| 语音通话 | 本期不做,场景不明确 | +| 会议类型 | 新增 `TaskType` 枚举区分普通待办项与会议,会议有专属录音/纪要/拆分 UI | +| 录音存储 | 转写完成后删除原始音频文件,节省空间 | +| 音频转写 | 优先服务端 Whisper API;后续补各平台原生 STT | +| LLM 拆分 prompt | 会议专用 prompt,侧重"提取行动项",输出优先级+原因 | +| 工单 02 复用 | `LlmClientService` 直接复用;会议拆分 prompt 独立于语音意图解析 | +| 多入口覆盖规则 | 后续每项新增功能必须在需求阶段确认 UI 入口 + 语音控制入口覆盖情况,详见 [05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md) | +| 附件存储策略 | 附件存储在应用数据目录 `Attachments/` 子目录;外部链接不复制文件仅存 URL;单文件 50MB / 每待办项 20 个上限 | +| 外部程序启动 | 通过 `IPlatformAttachmentOpener` 接口实现平台差异:Windows 用 `Process.Start`,Linux 用 `xdg-open` | +| 工单 04 与 03 共享文件 | `TaskEntity.cs`、`TodoDbContext.cs`、`task.ts`、`TaskEditDialog.vue` 为共享文件,工单 03 先写入,04 后续追加 | --- @@ -89,8 +179,11 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: | 依赖项 | 状态 | 来源 | |---|---|---| | TRAE MCP SDK | 已就绪 | 平台内置 | -| 语音识别服务 | 已就绪 | 平台内置 | +| 各平台原生 STT/TTS API | 已就绪 | 平台内置 | | Hua.Todo v1.2.0 | 已完成 | 上一版本 | +| LLM API(AI 拆分) | 待确认 | Host 端调用 | +| 浏览器 MediaRecorder API | 已就绪 | 前端录音 | +| 工单 02 LlmClientService | 待实现 | 会议拆分复用 | --- @@ -99,9 +192,15 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级: | 风险 | 影响 | 应对策略 | |---|---|---| | MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 | -| 语音识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 | +| 平台 STT 识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 | +| Linux STT 可用性差 | Linux 语音控制不可用 | Web Speech API 降级;或 `vosk` 离线模型 | +| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,语音指令其他部分不受影响 | +| 会议录音文件过大 | 上传超时/存储压力 | 前端限制最长 2 小时;压缩音频格式 | +| 浏览器 MediaRecorder 兼容性 | 部分平台录音不可用 | 降级提示使用文字输入 | +| STT 转写准确率不足 | 会议纪要质量差 | 转写后支持用户编辑修正 | --- **创建日期**:2026-06-15 +**修订日期**:2026-06-16 **版本**:v1.3.0 diff --git a/docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md b/docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md index 6c59aa7..3ebca6b 100644 --- a/docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md +++ b/docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md @@ -1,23 +1,25 @@ -# 研发工单 v1.3.0 - 02 语音通话与语音控制功能 +# 研发工单 v1.3.0 - 02 语音控制与 AI 辅助 --- ## 一、目标与范围 ### 1.1 目标 -实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项,提升用户交互体验。 +实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能,通过平台原生 STT/TTS 实现语音输入输出,通过 LLM 意图解析处理自然语言指令,离线降级到规则匹配,通过 LLM 提供 AI 拆分建议。 ### 1.2 范围 **包含**: -- 语音通话数据传输能力 -- 语音指令识别与解析 -- 语音控制 Todo 待办项操作 -- 与现有业务系统对接 +- 语音输入(STT):平台原生语音识别 → 文字 +- 语音播报(TTS):执行结果语音反馈 +- 语音指令解析与执行(CRUD + 子任务操作) +- 指令歧义处理(目标不唯一时返回候选列表) +- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建) **不包含**: -- 语音通话 UI 界面设计(仅提供能力层) -- 第三方语音服务集成(使用平台内置能力) +- 语音通话功能(场景不明确,本期不做) +- 前端语音 UI 设计(仅提供能力层与 API) +- 第三方语音服务集成(使用平台原生能力) --- @@ -26,109 +28,426 @@ | 条件 | 说明 | |---|---| | Hua.Todo v1.2.0 | 已完成,提供基础业务能力 | -| 平台语音服务 | 已就绪 | -| 工单 01(可选) | MCP 服务就绪后可通过 MCP 调用 | +| 各平台原生 STT/TTS | Windows(`Windows.Media.SpeechRecognition`/`SpeechSynthesis`)、Android(`SpeechRecognizer`/`TextToSpeech`)、iOS/macOS(`SFSpeechRecognizer`/`AVSpeechSynthesizer`)、Linux(WebKitGTK Web Speech API / `vosk` 离线模型) | +| LLM API | 在线模式意图解析 + AI 拆分共用;API Key 在 Host 端管理 | +| 工单 01(可选) | MCP 服务就绪后,语音指令与 AI 拆分也可通过 MCP 暴露 | --- -## 三、需求规格 +## 三、架构设计 -### 3.1 语音通话数据传输 - -**功能描述**:支持语音通话数据的实时传输 - -**接口设计**: -``` -工具名:startVoiceCall -参数: - - target: string - 通话目标标识 -返回: - - callId: string - 通话 ID - - status: string - 通话状态(connected/disconnected) -``` +### 3.1 整体链路 ``` -工具名:endVoiceCall -参数: - - callId: string - 通话 ID -返回: - - success: boolean - 是否成功结束 +平台 STT(语音→文字) + ↓ +IVoiceInputService(Core 接口,各平台实现) + ↓ 回调文字到前端 +WebView → POST /api/voice/command { text: "帮我把那个开会的删了吧" } + ↓ +Host API → IVoiceIntentParser(双策略) + ├─ 在线:LlmIntentParser(调 LLM,输出结构化意图+参数) + └─ 离线:RuleIntentParser(关键词规则匹配,覆盖高频指令) + ↓ +意图 + 参数 → 判断歧义 + ├─ 无歧义 → 调用 TaskService 执行 → TTS 播报结果 + ├─ 有歧义 → 返回候选列表 → 前端展示 → 用户确认 → 再执行 + └─ UNKNOWN → TTS 播报"没听懂,请再说一次" ``` -### 3.2 语音指令控制 +### 3.2 STT/TTS 分层策略 -**功能描述**:支持通过语音指令操作 Todo 待办项 +遵循与全局快捷键相同的平台分离模式(接口+平台目录): -**支持的语音指令**: - -| 指令类型 | 示例指令 | 对应操作 | +| 层 | 职责 | 位置 | |---|---|---| -| 创建任务 | "创建任务 开会" | 创建标题为"开会"的任务 | -| 创建任务(带优先级) | "创建高优先级任务 提交报告" | 创建高优先级任务 | -| 完成任务 | "完成任务 开会" | 标记任务"开会"为已完成 | -| 删除任务 | "删除任务 开会" | 删除任务"开会" | -| 更新任务 | "更新任务 开会 改为 团队会议" | 更新任务标题 | -| 查询任务 | "查询未完成任务" | 获取未完成任务列表 | -| 添加子任务 | "给任务开会添加子任务 准备PPT" | 为任务添加子任务 | +| `IVoiceInputService` / `IVoiceOutputService` | 接口定义 | `src/Hua.Todo.Core/Services/` | +| MAUI 平台实现 | 各平台原生 STT/TTS | `src/Hua.Todo.Maui/Platforms/{Windows,Android,macOS,iOS}/VoiceInputService.cs` | +| Avalonia 平台实现 | Linux 桌面 STT/TTS | `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` | +| Web 降级方案 | 浏览器 Web Speech API | 前端 `voiceInput.ts` | + +### 3.3 意图解析双策略(A+C 方案) + +采用 **LLM 主解析 + 规则降级** 的混合方案: + +#### 策略 A:在线 — LLM 意图解析(LlmIntentParser) + +在线模式下,将用户原始文字发送给 LLM,通过 system prompt 约束输出为结构化 JSON: -**接口设计**: ``` -工具名:executeVoiceCommand -参数: - - command: string - 语音指令文本 -返回: - - success: boolean - 是否执行成功 - - message: string - 执行结果描述 - - data: object - 返回数据(如任务列表) +System Prompt(精简版): +你是 Hua.Todo 的语音指令解析器。根据用户输入,输出以下 JSON 格式: +{ + "intent": "CREATE|UPDATE|DELETE|COMPLETE|UNCOMPLETE|QUERY|ADD_SUBTASK|AI_BREAKDOWN|UNKNOWN", + "params": { ... }, + "confidence": 0.0-1.0 +} + +意图说明: +- CREATE: 创建任务,params: { title, priority? } +- UPDATE: 修改任务,params: { targetTitle, newTitle } +- DELETE: 删除任务,params: { targetTitle } +- COMPLETE: 完成任务,params: { targetTitle } +- UNCOMPLETE: 取消完成,params: { targetTitle } +- QUERY: 查询任务,params: { filter? } +- ADD_SUBTASK: 添加子任务,params: { parentTitle, subTitle } +- AI_BREAKDOWN: AI辅助拆分,params: { targetTitle } +- UNKNOWN: 无法识别 + +只输出 JSON,不要解释。 ``` -### 3.3 语音状态管理 - -**接口设计**: +**示例调用**: ``` -工具名:getVoiceStatus -参数:无 -返回: - - isListening: boolean - 是否正在监听 - - isSpeaking: boolean - 是否正在播报 +输入:"帮我把那个开会的任务删了吧" +LLM 输出: +{ + "intent": "DELETE", + "params": { "targetTitle": "开会" }, + "confidence": 0.95 +} ``` ``` -工具名:speakText -参数: - - text: string - 要播报的文本 -返回: - - success: boolean - 是否成功 +输入:"这个项目太大了,帮我拆一下" +LLM 输出: +{ + "intent": "AI_BREAKDOWN", + "params": { "targetTitle": "这个项目" }, + "confidence": 0.88 +} +``` + +**优势**: +- 无需维护同义词表,LLM 天然理解各种自然语言表述 +- 扩展新意图只需修改 prompt,不改代码 +- 歧义场景 LLM 也能判断(如 confidence < 阈值时触发确认) + +#### 策略 C:离线 — 规则匹配降级(RuleIntentParser) + +离线/嵌入式模式下,降级到关键词规则匹配,只覆盖高频指令: + +| 意图 | 匹配规则 | 示例 | +|---|---|---| +| `CREATE` | 正则:`/^(创建|新增|添加|加)\s*(任务|待办)?\s*(.+)/` | "创建任务 开会" | +| `DELETE` | 正则:`/^(删除|删掉|移除)\s*(任务|待办)?\s*(.+)/` | "删除任务 开会" | +| `COMPLETE` | 正则:`/^(完成|做完)\s*(任务|待办)?\s*(.+)/` | "完成任务 开会" | +| `UNCOMPLETE` | 正则:`/^(取消完成|重新打开|恢复)\s*(.+)/` | "取消完成 开会" | +| `UPDATE` | 正则:`/^(修改|更新|编辑)\s*(.+?)\s*(改为|改成|改成)\s*(.+)/` | "修改任务 开会 改为 团队会议" | +| `QUERY` | 正则:`/^(查询|查看|列出|显示)\s*(.+)/` | "查询未完成任务" | +| `ADD_SUBTASK` | 正则:`/^(给|为)\s*(.+?)\s*(添加|加)\s*(子任务|子项)?\s*(.+)/` | "给任务开会添加子任务 准备PPT" | +| `AI_BREAKDOWN` | 正则:`/^(帮我拆分|AI拆分|智能拆分)\s*(.+)/` | "帮我拆分 开会" | + +**降级策略**: +- 离线模式下 `AI_BREAKDOWN` 意图不可用(需要 LLM),TTS 播报"离线模式不支持 AI 拆分" +- 规则匹配失败时返回 `UNKNOWN` + +#### 策略切换逻辑 + +```csharp +/// 语音意图解析器接口 +public interface IVoiceIntentParser +{ + /// 解析语音指令文本,返回意图与参数 + Task ParseAsync(string text, CancellationToken ct = default); +} + +/// 双策略解析器:在线走 LLM,离线走规则 +public class HybridVoiceIntentParser : IVoiceIntentParser +{ + private readonly LlmIntentParser _llmParser; + private readonly RuleIntentParser _ruleParser; + private readonly IConnectivityService _connectivity; + + public async Task ParseAsync(string text, CancellationToken ct) + { + if (_connectivity.IsOnline) + { + try + { + var result = await _llmParser.ParseAsync(text, ct); + if (result.Intent != VoiceIntent.UNKNOWN) + return result; + } + catch (Exception) + { + // LLM 调用失败,降级到规则 + } + } + + return _ruleParser.Parse(text); + } +} +``` + +### 3.4 语音指令处理完整流程 + +``` +原始文字 → HybridVoiceIntentParser + ├─ 在线:LlmIntentParser(LLM 结构化输出) + └─ 离线/降级:RuleIntentParser(关键词正则匹配) + ↓ +意图 + 参数 + confidence + ├─ confidence >= 阈值 且 目标唯一 → 执行 → TTS 播报 + ├─ 目标匹配多个 → 返回候选列表 → 用户确认 → 执行 + ├─ confidence < 阈值 → TTS "您是要...吗?" → 用户确认 + └─ UNKNOWN → TTS "没听懂,请再说一次" ``` --- -## 四、验收标准 +## 四、需求规格 + +### 4.1 语音输入(STT) + +各平台通过原生 API 将语音转为文字,通过 `IVoiceInputService` 接口统一暴露: + +```csharp +/// 语音输入服务接口,各平台实现原生 STT +public interface IVoiceInputService +{ + /// 是否正在监听 + bool IsListening { get; } + + /// 开始语音识别,识别结果通过回调返回 + Task StartListeningAsync(Action onResult, Action? onError = null); + + /// 停止语音识别 + Task StopListeningAsync(); +} +``` + +### 4.2 语音播报(TTS) + +```csharp +/// 语音输出服务接口,各平台实现原生 TTS +public interface IVoiceOutputService +{ + /// 是否正在播报 + bool IsSpeaking { get; } + + /// 播报文本 + Task SpeakAsync(string text); + + /// 停止播报 + Task StopSpeakingAsync(); +} +``` + +### 4.3 语音指令解析 + +#### 意图定义 + +| 意图 (Intent) | 参数 | 说明 | +|---|---|---| +| `CREATE` | `title`(必填)、`priority`(可选:High/Medium/Low) | 创建任务 | +| `UPDATE` | `targetTitle`(必填)、`newTitle`(必填) | 修改任务标题 | +| `DELETE` | `targetTitle`(必填) | 删除任务 | +| `COMPLETE` | `targetTitle`(必填) | 标记完成 | +| `UNCOMPLETE` | `targetTitle`(必填) | 取消完成 | +| `QUERY` | `filter`(可选:如"未完成"、"高优先级") | 查询任务 | +| `ADD_SUBTASK` | `parentTitle`(必填)、`subTitle`(必填) | 添加子任务 | +| `AI_BREAKDOWN` | `targetTitle`(必填) | AI 辅助拆分(仅在线) | +| `UNKNOWN` | 无 | 无法识别 | + +#### 歧义处理规则 + +当指令中 `targetTitle` 匹配到多个 Todo 待办项时: +1. **不直接执行**,返回歧义结果(包含匹配的候选列表) +2. 前端展示候选项,用户点选或语音确认后再执行 +3. 匹配规则:标题完全匹配优先,包含匹配次之 + +#### 置信度阈值 + +- LLM 返回 `confidence >= 0.8`:直接执行 +- `0.5 <= confidence < 0.8`:确认后执行(TTS "您是要...吗?") +- `confidence < 0.5`:按 UNKNOWN 处理 + +### 4.4 语音指令 API + +``` +POST /api/voice/command +请求体: +{ + "text": "帮我把那个开会的删了吧" // STT 识别后的原始文字 +} +响应体: +{ + "intent": "DELETE", // 解析出的意图 + "params": { // 提取的参数 + "targetTitle": "开会" + }, + "confidence": 0.95, // 置信度 + "result": { // 执行结果 + "success": true, + "message": "已删除任务:开会", + "data": null + } +} +``` + +歧义响应: +```json +{ + "intent": "COMPLETE", + "params": { "targetTitle": "开会" }, + "confidence": 0.92, + "result": { + "success": false, + "message": "找到多个匹配的任务,请确认", + "ambiguity": true, + "candidates": [ + { "id": 1, "title": "开会" }, + { "id": 5, "title": "开会讨论方案" } + ] + } +} +``` + +用户确认后二次请求: +``` +POST /api/voice/command/confirm +请求体: +{ + "intent": "COMPLETE", + "targetId": 5 // 用户选择的候选 ID +} +``` + +### 4.5 AI 辅助任务拆分 + +``` +POST /api/voice/ai-breakdown +请求体: +{ + "taskId": 5 // 要拆分的目标任务 ID +} +响应体: +{ + "suggestions": [ // LLM 生成的子任务建议 + { "title": "准备会议议程", "priority": "Medium" }, + { "title": "发送会议邀请", "priority": "High" }, + { "title": "预定会议室", "priority": "Low" } + ] +} +``` + +用户确认后批量创建: +``` +POST /api/voice/ai-breakdown/confirm +请求体: +{ + "parentTaskId": 5, + "subTasks": [ // 用户勾选后的子任务列表 + { "title": "准备会议议程", "priority": "Medium" }, + { "title": "发送会议邀请", "priority": "High" } + ] +} +``` + +**关键约束**: +- LLM 调用在 Host 端执行,API Key 不暴露到客户端 +- 意图解析与 AI 拆分共用同一 LLM 基础设施 +- 建议≠直接执行,必须用户确认后才创建子任务 +- 离线模式下 AI 拆分不可用,TTS 播报"离线模式不支持 AI 拆分" +- MCP 服务就绪后,`aiBreakdown` 可同步暴露为 MCP 工具 + +--- + +## 五、验收标准 | 验收项 | 验证方法 | 预期结果 | |---|---|---| -| 创建任务指令 | 说出"创建任务 测试" | 成功创建标题为"测试"的任务 | -| 创建带优先级任务 | 说出"创建高优先级任务 紧急" | 成功创建高优先级任务 | -| 完成任务指令 | 说出"完成任务 测试" | 任务状态变为已完成 | -| 删除任务指令 | 说出"删除任务 测试" | 任务被成功删除 | -| 查询任务指令 | 说出"查询未完成任务" | 返回未完成任务列表 | -| 语音播报 | 调用 speakText | 成功播放指定文本 | -| 语音通话 | 调用 startVoiceCall | 通话连接成功 | +| STT 语音输入 | 说话后检查回调文字 | 文字识别正确 | +| TTS 语音播报 | 执行指令后检查播报 | 播报内容与执行结果一致 | +| 创建任务(在线) | "帮我把开会的任务加上" | LLM 解析为 CREATE,成功创建 | +| 创建任务(离线降级) | "创建任务 测试" | 规则匹配为 CREATE,成功创建 | +| 创建带优先级任务 | "建一个高优先级任务 紧急" | 成功创建高优先级任务 | +| 完成任务指令 | "把那个开会的事标完成" | LLM 解析为 COMPLETE,执行成功 | +| 取消完成指令 | "取消完成 测试" | 任务状态恢复为未完成 | +| 删除任务指令 | "帮我把测试删了" | LLM 解析为 DELETE,删除成功 | +| 更新任务指令 | "把测试改成验收" | 任务标题更新 | +| 查询任务指令 | "有哪些没做完的" | LLM 解析为 QUERY,返回列表 | +| 添加子任务指令 | "给开会加个子任务 准备PPT" | 子任务创建成功 | +| 歧义处理 | 多个任务含"开会"时说"完成开会" | 返回候选列表,不直接执行 | +| AI 拆分建议 | "帮我拆分 开会" | 返回子任务建议列表 | +| AI 拆分确认 | 用户勾选后确认 | 仅创建勾选的子任务 | +| 离线 AI 拆分 | 离线时说"帮我拆分" | 播报"离线模式不支持 AI 拆分" | +| 无法识别指令 | 说出无关内容 | 播报"没听懂,请再说一次" | +| LLM 降级 | 断网时使用语音 | 自动降级到规则匹配,基本 CRUD 可用 | --- -## 五、Touch List +## 六、Touch List | 文件路径 | 修改类型 | 说明 | |---|---|---| -| `src/Hua.Todo.Application/Voice/` | 新增 | 语音服务目录 | -| `src/Hua.Todo.Application/Voice/VoiceService.cs` | 新增 | 语音服务实现 | -| `src/Hua.Todo.Application/Voice/VoiceCommandParser.cs` | 新增 | 语音指令解析器 | -| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO | -| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 服务注册扩展 | +| `src/Hua.Todo.Core/Services/IVoiceInputService.cs` | 新增 | STT 接口定义 | +| `src/Hua.Todo.Core/Services/IVoiceOutputService.cs` | 新增 | TTS 接口定义 | +| `src/Hua.Todo.Core/Services/IVoiceIntentParser.cs` | 新增 | 意图解析器接口 | +| `src/Hua.Todo.Application/Voice/LlmIntentParser.cs` | 新增 | LLM 意图解析器(在线策略) | +| `src/Hua.Todo.Application/Voice/RuleIntentParser.cs` | 新增 | 规则匹配解析器(离线降级策略) | +| `src/Hua.Todo.Application/Voice/HybridVoiceIntentParser.cs` | 新增 | 双策略解析器(在线走 LLM,离线走规则) | +| `src/Hua.Todo.Application/Voice/VoiceCommandIntent.cs` | 新增 | 意图枚举定义 | +| `src/Hua.Todo.Application/Voice/VoiceCommandExecutor.cs` | 新增 | 意图执行器(分发到 TaskService) | +| `src/Hua.Todo.Application/Voice/VoiceController.cs` | 新增 | 语音指令 API 端点(`/api/voice/command`、`/api/voice/ai-breakdown`) | +| `src/Hua.Todo.Application/Voice/AiBreakdownService.cs` | 新增 | AI 辅助拆分服务(调用 LLM 生成建议) | +| `src/Hua.Todo.Application/Voice/LlmClientService.cs` | 新增 | LLM 调用封装(意图解析 + AI 拆分共用) | +| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO(VoiceIntentResult、VoiceCommandRequest 等) | +| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 语音服务 DI 注册 | +| `src/Hua.Todo.Maui/Platforms/Windows/VoiceInputService.cs` | 新增 | Windows STT 实现 | +| `src/Hua.Todo.Maui/Platforms/Windows/VoiceOutputService.cs` | 新增 | Windows TTS 实现 | +| `src/Hua.Todo.Maui/Platforms/Android/VoiceInputService.cs` | 新增 | Android STT 实现 | +| `src/Hua.Todo.Maui/Platforms/Android/VoiceOutputService.cs` | 新增 | Android TTS 实现 | +| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceInputService.cs` | 新增 | macOS STT 实现 | +| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceOutputService.cs` | 新增 | macOS TTS 实现 | +| `src/Hua.Todo.Maui/Platforms/iOS/VoiceInputService.cs` | 新增 | iOS STT 实现 | +| `src/Hua.Todo.Maui/Platforms/iOS/VoiceOutputService.cs` | 新增 | iOS TTS 实现 | +| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` | 新增 | Linux STT 实现 | +| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceOutputService.cs` | 新增 | Linux TTS 实现 | +| `src/Hua.Todo.Web/src/api/voice.ts` | 新增 | 前端语音 API 模块 | +| `src/Hua.Todo.Web/src/composables/useVoiceInput.ts` | 新增 | 前端语音输入组合式函数(含 Web Speech API 降级) | --- **工单编号**:02 -**标题**:语音通话与语音控制功能 +**标题**:语音控制与 AI 辅助 **版本**:v1.3.0 +**修订日期**:2026-06-16 + +--- + +## 七、与其他工单的语音入口衔接 + +### 7.1 背景 + +Hua.Todo 目前有两个功能入口:**UI 入口**(WebView / 前端界面)和**语音控制入口**(本工单产出)。后续每项新增功能在需求阶段都必须确认这两个入口的覆盖情况。 + +### 7.2 工单 03(会议任务拆分)的语音入口 + +工单 03 的会议功能需要通过语音控制入口提供以下指令: + +| 语音指令 | 意图 | 参数 | 说明 | +|---|---|---|---| +| "新建会议 项目评审" | `CREATE_MEETING` | `title`(必填)、`priority`(可选) | 创建会议类型任务(TaskType.Meeting) | +| "记录会议内容" | `RECORD_MEETING` | `targetTitle`(必填) | 开始录制当前会议 | +| "停止录制" | `STOP_RECORDING` | 无 | 停止录制,触发转写 | +| "帮我拆分这个会议" | `AI_BREAKDOWN` | `targetTitle`(必填,目标为会议类型) | 对会议内容进行 AI 拆分(复用 AI_BREAKDOWN 意图,参数携带 TaskType=Meeting) | +| "查看会议待办项" | `QUERY` | `filter="会议"` | 查询所有会议类型任务 | + +**关键点**: +- `AI_BREAKDOWN` 意图需扩展以区分普通任务拆分和会议拆分(通过 `TaskType` 字段),会议拆分的 prompt 侧重"提取行动项" +- `CREATE_MEETING` 是 `CREATE` 意图的子类型,创建时字段 `taskType = Meeting`,需要 LLM intent parser 的 prompt 增加说明 +- 录音控制(`RECORD_MEETING` / `STOP_RECORDING`)是新意图,对应前端 MediaRecorder 的启动/停止 + +### 7.3 语音入口覆盖检查表 + +后续每个新增功能/工单,智能体必须在需求讨论阶段输出以下检查表: + +| 检查项 | 状态 | 备注 | +|---|---|---| +| UI 入口是否已规划 | ✅ / ❌ | 描述 UI 入口 | +| 语音控制入口是否已规划 | ✅ / ❌ | 描述对应的语音指令与意图 | +| 当前不可覆盖的入口 | 说明原因 | 如:语音控制暂不支持 XX 操作(选项过多/需可视化交互) | + +此规则已固化为项目规范,详见 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md)。 diff --git a/docs/project/研发工单-v1.3.0/03-01-会议数据模型与API.md b/docs/project/研发工单-v1.3.0/03-01-会议数据模型与API.md new file mode 100644 index 0000000..9f705f9 --- /dev/null +++ b/docs/project/研发工单-v1.3.0/03-01-会议数据模型与API.md @@ -0,0 +1,241 @@ +# 研发工单 v1.3.0 - 03-01 会议数据模型与 API + +> 父工单:[03-会议任务拆分](./03-会议任务拆分.md) + +--- + +## 一、目标 + +为"会议任务拆分"功能建立数据模型:扩展 `TaskEntity` 新增 `TaskType` 枚举和 `MeetingNotes`/`AudioDuration` 字段,定义完整的会议相关 API 端点与 DTO,完成数据库迁移。 + +## 二、范围 + +**包含**: +- 新增 `TaskType` 枚举(Normal / Meeting) +- `TaskEntity` 新增字段:`TaskType`、`MeetingNotes`、`AudioDuration` +- EF Core 数据库迁移 +- DTO 扩展:`CreateTaskDto`、`UpdateTaskDto`、`TaskDto` 新增对应字段 +- 会议 API 端点骨架:`MeetingController` + `MeetingService` +- API 路径:`/api/meeting/{taskId}/transcribe`、`/api/meeting/{taskId}/notes` +- 会议相关 DTO 定义:`TranscribeRequest`、`TranscribeResponse`、`MeetingNotesRequest` + +**不包含**: +- AI 拆分逻辑(由 03-03 完成) +- STT 转写实现(由 03-02 完成) +- 前端组件(由 03-04 完成) + +## 三、前置条件 + +| 条件 | 说明 | +|---|---| +| Hua.Todo v1.2.0 | 基础 Task CRUD 能力、父子任务、DynamicApi 中间件 | +| ABP 基类迁移(可选) | 若工单 09(v1.2.0)已完成,TaskEntity 已转为 ABP 基类,字段新增方式不同 | + +## 四、详细规格 + +### 4.1 TaskType 枚举 + +```csharp +/// 待办项类型,区分普通待办项与会议 +public enum TaskType +{ + /// 普通待办项(默认) + Normal = 0, + /// 会议:包含录音/纪要,支持 AI 任务拆分 + Meeting = 1 +} +``` + +文件位置:`src/Hua.Todo.Core/Entities/TaskType.cs` + +### 4.2 TaskEntity 新增字段 + +```csharp +/// 待办项类型 +public TaskType TaskType { get; set; } = TaskType.Normal; + +/// 会议纪要/转写文字(仅 Meeting 类型有值) +[MaxLength(20000)] +public string? MeetingNotes { get; set; } + +/// 录音时长(秒),仅 Meeting 类型有值 +public double? AudioDuration { get; set; } +``` + +### 4.3 EF Core 配置(TodoDbContext.cs) + +```csharp +builder.Entity(b => +{ + // ... 现有配置 ... + + b.Property(x => x.TaskType) + .HasDefaultValue(TaskType.Normal) + .HasConversion(); // 枚举存为整数 + + b.Property(x => x.MeetingNotes) + .HasMaxLength(20000); // 最多约 20000 字符 + + b.Property(x => x.AudioDuration) + .IsRequired(false); +}); +``` + +### 4.4 数据库迁移 + +``` +dotnet ef migrations add AddMeetingFieldsToTasks +``` + +迁移应在 `Hua.Todo.Application/Migrations/` 目录生成。 + +### 4.5 DTO 扩展 + +**CreateTaskDto** 新增: +```csharp +/// 待办项类型(0=Normal, 1=Meeting),默认 Normal +public TaskType TaskType { get; set; } = TaskType.Normal; +``` + +**TaskDto** 新增: +```csharp +/// 待办项类型 +public TaskType TaskType { get; set; } +/// 会议纪要/转写文字 +public string? MeetingNotes { get; set; } +/// 录音时长(秒) +public double? AudioDuration { get; set; } +``` + +### 4.6 会议 DTO + +```csharp +/// 转写请求 +public class TranscribeRequest +{ + public IFormFile Audio { get; set; } = null!; + public string? Format { get; set; } +} + +/// 转写响应 +public class TranscribeResponse +{ + public int TaskId { get; set; } + public string Transcript { get; set; } = string.Empty; + public double AudioDuration { get; set; } +} + +/// 保存会议纪要请求 +public class MeetingNotesRequest +{ + public string Notes { get; set; } = string.Empty; +} + +/// 会议纪要响应 +public class MeetingNotesResponse +{ + public int TaskId { get; set; } + public string MeetingNotes { get; set; } = string.Empty; +} +``` + +### 4.7 API 端点 + +#### POST /api/meeting/{taskId}/transcribe + +- 接收:multipart/form-data(音频文件) +- 返回:`TranscribeResponse` +- 业务:音频上传后异步转写,结果存回 `TaskEntity.MeetingNotes` +- 当前骨架:返回占位文字,具体转写逻辑由 03-02 实现 + +#### POST /api/meeting/{taskId}/notes + +- 接收:`MeetingNotesRequest` +- 返回:`MeetingNotesResponse` +- 业务:保存/更新会议纪要 + +### 4.8 MeetingService 骨架 + +```csharp +/// 会议业务服务 +public class MeetingService +{ + private readonly ITaskRepository _taskRepo; + + public MeetingService(ITaskRepository taskRepo) + { + _taskRepo = taskRepo; + } + + /// 验证 taskId 对应的待办项存在且为 Meeting 类型 + public async Task GetMeetingTaskOrThrow(int taskId) + { + var task = await _taskRepo.GetAsync(taskId); + if (task == null) + throw new NotFoundException($"任务 {taskId} 不存在"); + if (task.TaskType != TaskType.Meeting) + throw new BusinessException($"任务 {taskId} 不是会议类型"); + return task; + } + + /// 保存会议纪要 + public async Task SaveNotes(int taskId, string notes) + { + var task = await GetMeetingTaskOrThrow(taskId); + task.MeetingNotes = notes; + task.UpdatedAt = DateTime.UtcNow; + await _taskRepo.UpdateAsync(task); + return task; + } + + /// 保存转写结果与音频时长 + public async Task SaveTranscript(int taskId, string transcript, double audioDuration) + { + var task = await GetMeetingTaskOrThrow(taskId); + task.MeetingNotes = transcript; + task.AudioDuration = audioDuration; + task.UpdatedAt = DateTime.UtcNow; + await _taskRepo.UpdateAsync(task); + return task; + } +} +``` + +## 五、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| TaskType 枚举可用 | 编译通过 | `TaskType.Normal` / `TaskType.Meeting` 可正常赋值 | +| DB 迁移可执行 | `dotnet ef database update` | `Tasks` 表新增 `TaskType`/`MeetingNotes`/`AudioDuration` 列 | +| 创建 Meeting 类型任务 | `POST /api/task` 带 `taskType:1` | 返回的 TaskDto 中 `taskType=1` | +| 保存会议纪要 | `POST /api/meeting/{id}/notes` | `meetingNotes` 字段更新成功 | +| 非 Meeting 类型调用会议 API | 普通任务调 `/api/meeting/{id}/notes` | 返回业务异常 | +| 现有代码兼容 | 运行所有已有测试 | 不破坏现有业务 | + +## 六、风险 + +| 风险 | 影响 | 应对 | +|---|---|---| +| TaskEntity 已重构为 ABP 基类 | 字段新增方式不同 | 通过 ABP 的 `ExtraProperties` 或标准字段新增,迁移方式略有调整 | +| 字段长度限制 | 超长会议纪要截断 | `MeetingNotes` 设 `MaxLength(20000)`,前端也做长度限制 | + +## 七、Touch List + +| 文件路径 | 修改类型 | 是否共享 | +|---|---|---| +| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 | +| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 | +| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 | +| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 | +| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 | +| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 | +| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 新增 | 否 | +| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 | +| `Migrations/AddMeetingFieldsToTasks.cs` | 新增 | 是 | + +--- + +**工单编号**:03-01 +**标题**:会议数据模型与 API +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/docs/project/研发工单-v1.3.0/03-02-音频录制与转写.md b/docs/project/研发工单-v1.3.0/03-02-音频录制与转写.md new file mode 100644 index 0000000..d74f7d7 --- /dev/null +++ b/docs/project/研发工单-v1.3.0/03-02-音频录制与转写.md @@ -0,0 +1,199 @@ +# 研发工单 v1.3.0 - 03-02 音频录制与转写 + +> 父工单:[03-会议任务拆分](./03-会议任务拆分.md) +> +> 依赖:03-01(API 契约) + +--- + +## 一、目标 + +实现前端音频录制(MediaRecorder API)和后端音频转写(STT)能力,打通"录音 → 上传 → 转写文字 → 保存为会议纪要"的完整链路。 + +## 二、范围 + +**包含**: +- 前端录音控件(`AudioRecorder.vue`) +- 前端录音组合式函数(`useAudioRecorder.ts`) +- 录音状态管理(录制中/暂停/停止/时长显示) +- 音频上传 API 交互(POST multipart/form-data) +- 后端音频文件临时存储 +- 后端 STT 转写服务(`SttService.cs`) +- 转写结果回写 `MeetingNotes` + +**不包含**: +- 实时转写(边录边转) +- 音频持久存储(转写完成后删除音频文件) +- 音频云同步 +- 语音指令输入(属于工单 02) + +## 三、前置条件 + +| 条件 | 说明 | +|---|---| +| 03-01 完成 | `MeetingController`、`MeetingService` 基础骨架就绪 | + +## 四、详细规格 + +### 4.1 前端录音控件 + +**AudioRecorder.vue**: + +``` +┌────────────────────────────────┐ +│ 🎤 会议录音 │ +│ │ +│ ● 录制中... 00:15:23 │ +│ ┌──────────────────────────┐ │ +│ │ ▁▃▂▄▅▂▁▃▄▅▃▂▁▂▄▅▃▁ │ │ ← 简易波形 +│ └──────────────────────────┘ │ +│ │ +│ [⏹ 停止录音] │ +│ │ +│ 录音时长限制:最长 2 小时 │ +└────────────────────────────────┘ +``` + +状态: +- **就绪**:显示录音按钮 +- **录制中**:显示停止按钮 + 时长计时 + 简易波形条 +- **已停止**:显示"提交转写" / "重新录制" + +### 4.2 useAudioRecorder 组合式函数 + +```typescript +/// 音频录制器组合式函数,封装 MediaRecorder API +export function useAudioRecorder() { + // 状态 + const isRecording = ref(false) + const isPaused = ref(false) + const duration = ref(0) // 秒 + const audioBlob = ref(null) + const audioUrl = ref(null) // 用于预览播放 + + // 方法 + async function startRecording(): Promise // 请求麦克风权限,开始录制 + function stopRecording(): void // 停止录制,生成 Blob + function resetRecording(): void // 重置状态 + function getAudioBlob(): Blob | null // 获取录制结果 + + // 内部 + let mediaRecorder: MediaRecorder | null = null + let timerInterval: number | null = null + + // 音频格式:webm(Chrome/Firefox)、mp4(Safari) + // 时长限制:最长 2 小时(7200 秒) + + return { isRecording, isPaused, duration, audioBlob, audioUrl, + startRecording, stopRecording, resetRecording, getAudioBlob } +} +``` + +### 4.3 前端 API 模块 + +```typescript +// src/Hua.Todo.Web/src/api/meeting.ts + +/// 上传音频文件并请求转写 +async function transcribeAudio(taskId: number, audioBlob: Blob): Promise +{ + const formData = new FormData(); + formData.append('audio', audioBlob, 'meeting.webm'); + + const apiBaseUrl = window.__API_BASE_URL__ || 'http://localhost:5173/api'; + const resp = await fetch(`${apiBaseUrl}/meeting/${taskId}/transcribe`, { + method: 'POST', + body: formData + }); + if (!resp.ok) throw new Error(`转写请求失败: ${resp.status}`); + return resp.json(); +} +``` + +### 4.4 后端 SttService + +```csharp +/// 语音转写服务接口 +public interface ISttService +{ + /// 将音频文件转写为文字 + /// 转写文字 + Task TranscribeAsync(Stream audioStream, string format, CancellationToken ct = default); +} +``` + +实现策略(按优先级): + +| 平台/环境 | 实现方式 | 说明 | +|---|---|---| +| Windows MAUI | `Windows.Media.SpeechRecognition` 文件识别 | 系统自带,离线可用 | +| macOS/iOS | `SFSpeechRecognizer` 文件识别 | 需在线 | +| Android | `SpeechRecognizer` | 需在线 | +| Linux | WebKit 在线;本地 `whisper.cpp` 降级 | 多策略 | +| 通用服务端 | 扩展 `LlmClientService` 调用 Whisper API | Host 端部署 | + +> 初期(v1.3.0)优先实现服务端 Whisper API 调用方式(通过 `LlmClientService` 扩展),后续版本各平台原生逐补。 + +### 4.5 音频上传处理流程 + +``` +前端 AudioRecorder → stopRecording → Blob (webm/mp4) + ↓ +POST /api/meeting/{taskId}/transcribe (multipart/form-data) + ↓ +MeetingController.Transcribe() + ↓ 保存音频临时文件到 meetings/ 目录 + ↓ 调用 ISttService.TranscribeAsync() + ↓ 得到文字结果 + ↓ 删除临时音频文件 + ↓ 调用 MeetingService.SaveTranscript() 保存到数据库 + ↓ +返回 TranscribeResponse { taskId, transcript, audioDuration } +``` + +### 4.6 错误处理 + +| 场景 | 处理 | +|---|---| +| 音频格式不支持 | 返回 400 "不支持的音频格式,支持 webm/wav/mp3" | +| 音频文件太大 | 返回 400 "音频文件过大,请控制录音在 2 小时以内" | +| STT 服务不可用 | 返回 503 "转写服务暂不可用,请稍后重试" | +| taskId 不是会议类型 | 返回 400 "该待办项不是会议类型" | + +## 五、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| 录音按钮可用 | 点击录音按钮 | 浏览器弹出麦克风权限请求 | +| 录制过程 | 授权后开始录制 | 显示录音时长,波形条有变化 | +| 停止录音 | 点击停止按钮 | 时长停止,显示"提交转写"按钮 | +| 重新录制 | 点击"重新录制" | 状态重置,可再次录制 | +| 上传转写 | 提交录音文件 | 返回转写文字 | +| 纪要保存 | 转写完成后查看任务 | `meetingNotes` 字段有转写文字 | +| 权限拒绝 | 浏览器拒绝麦克风 | 提示"无法访问麦克风,请使用文字输入" | +| 浏览器不支持 | IE/Safari 旧版等 | 提示"当前浏览器不支持录音,请使用文字输入" | + +## 六、风险 + +| 风险 | 影响 | 应对 | +|---|---|---| +| Safari 不支持 webm | 无法录制 | 使用 mp4 格式(Safari 支持);MIME type 自动适配 | +| STT 准确率不足 | 转写错误多 | 支持用户编辑修正转写结果 | +| 长音频转写耗时长 | 用户等待 | 前端显示转写进度或"转写中"加载状态 | + +## 七、Touch List + +| 文件路径 | 修改类型 | +|---|---| +| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 | +| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 | +| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 | +| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 | +| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 | + +--- + +**工单编号**:03-02 +**标题**:音频录制与转写 +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/docs/project/研发工单-v1.3.0/03-03-AI任务拆分服务.md b/docs/project/研发工单-v1.3.0/03-03-AI任务拆分服务.md new file mode 100644 index 0000000..52e8f9d --- /dev/null +++ b/docs/project/研发工单-v1.3.0/03-03-AI任务拆分服务.md @@ -0,0 +1,281 @@ +# 研发工单 v1.3.0 - 03-03 AI 任务拆分服务 + +> 父工单:[03-会议任务拆分](./03-会议任务拆分.md) +> +> 依赖:03-01(API 契约)、工单 02(`LlmClientService`) + +--- + +## 一、目标 + +实现基于 LLM 的会议内容分析服务:接收会议纪要文字,通过 LLM prompt 工程提取行动项,生成结构化的待办项建议列表(含标题、优先级、理由)。 + +## 二、范围 + +**包含**: +- `MeetingAiBreakdownService`:会议文本 → LLM → 结构化建议列表 +- 会议拆分专用 prompt 设计与迭代 +- AI 拆分 API 端点:`POST /api/meeting/{taskId}/breakdown` +- 批量创建确认端点:`POST /api/meeting/{taskId}/breakdown/confirm` +- 在线可用性检测(离线时拒绝 AI 拆分请求) +- 复用工单 02 的 `LlmClientService` + +**不包含**: +- 单任务拆分(属于工单 02 `AI_BREAKDOWN` 意图) +- LLM 基础设施搭建(由工单 02 提供) +- 前端审阅 UI(由 03-04 完成) + +## 三、前置条件 + +| 条件 | 说明 | +|---|---| +| 03-01 完成 | `MeetingController` 骨架就绪 | +| 工单 02 `LlmClientService` | LLM 调用封装可用 | + +## 四、详细规格 + +### 4.1 MeetingAiBreakdownService + +```csharp +/// 会议 AI 拆分服务,将会议文字内容分析为待办项建议 +public class MeetingAiBreakdownService +{ + private readonly ILlmClientService _llmClient; + private readonly MeetingService _meetingService; + private readonly ITaskService _taskService; + + public MeetingAiBreakdownService( + ILlmClientService llmClient, + MeetingService meetingService, + ITaskService taskService) { ... } + + /// 分析会议内容,返回待办项建议列表 + public async Task> AnalyzeAsync( + int taskId, string? notes = null, CancellationToken ct = default) + { + // 1. 验证 taskId 为 Meeting 类型 + var meeting = await _meetingService.GetMeetingTaskOrThrow(taskId); + + // 2. 获取会议文字(参数优先,否则取数据库中的 meetingNotes) + var meetingText = notes ?? meeting.MeetingNotes; + if (string.IsNullOrWhiteSpace(meetingText)) + throw new BusinessException("会议内容为空,请先输入纪要或上传录音"); + + // 3. 构建 prompt,调用 LLM + var prompt = BuildBreakdownPrompt(meetingText, meeting.Title); + var response = await _llmClient.SendAsync(prompt, ct); + + // 4. 解析 LLM 返回的 JSON 为建议列表 + return ParseSuggestions(response); + } + + /// 确认建议并批量创建子任务 + public async Task ConfirmAndCreateAsync( + int taskId, List subTasks, CancellationToken ct = default) + { + // 1. 验证 taskId 为 Meeting 类型 + await _meetingService.GetMeetingTaskOrThrow(taskId); + + // 2. 批量创建子任务 + var created = new List(); + foreach (var item in subTasks) + { + var dto = new CreateTaskDto + { + Title = item.Title, + Priority = item.Priority, + ParentTaskId = taskId + }; + created.Add(await _taskService.CreateTaskAsync(dto)); + } + + return new BatchCreateResult { CreatedCount = created.Count, SubTasks = created }; + } +} +``` + +### 4.2 LLM Prompt 设计 + +``` +System Prompt: +你是专业的项目管理和会议纪要分析助手。根据用户提供的会议内容,提取所有需要 +后续行动的事项,生成待办项建议列表。 + +要求: +1. 每条建议包含标题(title)、优先级(priority)、原因(reason) +2. 标题应简洁明确(15 字以内),如"整理需求文档"、"安排评审会议" +3. 优先级:High(2)=紧急重要,Medium(1)=一般,Low(0)=可延迟 +4. 原因应解释为什么需要这个待办项,引用会议中的具体决策或讨论 +5. 只提取明确需要执行的事项,不要生成模糊或无来源的建议 +6. 建议数量根据会议内容合理判断,通常 3-10 条 +7. 如果会议内容无法提取出明确的行动项,返回空列表 + +输出格式(只输出 JSON,不要解释): +{ + "suggestions": [ + { + "title": "待办项标题", + "priority": 1, + "reason": "原因说明" + } + ] +} +``` + +### 4.3 DTO 定义 + +```csharp +/// 会议任务拆分建议 +public class MeetingTaskSuggestion +{ + /// 建议的待办项标题 + public string Title { get; set; } = string.Empty; + + /// 建议优先级 + public TaskPriority Priority { get; set; } = TaskPriority.Medium; + + /// 拆分原因(LLM 输出,用于 UI 展示) + public string Reason { get; set; } = string.Empty; +} + +/// 拆分请求 +public class BreakdownRequest +{ + /// 会议纪要文字(可选,不传则使用已保存的 meetingNotes) + public string? Notes { get; set; } +} + +/// 拆分响应 +public class BreakdownResponse +{ + public int TaskId { get; set; } + public List Suggestions { get; set; } = new(); +} + +/// 批量创建请求中的单个子任务 +public class SubTaskCreateItem +{ + public string Title { get; set; } = string.Empty; + public TaskPriority Priority { get; set; } = TaskPriority.Medium; +} + +/// 批量确认请求 +public class ConfirmBreakdownRequest +{ + public List SubTasks { get; set; } = new(); +} + +/// 批量创建结果 +public class BatchCreateResult +{ + public int CreatedCount { get; set; } + public List SubTasks { get; set; } = new(); +} +``` + +### 4.4 API 端点 + +#### POST /api/meeting/{taskId}/breakdown + +- 接收:`BreakdownRequest` +- 返回:`ApiResponse` +- 处理: + 1. 获取会议文字(参数优先于数据库) + 2. 调用 `MeetingAiBreakdownService.AnalyzeAsync()` + 3. 返回建议列表 +- 错误: + - 内容为空 → 400 "会议内容为空" + - 离线 → 503 "AI 拆分仅在线可用" + - taskId 非 Meeting 类型 → 400 + +#### POST /api/meeting/{taskId}/breakdown/confirm + +- 接收:`ConfirmBreakdownRequest` +- 返回:`ApiResponse` +- 处理: + 1. 验证 taskId 为 Meeting 类型 + 2. 逐条创建子任务(通过 `TaskService.CreateTaskAsync`) + 3. 返回创建结果 + +### 4.5 LLM 响应解析 + +```csharp +/// 解析 LLM 返回的 JSON 为建议列表 +private List ParseSuggestions(string llmResponse) +{ + // 1. 清理 LLM 响应(移除可能的 markdown 代码块包裹、前后空白) + var json = CleanJsonResponse(llmResponse); + + // 2. 反序列化 + var result = JsonSerializer.Deserialize(json); + + // 3. 验证 + if (result?.Suggestions == null || result.Suggestions.Count == 0) + return new List(); + + // 4. 过滤无效条目(标题为空) + return result.Suggestions + .Where(s => !string.IsNullOrWhiteSpace(s.Title)) + .Select(s => new MeetingTaskSuggestion + { + Title = s.Title.Trim(), + Priority = s.Priority, + Reason = s.Reason?.Trim() ?? string.Empty + }) + .ToList(); +} + +/// LLM 原始响应结构 +private class LlBreakdownResponse +{ + public List Suggestions { get; set; } = new(); +} +``` + +### 4.6 错误处理 + +| 场景 | HTTP 状态码 | message | +|---|---|---| +| 会议文字内容为空 | 400 | 会议内容为空,请先输入纪要或上传录音 | +| taskId 不存在 | 404 | 待办项不存在 | +| taskId 不是 Meeting 类型 | 400 | 该待办项不是会议类型 | +| LLM API 调用失败 | 502 | AI 拆分服务暂时不可用,请稍后重试 | +| LLM 返回格式异常 | 500 | AI 拆分结果解析失败 | +| 离线 | 503 | AI 拆分仅在线可用 | +| 确认时子任务列表为空 | 400 | 请至少选择一个待办项 | + +## 五、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| 会议内容分析 | 提交一段会议纪要文字 | 返回 3-10 条结构化建议 | +| 建议质量 | 检查建议的标题、优先级、原因 | 标题明确、优先级合理、原因引用会议内容 | +| 空内容拒绝 | 不传 notes 且 meetingNotes 为空 | 返回 400 | +| 非 Meeting 类型拒绝 | 传普通待办项 ID | 返回 400 | +| 在线依赖 | 离线时请求拆分 | 返回 503 | +| 批量创建 | 确认勾选的建议 | 子任务批量创建成功 | +| LLM 异常降级 | 模拟 LLM 调用失败 | 返回 502,不影响其他功能 | +| 空结果处理 | 会议内容无可提取的待办项 | 返回空建议列表 + 提示信息 | + +## 六、风险 + +| 风险 | 影响 | 应对 | +|---|---|---| +| LLM 返回格式不符 | 建议解析失败 | 增加 JSON 清理逻辑(移除 markdown 包裹、前后文本) | +| LLM Token 限制 | 长会议纪要超出上下文窗口 | 限制文本长度(如 4000 字);超长时截断并告知用户 | +| Prompt 需要迭代 | 建议质量不达预期 | prompt 作为配置项,支持热更新 | + +## 七、Touch List + +| 文件路径 | 修改类型 | +|---|---| +| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 | +| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 | +| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 修改 | + +--- + +**工单编号**:03-03 +**标题**:AI 任务拆分服务 +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/docs/project/研发工单-v1.3.0/03-04-任务建议与确认UI.md b/docs/project/研发工单-v1.3.0/03-04-任务建议与确认UI.md new file mode 100644 index 0000000..a9ce7db --- /dev/null +++ b/docs/project/研发工单-v1.3.0/03-04-任务建议与确认UI.md @@ -0,0 +1,295 @@ +# 研发工单 v1.3.0 - 03-04 任务建议与确认 UI + +> 父工单:[03-会议任务拆分](./03-会议任务拆分.md) +> +> 依赖:03-01、03-03(后端 API 就绪) + +--- + +## 一、目标 + +实现会议任务拆分的完整前端 UI:在 TaskItem 中为 Meeting 类型待办项提供录音/纪要/拆分入口,实现建议审阅与确认对话框,打通从会议内容输入到批量创建子任务的端到端用户体验。 + +## 二、范围 + +**包含**: +- Meeting 类型待办项的特殊 UI(图标、录音/纪要/拆分入口) +- 录音控件的嵌入(调用 03-02 的 `AudioRecorder.vue` + `useAudioRecorder`) +- 纪要输入区域(textarea 保存会议文字内容) +- AI 拆分请求与加载状态 +- 建议审阅对话框(`MeetingBreakdownDialog.vue`) +- 建议勾选、编辑、调整优先级、手动补充 +- 确认后批量创建子任务 +- 前端类型扩展(`taskType`、`meetingNotes`、`audioDuration`) + +**不包含**: +- 录音控件本身(由 03-02 提供) +- 后端逻辑(由 03-01、03-03 提供) + +## 三、前置条件 + +| 条件 | 说明 | +|---|---| +| 03-01 完成 | 后端 API 就绪(meeting 端点 + TaskType) | +| 03-02 完成 | AudioRecorder 组件和 useAudioRecorder 可用 | +| 03-03 完成 | breakdown/breakdown/confirm 端点可用 | + +## 四、详细规格 + +### 4.1 前端类型扩展 + +```typescript +// src/Hua.Todo.Web/src/types/task.ts + +/// 待办项类型 +type TaskType = 0 | 1; // 0=Normal, 1=Meeting + +interface Task { + // ... 已有字段 ... + /** 待办项类型 */ + taskType?: TaskType; + /** 会议纪要/转写文字 */ + meetingNotes?: string; + /** 录音时长(秒) */ + audioDuration?: number; +} + +interface CreateTaskDto { + title: string; + priority: TaskPriority; + parentTaskId?: number; + /** 待办项类型,默认 0 */ + taskType?: TaskType; +} +``` + +### 4.2 会议相关类型 + +```typescript +// 新增或扩展 meeting.ts 中的类型 + +/// 会议任务拆分建议 +interface MeetingTaskSuggestion { + title: string; + priority: TaskPriority; + reason: string; +} + +/// 拆分响应 +interface BreakdownResponse { + taskId: number; + suggestions: MeetingTaskSuggestion[]; +} + +/// 待确认的子任务项 +interface PendingSubTask { + title: string; + priority: TaskPriority; + /** 是否被用户勾选 */ + checked: boolean; + /** LLM 拆分的原始建议(含原因) */ + suggestion?: MeetingTaskSuggestion; + /** 是否为用户手动添加的条目 */ + isManual?: boolean; +} +``` + +### 4.3 TaskItem.vue 扩展 + +为 Meeting 类型的待办项显示额外操作入口: + +``` +┌──────────────────────────────────────────────┐ +│ 📋 ☐ 周一产品评审会 🔴 H ··· │ +│ 🎙 会议 · 📝 有纪要 · 0个子任务 │ +│ [🎤] [📝] [🤖] │ +│ ┌─ ✅ 整理需求文档 🔴 │ ← 已有子任务(若有) +│ └─ ☐ 安排评审会议 🟡 │ +└──────────────────────────────────────────────┘ + +[🎤] = 录音按钮 +[📝] = 打开纪要编辑区 +[🤖] = AI 拆分(需有会议内容) +``` + +条件渲染逻辑: +- `task.taskType === 1`(Meeting)时,显示会议图标 `🎙` 标签 +- 显示三个操作按钮在任务行右侧 +- 拆分子任务入口:点击 `[🤖]` → 触发 `handleAiBreakdown()` + +### 4.4 会议纪要编辑区(内联) + +点击 `[📝]` 后展开内联编辑区: + +``` +┌──────────────────────────────────────────────┐ +│ 📋 ☐ 周一产品评审会 │ +│ │ +│ 会议纪要: │ +│ ┌──────────────────────────────────────┐ │ +│ │ 今天的产品评审会主要讨论了三个议题: │ │ +│ │ 第一,确定 Q3 产品路线图... │ │ +│ │ 第二,... │ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +│ 录音时长:30分15秒 │ +│ │ +│ [🔊 开始录音] [🤖 AI拆分] [💾 保存纪要] │ +└──────────────────────────────────────────────┘ +``` + +### 4.5 AI 拆分流程(前端) + +```typescript +// 在 TaskItem.vue 或拆分逻辑中 + +async function handleAiBreakdown(task: Task) { + // 1. 检查是否有会议内容 + if (!task.meetingNotes) { + showToast('请先输入纪要或上传录音', 'warning'); + return; + } + + // 2. 调用 AI 拆分 + isLoading.value = true; + try { + const resp = await meetingApi.requestBreakdown(task.id); + if (resp.data.suggestions.length === 0) { + showToast('未从会议内容中提取到待办项,请确认纪要内容', 'info'); + return; + } + + // 3. 打开审阅对话框 + openBreakdownDialog(task, resp.data.suggestions); + } catch (err) { + showToast('AI拆分失败,请稍后重试', 'error'); + } finally { + isLoading.value = false; + } +} +``` + +### 4.6 MeetingBreakdownDialog.vue + +审阅确认对话框: + +``` +┌──────────────────────────────────────────────┐ +│ 会议任务拆分 — 周一产品评审会 [✕] │ +│ │ +│ AI 根据会议内容生成了以下待办项建议: │ +│ 请勾选需要创建的条目,可编辑标题和优先级。 │ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ ☑ 整理产品需求文档 [🔴 高 ▼] │ │ +│ │ 📝 会议中提到需要在本周五前完成 │ │ +│ │ │ │ +│ │ ☑ 安排技术方案评审 [🟡 中 ▼] │ │ +│ │ 📝 张三负责在下周三前输出技术方案 │ │ +│ │ │ │ +│ │ ☑ 跟进客户反馈 [🟢 低 ▼] │ │ +│ │ 📝 李四反馈了三个客户问题 │ │ +│ │ │ │ +│ │ ☐ 项目总结报告 [🟡 中 ▼] │ │ +│ │ 📝 月末需要汇总各模块进度 │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ [+ 手动添加待办项] │ +│ │ +│ 已选 3 项 │ +│ [取消] [✅ 确认创建 (3)] │ +└──────────────────────────────────────────────┘ +``` + +交互: +- 每条建议默认勾选 +- 点击标题可编辑(内联 input) +- 优先级下拉可调整 +- 取消勾选的条目不会被创建 +- `[+ 手动添加]` 新增空白条目 +- 底部显示已选数量 + +### 4.7 确认创建 + +```typescript +async function confirmBreakdown() { + const selected = pendingSubTasks.value + .filter(s => s.checked) + .map(s => ({ title: s.title, priority: s.priority })); + + if (selected.length === 0) { + showToast('请至少选择一个待办项', 'warning'); + return; + } + + isCreating.value = true; + try { + const resp = await meetingApi.confirmBreakdown(task.id, selected); + showToast(`已成功创建 ${resp.data.createdCount} 个待办项`, 'success'); + closeDialog(); + emit('updated'); // 刷新任务列表 + } catch (err) { + showToast('创建失败,请重试', 'error'); + } finally { + isCreating.value = false; + } +} +``` + +### 4.8 创建任务时的类型选择 + +在 `TaskList.vue` 的快速创建表单中增加类型选择: + +``` +┌──────────────────────────────────────────────┐ +│ [+ 新待办项] [_____输入标题_____] │ +│ 优先级: [🟡 中 ▼] 类型: [📋 普通 ▼] │ +│ [🎙 会议] │ +│ [+ 添加] │ +└──────────────────────────────────────────────┘ +``` + +默认类型为 `Normal`(普通),选择 `Meeting` 后创建的待办项为会议类型。 + +## 五、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| 创建 Meeting 类型 | 新建时选择"会议"类型 | 列表中显示 🎙 图标 | +| 会议操作按钮 | 展开 Meeting 类型任务 | 显示录音/纪要/拆分三个按钮 | +| 普通类型不显示 | 查看普通待办项 | 无会议相关按钮 | +| 录音入口 | 点击录音按钮 | 弹出/展开 AudioRecorder 组件 | +| 纪要编辑 | 输入会议纪要后保存 | `meetingNotes` 保存成功 | +| AI 拆分请求 | 点击 AI 拆分按钮 | 加载状态 → 审阅对话框弹出 | +| 无内容拒绝拆分 | 无纪要时点击拆分 | 提示"请先输入纪要" | +| 建议审阅 | 审阅对话框中勾选/取消/编辑 | 操作流畅,状态正确 | +| 手动添加 | 点击"+ 手动添加" | 新增空白条目,可编辑 | +| 批量创建 | 确认勾选的条目 | 子任务创建到父任务下 | +| 离线降级提示 | 离线时点击拆分 | 提示"AI 拆分仅在线可用" | +| 响应式 | 缩小浏览器窗口 | 对话框和控件正常显示 | + +## 六、风险 + +| 风险 | 影响 | 应对 | +|---|---|---| +| 组件嵌套复杂 | TaskItem 已有子任务递归,再加会议 UI 后层级深 | 会议功能用可选插槽方式注入,不影响普通任务渲染 | +| 录音与拆分同时操作 | 用户可能在转写期间又点拆分 | 操作按钮在加载中时禁用 | +| 大量建议渲染 | LLM 返回 10+ 条建议时列表长 | 审阅列表设最大高度 + 滚动;建议上限 15 条 | + +## 七、Touch List + +| 文件路径 | 修改类型 | +|---|---| +| `src/Hua.Todo.Web/src/types/task.ts` | 修改(新增 TaskType、meetingNotes、audioDuration) | +| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 | +| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增(由 03-02 产出,本工单集成) | +| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 | +| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改(Meeting 类型条件渲染) | +| `src/Hua.Todo.Web/src/components/TaskList.vue` | 修改(新建任务时类型选择) | + +--- + +**工单编号**:03-04 +**标题**:任务建议与确认 UI +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/docs/project/研发工单-v1.3.0/03-会议任务拆分.md b/docs/project/研发工单-v1.3.0/03-会议任务拆分.md new file mode 100644 index 0000000..c83c8df --- /dev/null +++ b/docs/project/研发工单-v1.3.0/03-会议任务拆分.md @@ -0,0 +1,341 @@ +# 研发工单 v1.3.0 - 03 会议任务拆分 + +> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。 + +--- + +## 一、目标与范围 + +### 1.1 目标 + +实现"会议内容 → AI 任务拆分"功能:用户创建一个标记为"会议"类型的 Todo 待办项作为父目录,通过录音或文字输入会议内容,由 AI 分析后生成待办项建议列表,用户确认后批量创建为子任务。 + +### 1.2 核心流程 + +``` +用户创建"会议"类型父任务(指定标题,如"周一产品评审会") + ↓ +选择输入方式: + ├─ 录音:录制会议音频 → 系统转写为文字 + └─ 文字:直接输入/粘贴会议纪要 + ↓ +AI 分析会议内容 → 生成待办项建议列表 +(每条建议含:标题、优先级、可选截止日期) + ↓ +用户审阅建议列表: + ├─ 勾选/取消勾选 + ├─ 编辑建议内容(修改标题、调整优先级) + └─ 手动补充新条目 + ↓ +确认 → 批量创建为父任务的子待办项 +``` + +### 1.3 范围 + +**包含**: +- "会议"类型标记:创建 Todo 时可指定为会议类型,UI 上以图标/标签区分 +- 音频录制:前端录音控件,录制会议音频 +- 音频转写:将录音发送至后端,调用 STT 服务转为文字 +- 文字输入:直接输入/粘贴会议纪要文本 +- AI 任务拆分:将会议文字内容发送给 LLM,生成结构化待办项建议 +- 建议审阅与确认 UI:用户勾选、编辑、补充建议后批量创建子任务 +- 与工单 02 的 LLM 基础设施复用(`LlmClientService`) + +**不包含**: +- 实时语音转写(边录边转,后续版本考虑) +- 多人协作/会议纪要共享 +- 音频文件持久存储(录音转写完成后不保留音频,节省空间;如需保留为后续版本需求) +- 会议录音的云同步(后续版本) + +--- + +## 二、前置条件 + +| 条件 | 说明 | 状态 | +|---|---|---| +| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 | +| 工单 02 LLM 基础设施 | `LlmClientService`、AI 拆分服务 | 待实现 | +| 浏览器 MediaRecorder API | 前端音频录制 | 已就绪(主流浏览器支持) | +| 后端 STT 服务 | 音频转文字(可复用工单 02 的 STT 能力或调用第三方 API) | 待实现 | + +--- + +## 三、子工单拆分 + +| 子工单 | 标题 | 依赖 | 可并行 | +|---|---|---|---| +| 03-01 | 会议数据模型与 API | 无 | 是 | +| 03-02 | 音频录制与转写 | 03-01(API 契约) | 部分(前端录音 UI 可并行) | +| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 部分(prompt 设计可并行) | +| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 否(依赖后端 API 就绪) | + +### 执行顺序建议 + +``` +03-01(模型与API) ──────┐ + ├──→ 03-04(确认UI) +03-02(录制与转写) ──────┤ + │ +03-03(AI拆分服务) ──────┘ +``` + +03-01、03-02、03-03 可部分并行推进;03-04 需等前三者 API 就绪后开始。 + +--- + +## 四、架构设计 + +### 4.1 整体架构 + +``` +┌──────────────────────────────────────────────────┐ +│ Vue 前端 │ +│ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ +│ │ 录音控件 │ │ 文字输入区 │ │ 建议审阅 │ │ +│ │ MediaRecorder│ │ Textarea │ │ 确认对话框 │ │ +│ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘ │ +│ │ │ │ │ +│ ▼ ▼ │ │ +│ ┌──────────────────────────────┐ │ │ +│ │ meetingApi (前端 API 模块) │ │ │ +│ └──────────────┬───────────────┘ │ │ +└─────────────────┼──────────────────────┼─────────┘ + │ HTTP │ + ▼ │ +┌─────────────────────────────────────────┤ +│ 后端 (Application 层) │ +│ ┌────────────┐ ┌─────────────────┐ │ +│ │ Meeting │ │ MeetingAi │ │ +│ │ Controller │ │ BreakdownService│ │ +│ └─────┬──────┘ └───────┬─────────┘ │ +│ │ │ │ +│ ┌─────▼──────┐ ┌──────▼─────────┐ │ +│ │ Meeting │ │ LlmClient │ │ +│ │ Service │ │ Service (复用02)│ │ +│ └─────┬──────┘ └────────────────┘ │ +│ │ │ +│ ┌─────▼──────┐ │ +│ │ STT 服务 │ │ +│ │ (转写音频) │ │ +│ └────────────┘ │ +└─────────────────────────────────────────┘ +``` + +### 4.2 数据模型扩展 + +在现有 `TaskEntity` 上新增 `TaskType` 字段,区分普通待办项与会议: + +```csharp +/// 待办项类型枚举 +public enum TaskType +{ + /// 普通待办项(默认) + Normal = 0, + /// 会议(可包含录音、纪要,支持 AI 任务拆分) + Meeting = 1 +} +``` + +`TaskEntity` 新增字段: + +| 字段 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `TaskType` | `TaskType` | `Normal` | 待办项类型 | +| `MeetingNotes` | `string?` | null | 会议纪要/转写文字(仅 Meeting 类型有值) | +| `AudioDuration` | `double?` | null | 录音时长(秒),仅 Meeting 类型有值 | + +> 注意:音频文件本身不持久存储,转写完成后仅保留文字结果。`AudioDuration` 用于 UI 展示录音时长。 + +### 4.3 API 设计 + +#### 4.3.1 创建会议类型待办项 + +``` +POST /api/task +请求体: +{ + "title": "周一产品评审会", + "priority": 1, + "taskType": 1 // TaskType.Meeting = 1 +} +响应体: +{ + "success": true, + "data": { + "id": 42, + "title": "周一产品评审会", + "taskType": 1, + "meetingNotes": null, + "audioDuration": null, + ... + } +} +``` + +#### 4.3.2 上传录音并转写 + +``` +POST /api/meeting/{taskId}/transcribe +Content-Type: multipart/form-data +字段: + - audio: 音频文件(webm/wav/mp3) + - format: 音频格式(可选,默认从文件扩展名推断) + +响应体: +{ + "success": true, + "data": { + "taskId": 42, + "transcript": "今天的产品评审会主要讨论了三个议题:第一...", + "audioDuration": 1830.5 + } +} +``` + +#### 4.3.3 保存会议纪要(文字输入) + +``` +POST /api/meeting/{taskId}/notes +请求体: +{ + "notes": "今天的产品评审会主要讨论了三个议题:第一..." +} +响应体: +{ + "success": true, + "data": { + "taskId": 42, + "meetingNotes": "今天的产品评审会主要讨论了三个议题:第一..." + } +} +``` + +#### 4.3.4 AI 任务拆分建议 + +``` +POST /api/meeting/{taskId}/breakdown +请求体: +{ + "notes": "..." // 可选,若不传则使用已保存的 meetingNotes +} +响应体: +{ + "success": true, + "data": { + "taskId": 42, + "suggestions": [ + { + "title": "整理产品需求文档", + "priority": 2, + "reason": "会议中提到需要在本周五前完成需求文档的整理" + }, + { + "title": "安排技术方案评审", + "priority": 1, + "reason": "张三负责在下周三前输出技术方案" + }, + { + "title": "跟进客户反馈", + "priority": 0, + "reason": "李四反馈了三个客户问题,需后续跟进" + } + ] + } +} +``` + +#### 4.3.5 确认并批量创建子任务 + +``` +POST /api/meeting/{taskId}/breakdown/confirm +请求体: +{ + "subTasks": [ + { "title": "整理产品需求文档", "priority": 2 }, + { "title": "安排技术方案评审", "priority": 1 } + ] +} +响应体: +{ + "success": true, + "data": { + "createdCount": 2, + "subTasks": [ + { "id": 43, "title": "整理产品需求文档", "priority": 2, "parentTaskId": 42 }, + { "id": 44, "title": "安排技术方案评审", "priority": 1, "parentTaskId": 42 } + ] + } +} +``` + +--- + +## 五、与工单 02 的边界 + +| 能力 | 工单 02 | 工单 03 | +|---|---|---| +| LLM 调用 | `LlmClientService` 基础设施 | 复用 `LlmClientService`,新增会议拆分专用 prompt | +| 语音输入 | STT 平台原生能力(语音指令) | 前端 MediaRecorder 录音 → 后端 STT 转写(场景不同) | +| AI 拆分 | 对已有单个任务做拆分(`AI_BREAKDOWN` 意图) | 对会议内容做拆分(输入为长文本,输出为多条建议) | +| 确认流程 | 语音确认/点选候选 | 专门的审阅 UI(勾选、编辑、补充) | + +**复用关系**: +- `LlmClientService`:03 直接复用 02 的 LLM 调用封装 +- `AiBreakdownService`:02 的单任务拆分服务,03 不直接复用(输入形式和输出结构不同),但设计上保持一致的调用模式 + +--- + +## 六、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| 创建会议类型待办项 | 创建 Todo 时选择"会议"类型 | 创建成功,列表中显示会议图标/标签 | +| 录音功能 | 点击录音按钮,录制一段音频 | 录音控件正常工作,显示录音时长 | +| 录音转写 | 录音完成后提交 | 后端返回转写文字,文字内容基本准确 | +| 文字输入纪要 | 在会议待办项中粘贴会议纪要 | 保存成功,再次打开可见纪要内容 | +| AI 拆分建议 | 提交会议内容请求 AI 拆分 | 返回 3-10 条建议,每条含标题+优先级+理由 | +| 建议审阅 | 勾选/取消/编辑建议 | UI 支持勾选、编辑标题和优先级 | +| 批量创建 | 确认勾选的建议 | 子任务批量创建到父任务下 | +| 离线降级 | 离线时请求 AI 拆分 | 提示"离线模式不支持 AI 拆分" | +| 非会议类型 | 普通待办项不显示录音/拆分入口 | 功能入口仅对 Meeting 类型显示 | + +--- + +## 七、风险与回滚 + +| 风险 | 影响 | 应对策略 | +|---|---|---| +| STT 转写准确率不足 | 会议纪要质量差,影响 AI 拆分效果 | 支持文字输入作为备选;转写后允许用户编辑修正 | +| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,其他会议功能(录音、纪要)不受影响 | +| 录音文件过大 | 上传超时/存储压力大 | 前端限制录音时长(建议最长 2 小时);压缩音频格式 | +| 浏览器 MediaRecorder 兼容性 | 部分平台录音功能不可用 | 降级提示"当前浏览器不支持录音,请使用文字输入" | + +--- + +## 八、Touch List + +| 文件路径 | 修改类型 | 是否共享 | 说明 | +|---|---|---|---| +| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 | TaskType 枚举 | +| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 | 新增 TaskType/MeetingNotes/AudioDuration 字段 | +| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 | 新增字段映射 + 迁移 | +| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 | DTO 扩展(CreateTaskDto/TaskDto 新增字段) | +| `src/Hua.Todo.Application/Meeting/` | 新增目录 | 否 | 会议相关服务目录 | +| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 | 会议 API 端点 | +| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 | 会议业务逻辑 | +| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 | 否 | 会议 AI 拆分服务 | +| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 | 否 | 音频转写服务 | +| `src/Hua.Todo.Application/Meeting/Models/` | 新增 | 否 | 会议相关 DTO | +| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 | 否 | 前端会议 API 模块 | +| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 | 否 | 前端录音组合式函数 | +| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 | 否 | 会议任务拆分审阅对话框 | +| `src/Hua.Todo.Web/src/components/MeetingTaskItem.vue` | 新增 | 否 | 会议类型待办项(含录音/纪要入口) | +| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 | 否 | 录音控件 | +| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 | 类型扩展(taskType/meetingNotes) | + +--- + +**工单编号**:03 +**标题**:会议任务拆分 +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/docs/project/研发工单-v1.3.0/04-富文本描述与附件管理.md b/docs/project/研发工单-v1.3.0/04-富文本描述与附件管理.md new file mode 100644 index 0000000..efdb325 --- /dev/null +++ b/docs/project/研发工单-v1.3.0/04-富文本描述与附件管理.md @@ -0,0 +1,554 @@ +# 研发工单 v1.3.0 - 04 富文本描述、附件与外部链接 + +> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。 + +--- + +## 一、目标与范围 + +### 1.1 目标 + +当前 Todo 待办项仅包含单行标题(`Title`),无法满足"一个待办项关联更多上下文信息"的需求。本工单为 Todo 待办项新增三项能力: + +| 能力 | 描述 | +|---|---| +| **多行描述(Description)** | 每个待办项可附带一段多行文字描述,用于记录详细说明、步骤、备注等 | +| **文件附件(Attachments)** | 可为一个待办项关联多个本地文件,支持上传、查看、下载、删除 | +| **外部链接与程序启动** | 附件可以是外部 URL(点击在系统默认浏览器打开)或本地文件路径/可执行文件(点击通过系统关联程序打开) | + +### 1.2 平台范围 + +| 平台 | 是否支持 | 说明 | +|---|---|---| +| Windows(MAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 | +| Linux(Avalonia) | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 | +| macOS(MAUI) | 待定 | 视为 Windows 同级,但本期不单独投入 | +| iOS / Android | ❌ 不支持 | 移动端不提供附件/描述编辑入口,但**移动端编辑待办项时不得覆盖/清空已有描述和附件数据**(详见 1.4 节) | + +### 1.3 范围 + +**包含**: + +- `TaskEntity` 新增 `Description` 字段(多行文本) +- 新增 `AttachmentEntity` 数据模型(附件元数据) +- 附件 CRUD API(上传、列表、下载、删除、打开) +- 前端编辑对话框扩展(描述 textarea + 附件管理区域) +- 前端待办项列表/详情展示(描述预览、附件数量角标) +- 桌面端通过系统关联程序打开附件(URL → 浏览器,文件路径 → 关联应用) + +**不包含**: + +- 附件云同步(本期不涉及,后续版本规划) +- 附件预览(如图片缩略图、PDF 内嵌预览,本期不做) +- 移动端附件管理(本期仅桌面端) +- 富文本编辑器(如 Markdown 渲染,本期仅纯文本多行) + +### 1.4 跨平台数据安全:移动端编辑不得破坏已有数据(强制) + +**背景**:移动端(iOS/Android)不提供描述/附件的编辑 UI,但用户仍可在移动端修改待办项标题、优先级、完成状态等基础字段。如果没有防护,移动端发起更新请求时会用"空值"覆盖掉桌面端已设置的 `Description` 和 `Attachments`,导致数据丢失。 + +**约束**: + +| 约束项 | 要求 | +|---|---| +| **后端 API 必须支持真正的部分更新** | `PUT /api/task/{id}` 的请求体中,**未传递的字段保持原值不变**,不得将缺失字段视为"置空"。即:`description` 不在 JSON 中 → 不修改数据库中的 `Description`;`description` 为 `null` → 清空 | +| **UpdateTaskDto 所有扩展字段均为 optional** | `description`、`attachments` 相关字段在 DTO 中均标记为可选(`string?` / `null`),与必填字段(`id`)区分 | +| **移动端前端不传递未知字段** | 移动端构建 `UpdateTaskDto` 时只传 `id`、`title`、`priority`、`isCompleted`,不传 `description`、`attachments`。后端对这些字段视为"不修改" | +| **Attachment 实体独立于 Task 更新** | 附件 CRUD 走独立端点(`/api/task/{id}/attachments`),不通过 `PUT /api/task/{id}` 携带。移动端不调附件端点,自然不会破坏附件数据 | + +**验证方式**: +1. 桌面端创建待办项,添加描述 + 上传附件 +2. 移动端编辑同一待办项(只改标题),保存 +3. 回到桌面端查看 → 描述和附件完整保留,未被覆盖 + +--- + +## 二、前置条件 + +| 条件 | 说明 | 状态 | +|---|---|---| +| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 | +| 工单 03 TaskType 枚举 | `TaskEntity` 已有字段扩展模式可参考 | 待实现(03 先于 04 或并行) | +| 桌面 WebView 文件选择 | 前端 `` 能力 | 已就绪 | +| 桌面程序启动 | 通过后端 `Process.Start` / `xdg-open` 实现 | 需新增 | + +--- + +## 三、功能入口覆盖检查 + +> 依据 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md),新功能必须检查两个入口覆盖情况。 + +| 入口 | 已规划 | 方案 | +|---|---|---| +| UI | ✅ | 编辑对话框新增描述 textarea + 附件列表区域;TaskItem 卡片显示描述预览与附件数量角标 | +| 语音 | ⚠️ 部分 | 语音可追加/编辑描述文本(通过"给「标题」添加描述"意图);附件上传与外部程序启动不适合语音入口 | + +> 语音入口覆盖说明: +> - **支持**:通过语音添加/修改待办项描述("给「XXX」添加备注:...") +> - **不支持**:语音上传附件、语音打开外部程序 —— 两者本质上是可视化交互,语音不是合适的入口。本期不覆盖。 +> - TODO:工单 02 的 LLM intent parser prompt 需预留 `SET_DESCRIPTION` 意图,参数为 `targetTask` + `description` + +--- + +## 四、数据模型设计 + +### 4.1 TaskEntity 扩展 + +在现有 `TaskEntity` 上新增一个字段: + +```csharp +/// +/// 多行描述文本(可为空)。 +/// 用于记录待办项相关的详细说明、步骤或备注。 +/// +public string? Description { get; set; } +``` + +| 字段 | 类型 | 默认值 | 约束 | 说明 | +|---|---|---|---|---| +| `Description` | `string?` | null | 最大 5000 字符 | 多行描述文本 | + +### 4.2 AttachmentEntity(新增) + +在 `Hua.Todo.Core/Entities/` 下新增 `AttachmentEntity.cs`: + +```csharp +/// 附件类型枚举 +public enum AttachmentType +{ + /// 本地文件(已复制到应用数据目录) + LocalFile = 0, + /// 外部链接(URL) + ExternalLink = 1 +} + +/// 附件实体,表示待办项关联的文件或外部链接 +public class AttachmentEntity +{ + /// 附件唯一标识符 + public int Id { get; set; } + + /// 所属待办项ID + public int TaskId { get; set; } + + /// 显示名称(用户可见的文件名或链接标题) + public string FileName { get; set; } = string.Empty; + + /// 存储路径(本地文件的相对路径)或外部URL + public string FilePath { get; set; } = string.Empty; + + /// 文件大小(字节),外部链接为 0 + public long FileSize { get; set; } + + /// MIME 类型(如 text/plain、application/pdf),外部链接为空字符串 + public string ContentType { get; set; } = string.Empty; + + /// 附件类型 + public AttachmentType AttachmentType { get; set; } = AttachmentType.LocalFile; + + /// 创建时间(UTC) + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + // 导航属性 + public TaskEntity Task { get; set; } = null!; +} +``` + +| 字段 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `Id` | `int` | 自增 | 主键 | +| `TaskId` | `int` | - | FK → Tasks.Id | +| `FileName` | `string` | - | 显示名称,最大 256 字符 | +| `FilePath` | `string` | - | 存储路径或外部 URL,最大 1024 字符 | +| `FileSize` | `long` | 0 | 字节数 | +| `ContentType` | `string` | "" | MIME 类型 | +| `AttachmentType` | `AttachmentType` | `LocalFile` | 本地文件 vs 外部链接 | +| `CreatedAt` | `DateTime` | `UtcNow` | 创建时间 | + +`TaskEntity` 新增导航属性: +```csharp +public List Attachments { get; set; } = new(); +``` + +### 4.3 数据库表 + +| 表名 | 说明 | +|---|---| +| `T_Attachments` | 附件表 | + +> 注:表名沿用 ABP 模板规范 `T_{实体名}s`(见 [.trae/rules/项目/03-数据模型与迁移约束.md](../../../.trae/rules/项目/03-数据模型与迁移约束.md))。 + +### 4.4 附件存储策略 + +| 策略项 | 决定 | +|---|---| +| 存储位置 | 应用数据目录下 `Attachments/` 子目录(与数据库 `.db` 同级) | +| 文件命名 | `{attachmentId}_{originalFileName}`,避免重名冲突 | +| 文件大小上限 | 单文件 50MB(`appsettings.json` 可配置) | +| 总附件数上限 | 每个待办项最多 20 个附件 | +| 外部链接 | 不复制文件,仅存储 URL 字符串;打开时调用系统默认浏览器 | + +--- + +## 五、API 设计 + +### 5.1 已修改:创建/更新待办项(扩展 Description) + +``` +POST /api/task +请求体新增字段: +{ + "title": "...", + "priority": 1, + "description": "多行描述文本(可选)" // ← 新增 +} + +PUT /api/task/{id} +请求体新增字段(所有扩展字段均为可选): +{ + "id": 42, + "title": "...", + "priority": 1, + "description": "..." // ← 可选;不传=保持原值,传 null=清空 +} +``` + +> **部分更新语义(强制)**:`PUT` 端点必须实现"仅更新已传递字段"的逻辑。 +> - `description` 字段**不在 JSON 中** → 数据库 `Description` 保持原值 +> - `description` 字段**为 `null`** → 数据库 `Description` 清空为 `null` +> - 此设计确保移动端不传递 `description` 时不会意外清空已有描述 + +### 5.2 附件上传 + +``` +POST /api/task/{taskId}/attachments +Content-Type: multipart/form-data +字段: + - file: 文件内容(必填,最大 50MB) + +响应体: +{ + "success": true, + "data": { + "id": 1, + "taskId": 42, + "fileName": "需求文档.pdf", + "fileSize": 204800, + "contentType": "application/pdf", + "attachmentType": 0, + "createdAt": "2026-06-16T10:00:00Z" + } +} +``` + +### 5.3 添加外部链接 + +``` +POST /api/task/{taskId}/attachments/link +请求体: +{ + "url": "https://example.com/doc", + "fileName": "参考文档" // 可选,不传则使用 URL 作为显示名 +} + +响应体: +{ + "success": true, + "data": { + "id": 2, + "taskId": 42, + "fileName": "参考文档", + "filePath": "https://example.com/doc", + "fileSize": 0, + "contentType": "", + "attachmentType": 1, + "createdAt": "2026-06-16T10:00:00Z" + } +} +``` + +### 5.4 获取附件列表 + +``` +GET /api/task/{taskId}/attachments + +响应体: +{ + "success": true, + "data": [ + { + "id": 1, + "fileName": "需求文档.pdf", + "fileSize": 204800, + "contentType": "application/pdf", + "attachmentType": 0, + "createdAt": "2026-06-16T10:00:00Z" + }, + { + "id": 2, + "fileName": "参考文档", + "filePath": "https://example.com/doc", + "attachmentType": 1, + "createdAt": "2026-06-16T10:05:00Z" + } + ] +} +``` + +### 5.5 下载附件 + +``` +GET /api/attachments/{attachmentId}/download + +响应:文件流(Content-Disposition: attachment; filename="需求文档.pdf") +``` + +### 5.6 打开附件(桌面端) + +``` +POST /api/attachments/{attachmentId}/open + +响应体: +{ + "success": true, + "data": { + "opened": true + } +} + +// 后端行为: +// - AttachmentType.LocalFile → Process.Start(filePath)(Windows)或 xdg-open(Linux) +// - AttachmentType.ExternalLink → Process.Start(url)(在默认浏览器打开) +``` + +### 5.7 删除附件 + +``` +DELETE /api/task/{taskId}/attachments/{attachmentId} + +响应体: +{ + "success": true, + "message": "附件已删除" +} +``` + +--- + +## 六、前端设计 + +### 6.1 类型扩展 + +`src/Hua.Todo.Web/src/types/task.ts` 新增 / 修改: + +```typescript +// Task 接口新增字段 +export interface Task { + // ... 既有字段 + description?: string; // 多行描述 + attachments?: AttachmentItem[]; // 附件列表 + attachmentCount?: number; // 附件数量(列表视图用,不传完整列表) +} + +export interface AttachmentItem { + id: number; + taskId: number; + fileName: string; + filePath: string; + fileSize: number; + contentType: string; + attachmentType: AttachmentType; // 0=本地文件, 1=外部链接 + createdAt: string; +} + +export type AttachmentType = 0 | 1; +``` + +### 6.2 组件变更 + +| 组件 | 变更类型 | 说明 | +|---|---|---| +| `TaskEditDialog.vue` | 修改 | 新增描述 textarea + 附件管理区域(上传按钮、附件列表) | +| `TaskItem.vue` | 修改 | 卡片底部显示描述预览(单行截断)+ 附件数量角标 | +| `AttachmentList.vue` | 新增 | 附件列表组件:文件名、大小、类型图标、删除按钮、打开按钮 | +| `LinkInputDialog.vue` | 新增 | 外部链接输入弹出框(URL + 显示名称) | + +### 6.3 新增 API 模块 + +`src/Hua.Todo.Web/src/api/attachments.ts`: + +```typescript +// uploadAttachment(taskId, file) — POST multipart/form-data +// addLink(taskId, url, fileName?) — POST /api/task/{id}/attachments/link +// getAttachments(taskId) — GET +// deleteAttachment(taskId, attId) — DELETE +// openAttachment(attId) — POST /api/attachments/{id}/open +// downloadAttachment(attId) — GET /api/attachments/{id}/download +``` + +### 6.4 交互流程 + +``` +编辑待办项对话框 +├─ 标题输入框(现有) +├─ 优先级选择(现有) +├─ 描述 textarea(新增:多行文本,placeholder "添加备注、步骤说明...") +├─ 附件区域(新增) +│ ├─ [+ 上传文件] 按钮 → 触发文件选择器 → 上传至后端 +│ ├─ [+ 添加链接] 按钮 → 弹出 URL 输入框 → 保存为外部链接附件 +│ └─ 附件列表(每项显示:图标 + 文件名 + 大小 + [打开] [删除]) +│ ├─ 本地文件:[打开] → 调用后端 open API → 系统关联程序打开 +│ ├─ 外部链接:[打开] → 调用后端 open API → 浏览器打开 +│ └─ [删除] → 确认 → 删除附件 +└─ [取消] [保存] +``` + +### 6.5 列表卡片展示 + +``` +┌─────────────────────────────────────┐ +│ ☐ 整理产品需求文档 │ +│ 高优先级 │ +│ 需要在本周五前完成需求文档的整理... │ ← 描述预览(单行,超长截断) +│ 📎 2 个附件 │ ← 附件数量角标 +└─────────────────────────────────────┘ +``` + +--- + +## 七、后端架构 + +### 7.1 新增服务 + +| 服务/类 | 位置 | 说明 | +|---|---|---| +| `AttachmentEntity` | `Hua.Todo.Core/Entities/` | 附件实体 | +| `AttachmentType` | `Hua.Todo.Core/Entities/` | 附件类型枚举 | +| `IAttachmentRepository` | `Hua.Todo.Core/Repositories/` | 仓储接口 | +| `AttachmentRepository` | `Hua.Todo.Application/Data/` | EF Core 仓储实现 | +| `AttachmentService` | `Hua.Todo.Application/Services/` | 附件业务逻辑(上传、下载、打开、清理) | +| `IAttachmentService` | `Hua.Todo.Application/Services/` | 服务接口 | +| `AttachmentController` | `Hua.Todo.Application/DynamicApi/` 或独立 Controller | 附件动态 API 端点(或手动 Controller) | +| `PlatformAttachmentOpener` | `Hua.Todo.{Maui,Avalonia}/Services/` | 平台特定文件/链接打开实现(`Process.Start` / `xdg-open`) | + +### 7.2 平台差异:外部程序启动 + +| 平台 | 实现方式 | +|---|---| +| Windows(MAUI) | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` | +| Linux(Avalonia) | `System.Diagnostics.Process.Start("xdg-open", path)` | + +通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。 + +### 7.3 附件文件管理 + +- 上传的本地文件复制到 `/Hua.Todo/Attachments/{attachmentId}_{originalFileName}` +- 删除附件时同步删除磁盘文件 +- 删除待办项时级联删除其所有附件(实体 + 文件) +- 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示) + +### 7.4 安全约束 + +| 约束 | 说明 | +|---|---| +| 文件类型白名单 | 不限制(本地工具场景,用户自行管理) | +| 文件大小上限 | 50MB(可配置) | +| 路径遍历防护 | 文件名去除 `../`、`..\` 等危险路径片段 | +| 外部链接验证 | 仅允许 `http://`、`https://` 协议 | + +--- + +## 八、子工单拆分 + +本工单体积适中,不拆分子工单,单文件完整实现。 + +如需与工单 03 并行推进,注意以下共享文件: + +| 共享文件 | Writer | 说明 | +|---|---|---| +| `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration;04 后续追加 Description/Attachments 导航属性 | +| `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 | +| `task.ts` | **工单 03** 先写入 | 03 新增 taskType;04 后追加 description、attachments | +| `TaskEditDialog.vue` | 协调 | 03 新增 MeetingType 条件渲染;04 追加描述 + 附件区域 | + +> 执行顺序:优先完成 03-01(数据模型),再开始 04 的后端模型部分。前端组件部分可独立并行。 + +--- + +## 九、验收标准 + +| 验收项 | 验证方法 | 预期结果 | +|---|---|---| +| 描述编辑 | 编辑待办项,在描述 textarea 输入多行文字,保存 | 再次打开编辑框,描述内容完整保留 | +| 描述显示 | 查看待办项列表/详情 | 列表卡片显示描述预览(单行截断),详情显示完整描述 | +| 附件上传 | 编辑待办项,点击上传文件,选择一个本地文件 | 文件上传成功,附件列表显示文件名和大小 | +| 附件下载 | 在附件列表点击下载按钮 | 文件以原始文件名下载到本地 | +| 附件删除 | 点击附件删除按钮 | 附件从列表移除,磁盘文件同步清理 | +| 外部链接添加 | 点击添加链接,输入 URL | 链接保存为附件,类型标记为"外部链接" | +| 打开本地文件 | 点击本地附件"打开" | 系统关联程序打开该文件(如 PDF → PDF 阅读器) | +| 打开外部链接 | 点击外部链接附件"打开" | 系统默认浏览器打开该 URL | +| 打开可执行文件 | 附件为 `.exe`(Windows)或可执行脚本(Linux) | 系统正常运行该程序 | +| 附件数量限制 | 超过 20 个附件后继续上传 | 提示"每个待办项最多 20 个附件" | +| 文件大小限制 | 上传超过 50MB 的文件 | 提示"文件大小不能超过 50MB" | +| 待办项删除级联 | 删除有附件的待办项 | 附件记录和磁盘文件同步清理 | +| 空描述 | 创建待办项时不填描述 | 正常创建,列表中不显示描述行 | +| 跨平台安全 | 桌面端设置描述+附件 → 移动端只改标题并保存 → 桌面端查看 | 描述和附件完整保留,未被覆盖(详见 1.4 节) | + +--- + +## 十、风险与回滚 + +| 风险 | 影响 | 应对策略 | +|---|---|---| +| `Process.Start` 在不同 Linux 发行版行为差异 | 部分 Linux 文件打开失败 | 用 `xdg-open` 兜底;提供错误提示"无法打开文件:{原因}" | +| 附件文件被用户手动删除 | 数据库有记录但文件不存在 | 启动时校验,缺失文件标记为"失效",UI 用灰色 + 感叹号提示 | +| 大文件上传耗尽磁盘空间 | 应用数据目录磁盘满 | 单文件 50MB + 每待办项 20 个上限 = 最多 1GB;检查和提示剩余空间 | +| 并发编辑附件冲突 | 多窗口同时删除/添加附件 | 附件操作为即时保存(非批量提交),利用数据库事务隔离 | + +--- + +## 十一、Touch List + +| 文件路径 | 修改类型 | 是否共享 | 说明 | +|---|---|---|---| +| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是(与 03 共享) | 新增 Description 字段 + Attachments 导航属性 | +| `src/Hua.Todo.Core/Entities/AttachmentEntity.cs` | 新增 | 否 | 附件实体 | +| `src/Hua.Todo.Core/Entities/AttachmentType.cs` | 新增 | 否 | 附件类型枚举 | +| `src/Hua.Todo.Core/Repositories/IAttachmentRepository.cs` | 新增 | 否 | 附件仓储接口 | +| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是(与 03 共享) | 新增 Attachments DbSet + 关系映射 | +| `src/Hua.Todo.Application/Services/IAttachmentService.cs` | 新增 | 否 | 附件服务接口 | +| `src/Hua.Todo.Application/Services/AttachmentService.cs` | 新增 | 否 | 附件业务逻辑 | +| `src/Hua.Todo.Application/Controllers/AttachmentController.cs` | 新增 | 否 | 附件 API 端点 | +| `src/Hua.Todo.Application/Models/AttachmentModels.cs` | 新增 | 否 | 附件 DTO | +| `src/Hua.Todo.Core/Services/IPlatformAttachmentOpener.cs` | 新增 | 否 | 平台文件打开接口 | +| `src/Hua.Todo.Maui/Services/Platforms/MauiAttachmentOpener.cs` | 新增 | 否 | Windows MAUI 文件打开实现 | +| `src/Hua.Todo.Avalonia/Services/Platforms/AvaloniaAttachmentOpener.cs` | 新增 | 否 | Linux Avalonia 文件打开实现 | +| `src/Hua.Todo.Web/src/api/attachments.ts` | 新增 | 否 | 前端附件 API 模块 | +| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是(与 03 共享) | 新增 description、AttachmentItem 等类型 | +| `src/Hua.Todo.Web/src/components/TaskEditDialog.vue` | 修改 | 是(与 03 共享) | 新增描述 textarea + 附件管理区域 | +| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改 | 否 | 描述预览 + 附件角标 | +| `src/Hua.Todo.Web/src/components/AttachmentList.vue` | 新增 | 否 | 附件列表组件 | +| `src/Hua.Todo.Web/src/composables/useAttachments.ts` | 新增 | 否 | 附件管理组合式函数 | + +--- + +## 十二、与工单 03 的交互 + +| 维度 | 工单 03 | 工单 04 | +|---|---|---| +| 描述文本 | `MeetingNotes`(会议纪要,仅会议类型) | `Description`(通用多行描述,所有类型) | +| 附件 | 不涉及 | 通用附件管理 | +| 外部链接 | 不涉及 | 通用外部链接 | + +> `MeetingNotes` 与 `Description` 是两个独立字段: +> - `MeetingNotes`:会议转写/文字纪要,可能很长(数千字),仅在会议类型下有值 +> - `Description`:通用简短备注(上限 5000 字符),所有类型的待办项都可以有 +> +> 两者可共存:会议类型的待办项可以同时有 `MeetingNotes`(AI 拆分的输入)和 `Description`(用户的简短备注)。 + +--- + +**工单编号**:04 +**标题**:富文本描述、附件与外部链接 +**版本**:v1.3.0 +**创建日期**:2026-06-16 diff --git a/src/Hua.Todo.Application/Hua.Todo.Application.csproj b/src/Hua.Todo.Application/Hua.Todo.Application.csproj index ff4eedd..71f4038 100644 --- a/src/Hua.Todo.Application/Hua.Todo.Application.csproj +++ b/src/Hua.Todo.Application/Hua.Todo.Application.csproj @@ -13,12 +13,23 @@ + + + + + + + + + + + diff --git a/src/Hua.Todo.Application/Mcp/DynamicMcpToolExtensions.cs b/src/Hua.Todo.Application/Mcp/DynamicMcpToolExtensions.cs new file mode 100644 index 0000000..5dd61f1 --- /dev/null +++ b/src/Hua.Todo.Application/Mcp/DynamicMcpToolExtensions.cs @@ -0,0 +1,169 @@ +using System.ComponentModel; +using System.Reflection; +using Hua.Todo.Application.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Hua.Todo.Application.Mcp; + +/// +/// 从 接口自动生成 MCP 工具的扩展方法。 +/// +/// 新增 API 端点后无需手动添加 MCP 工具 —— 启动时自动扫描所有 +/// IDynamicApiService 接口的公共方法,将其注册为 MCP 工具。 +/// 工具名从接口名 + 方法名按约定推导(如 ITaskService.GetAllTasksAsync +/// → task_get_all_tasks),与 DynamicApi 路由推导逻辑保持一致。 +/// +/// +public static class DynamicMcpToolExtensions +{ + /// + /// 扫描程序集中所有 IDynamicApiService 接口, + /// 将其公共方法自动注册为 MCP 工具。 + /// + /// MCP Server 构建器。 + /// MCP Server 构建器。 + public static IMcpServerBuilder WithDynamicApiTools(this IMcpServerBuilder builder) + { + var assembly = typeof(IDynamicApiService).Assembly; + + var serviceInterfaces = assembly.GetTypes() + .Where(t => t.IsInterface + && typeof(IDynamicApiService).IsAssignableFrom(t) + && t != typeof(IDynamicApiService)) + .ToList(); + + foreach (var serviceType in serviceInterfaces) + { + RegisterToolsFromInterface(builder, serviceType); + } + + return builder; + } + + /// + /// 将单个服务接口的所有公共方法注册为 MCP 工具。 + /// 每个方法通过 McpServerTool.Create(MethodInfo, createTargetFunc) 注册, + /// MCP SDK 自动处理参数反序列化(包括复杂 DTO 类型)和返回值序列化。 + /// + private static void RegisterToolsFromInterface(IMcpServerBuilder builder, Type serviceType) + { + var prefix = DeriveServiceName(serviceType); + var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + foreach (var method in methods) + { + var toolName = $"{prefix}_{ToSnakeCase(StripAsyncSuffix(method.Name))}"; + var description = GetMethodDescription(method); + var capturedServiceType = serviceType; + var capturedMethod = method; + + // 使用 McpServerTool.Create(MethodInfo, createTargetFunc, options) 注册。 + // createTargetFunc 在每次工具调用时执行,从 RequestContext.Services + // 解析 DI 容器中注册的服务实例,支持 Scoped 生命周期。 + var tool = McpServerTool.Create( + capturedMethod, + (RequestContext ctx) => + ActivatorUtilities.GetServiceOrCreateInstance(ctx.Services!, capturedServiceType), + new McpServerToolCreateOptions + { + Name = toolName, + Description = description, + ReadOnly = IsReadOnlyMethod(method.Name), + Destructive = IsDestructiveMethod(method.Name), + }); + + builder.Services.AddSingleton(tool); + } + } + + #region 名称与描述推导 + + /// + /// 从接口名推导服务名前缀,与 DynamicApi 中间件逻辑一致: + /// ITaskService → task, ICloudAuthService → cloud_auth + /// + public static string DeriveServiceName(Type serviceType) + { + var name = serviceType.Name; + + if (name.StartsWith("I") && name.Length > 1 && char.IsUpper(name[1])) + name = name[1..]; + + if (name.EndsWith("Service")) + name = name[..^"Service".Length]; + + if (name.EndsWith("App")) + name = name[..^"App".Length]; + + return ToSnakeCase(name); + } + + /// + /// PascalCase → snake_case。 + /// + public static string ToSnakeCase(string name) + { + if (string.IsNullOrEmpty(name)) return name; + + var sb = new System.Text.StringBuilder(); + for (var i = 0; i < name.Length; i++) + { + var c = name[i]; + if (char.IsUpper(c)) + { + if (i > 0) sb.Append('_'); + sb.Append(char.ToLower(c)); + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } + + /// + /// 去掉方法名的 Async 后缀用于工具命名。 + /// + private static string StripAsyncSuffix(string name) + => name.EndsWith("Async") ? name[..^5] : name; + + /// + /// 从方法的 Description 特性或方法名前缀推导中文描述。 + /// + private static string GetMethodDescription(MethodInfo method) + { + var descAttr = method.GetCustomAttribute(); + if (descAttr != null) return descAttr.Description; + + var name = method.Name; + if (name.StartsWith("Get")) return $"获取{StripAsyncSuffix(name[3..])}"; + if (name.StartsWith("Create")) return $"创建{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Update")) return $"更新{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Delete")) return $"删除{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Toggle")) return $"切换{StripAsyncSuffix(name[6..])}"; + + return StripAsyncSuffix(name); + } + + #endregion + + #region 行为注解 + + /// + /// 根据方法名前缀判断是否为只读操作。 + /// + private static bool IsReadOnlyMethod(string name) + => name.StartsWith("Get") || name.StartsWith("List") + || name.StartsWith("Query") || name.StartsWith("Search"); + + /// + /// 根据方法名前缀判断是否为破坏性操作。 + /// + private static bool IsDestructiveMethod(string name) + => name.StartsWith("Delete") || name.StartsWith("Remove"); + + #endregion +} diff --git a/src/Hua.Todo.Application/Mcp/McpEndpointExtensions.cs b/src/Hua.Todo.Application/Mcp/McpEndpointExtensions.cs new file mode 100644 index 0000000..7273c00 --- /dev/null +++ b/src/Hua.Todo.Application/Mcp/McpEndpointExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Hua.Todo.Application.Mcp; + +/// +/// MCP Server 端点映射扩展方法。 +/// 仅限 net10.0 目标(需要 ASP.NET Core),非 net10.0 目标时此文件不参与编译。 +/// +public static class McpEndpointExtensions +{ + /// + /// 将 MCP Server 端点映射到指定路径。 + /// 默认路径为 /mcp,客户端通过 Streamable HTTP 协议连接。 + /// + /// 端点路由构建器。 + /// 路由路径,默认 /mcp。 + /// 端点路由构建器。 + public static IEndpointRouteBuilder MapMcpServer(this IEndpointRouteBuilder endpoints, string pattern = "/mcp") + { + endpoints.MapMcp(pattern); + return endpoints; + } +} diff --git a/src/Hua.Todo.Application/Mcp/McpServiceCollectionExtensions.cs b/src/Hua.Todo.Application/Mcp/McpServiceCollectionExtensions.cs new file mode 100644 index 0000000..d95df3a --- /dev/null +++ b/src/Hua.Todo.Application/Mcp/McpServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Server; + +namespace Hua.Todo.Application.Mcp; + +/// +/// MCP Server 依赖注入扩展方法。 +/// 仅限 net10.0 目标(需要 ASP.NET Core),非 net10.0 目标时此文件不参与编译。 +/// +public static class McpServiceCollectionExtensions +{ + /// + /// 注册 MCP Server,并自动从所有 IDynamicApiService 接口生成 MCP 工具。 + /// 调用前须已注册 AddApplicationServices。 + /// + /// 工具为动态关联:新增 IDynamicApiService 接口或方法后, + /// 无需手动添加 MCP 工具,重启即可自动暴露。 + /// + /// + /// 服务集合。 + /// 服务集合。 + public static IServiceCollection AddMcpServerServices(this IServiceCollection services) + { + services.AddMcpServer(options => + { + options.ServerInfo = new ModelContextProtocol.Protocol.Implementation + { + Name = "Hua.Todo MCP Server", + Version = "1.0.0" + }; + }) + .WithHttpTransport(options => + { + // 无状态模式:无需服务端→客户端请求,支持水平扩展 + options.Stateless = true; + }) + .WithDynamicApiTools(); + + return services; + } +} diff --git a/src/Hua.Todo.Host/Program.cs b/src/Hua.Todo.Host/Program.cs index 2f98173..acb2a7b 100644 --- a/src/Hua.Todo.Host/Program.cs +++ b/src/Hua.Todo.Host/Program.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Hua.Todo.Application; using Hua.Todo.Application.DynamicApi; using Hua.Todo.Application.Interfaces; +using Hua.Todo.Application.Mcp; using Hua.Todo.Application.Models; var builder = WebApplication.CreateBuilder(args); @@ -13,6 +14,9 @@ builder.Services.AddAuthorization(); builder.Services.AddApplicationServices("Data Source=Hua.Todo.db"); +// 注册 MCP Server(依赖 ITaskService,须在 AddApplicationServices 之后调用) +builder.Services.AddMcpServerServices(); + builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => @@ -43,4 +47,7 @@ app.UseCors("AllowAll"); app.UseAuthorization(); app.UseDynamicApi(); +// 映射 MCP Server 端点(Streamable HTTP,路径 /mcp) +app.MapMcpServer(); + app.Run(); diff --git a/src/Hua.Todo.Tests/DynamicMcpToolExtensionsTests.cs b/src/Hua.Todo.Tests/DynamicMcpToolExtensionsTests.cs new file mode 100644 index 0000000..b16710a --- /dev/null +++ b/src/Hua.Todo.Tests/DynamicMcpToolExtensionsTests.cs @@ -0,0 +1,324 @@ +using System.ComponentModel; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Server; +using Hua.Todo.Application.Interfaces; +using Hua.Todo.Application.Mcp; +using Hua.Todo.Application.Models; +using Xunit; + +namespace Hua.Todo.Tests; + +/// +/// DynamicMcpToolExtensions 单元测试。 +/// 重点验证命名推导、描述生成、行为注解等纯逻辑,以及动态工具注册。 +/// +public class DynamicMcpToolExtensionsTests +{ + #region 命名推导 + + /// + /// DeriveServiceName:ITaskService → task + /// + [Fact] + public void DeriveServiceName_ITaskService_ReturnsTask() + { + var result = DynamicMcpToolExtensions.DeriveServiceName(typeof(ITaskService)); + Assert.Equal("task", result); + } + + /// + /// DeriveServiceName:复合接口名转 snake_case。 + /// + [Fact] + public void DeriveServiceName_MultiWordInterface_ReturnsSnakeCase() + { + // 假设存在 ICloudSyncService → cloud_sync + // 用动态类型模拟 + var result = DynamicMcpToolExtensions.DeriveServiceName(typeof(ITestMultiWordService)); + Assert.Equal("test_multi_word", result); + } + + /// + /// ToSnakeCase:PascalCase → snake_case + /// + [Fact] + public void ToSnakeCase_PascalCase_ConvertsToSnakeCase() + { + Assert.Equal("get_all", DynamicMcpToolExtensions.ToSnakeCase("GetAll")); + Assert.Equal("create_task", DynamicMcpToolExtensions.ToSnakeCase("CreateTask")); + Assert.Equal("toggle_complete", DynamicMcpToolExtensions.ToSnakeCase("ToggleComplete")); + Assert.Equal("id", DynamicMcpToolExtensions.ToSnakeCase("Id")); + Assert.Equal("", DynamicMcpToolExtensions.ToSnakeCase("")); + } + + #endregion + + #region 工具名组合 + + /// + /// 完整工具名:prefix + 方法名 = task_get_all_tasks + /// 验证 StripAsyncSuffix 和 ToSnakeCase 的组合效果。 + /// + [Fact] + public void ToolNaming_PrefixAndMethod_FormatsCorrectly() + { + var prefix = DynamicMcpToolExtensions.DeriveServiceName(typeof(ITaskService)); + Assert.Equal("task", prefix); + + // GetActiveTasksAsync → get_active_tasks + var methodSuffix = DynamicMcpToolExtensions.ToSnakeCase( + StripAsyncSuffix("GetActiveTasksAsync")); + Assert.Equal("get_active_tasks", methodSuffix); + } + + /// + /// StripAsyncSuffix:去掉 Async 后缀。 + /// + [Fact] + public void StripAsyncSuffix_RemovesAsync() + { + Assert.Equal("GetAllTasks", StripAsyncSuffix("GetAllTasksAsync")); + Assert.Equal("Create", StripAsyncSuffix("CreateAsync")); + Assert.Equal("Toggle", StripAsyncSuffix("ToggleAsync")); + Assert.Equal("Delete", StripAsyncSuffix("DeleteAsync")); + Assert.Equal("GetById", StripAsyncSuffix("GetById")); // 无 Async 后缀不修改 + } + + #endregion + + #region 描述生成 + + /// + /// GetMethodDescription:方法名 → 中文描述 + /// + [Fact] + public void GetMethodDescription_GeneratesChineseDescription() + { + var getMethod = typeof(ITaskService).GetMethod(nameof(ITaskService.GetAllTasksAsync))!; + var createMethod = typeof(ITaskService).GetMethod(nameof(ITaskService.CreateTaskAsync))!; + var updateMethod = typeof(ITaskService).GetMethod(nameof(ITaskService.UpdateTaskAsync))!; + var deleteMethod = typeof(ITaskService).GetMethod(nameof(ITaskService.DeleteTaskAsync))!; + var toggleMethod = typeof(ITaskService).GetMethod(nameof(ITaskService.ToggleCompleteAsync))!; + + Assert.StartsWith("获取", GetMethodDescription(getMethod)); + Assert.StartsWith("创建", GetMethodDescription(createMethod)); + Assert.StartsWith("更新", GetMethodDescription(updateMethod)); + Assert.StartsWith("删除", GetMethodDescription(deleteMethod)); + Assert.StartsWith("切换", GetMethodDescription(toggleMethod)); + } + + /// + /// GetMethodDescription:带 DescriptionAttribute 时优先使用。 + /// + [Fact] + public void GetMethodDescription_UsesDescriptionAttribute() + { + var method = typeof(ITestAnnotatedService).GetMethod(nameof(ITestAnnotatedService.DoSomething))!; + var desc = GetMethodDescription(method); + Assert.Equal("执行自定义操作", desc); + } + + #endregion + + #region 行为注解 + + /// + /// IsReadOnlyMethod:Get/List/Query/Search 前缀返回 true。 + /// + [Fact] + public void IsReadOnlyMethod_GetPrefix_ReturnsTrue() + { + Assert.True(IsReadOnlyMethod("GetAllTasksAsync")); + Assert.True(IsReadOnlyMethod("ListSubTodosAsync")); + Assert.True(IsReadOnlyMethod("QueryByDateAsync")); + Assert.True(IsReadOnlyMethod("SearchByKeywordAsync")); + } + + /// + /// IsReadOnlyMethod:非只读前缀返回 false。 + /// + [Fact] + public void IsReadOnlyMethod_NonReadPrefix_ReturnsFalse() + { + Assert.False(IsReadOnlyMethod("CreateTaskAsync")); + Assert.False(IsReadOnlyMethod("UpdateTaskAsync")); + Assert.False(IsReadOnlyMethod("DeleteTaskAsync")); + Assert.False(IsReadOnlyMethod("ToggleCompleteAsync")); + } + + /// + /// IsDestructiveMethod:Delete/Remove 前缀返回 true。 + /// + [Fact] + public void IsDestructiveMethod_DeletePrefix_ReturnsTrue() + { + Assert.True(IsDestructiveMethod("DeleteTaskAsync")); + Assert.True(IsDestructiveMethod("RemoveItemAsync")); + } + + /// + /// IsDestructiveMethod:非破坏性操作返回 false。 + /// + [Fact] + public void IsDestructiveMethod_NonDestructive_ReturnsFalse() + { + Assert.False(IsDestructiveMethod("GetAllTasksAsync")); + Assert.False(IsDestructiveMethod("CreateTaskAsync")); + Assert.False(IsDestructiveMethod("UpdateTaskAsync")); + } + + #endregion + + #region 动态工具注册(集成测试) + + /// + /// 验证 WithDynamicApiTools 扫描 IDynamicApiService 所在程序集,能发现 ITaskService 并生成工具。 + /// 工具数量 = ITaskService 的公共方法数(9 个)。 + /// + [Fact] + public void WithDynamicApiTools_ScansAssemblyAndRegistersTools() + { + // Arrange + var services = new ServiceCollection(); + services.AddScoped(); + + // Act - 仅注册工具到 DI,不绑定传输层 + services.AddMcpServer() + .WithDynamicApiTools(); + + var provider = services.BuildServiceProvider(); + + // Assert:验证 McpServerTool 实例已注册到 DI + var tools = provider.GetServices().ToList(); + Assert.NotEmpty(tools); + + // ITaskService 有 9 个公共方法 + Assert.Equal(9, tools.Count); + + // 工具名应与推导规则一致 + var toolNames = tools.Select(t => t.ProtocolTool.Name).OrderBy(n => n).ToList(); + Assert.Contains("task_get_all_tasks", toolNames); + Assert.Contains("task_get_task_by_id", toolNames); + Assert.Contains("task_get_active_tasks", toolNames); + Assert.Contains("task_get_completed_tasks", toolNames); + Assert.Contains("task_create_task", toolNames); + Assert.Contains("task_update_task", toolNames); + Assert.Contains("task_toggle_complete", toolNames); + Assert.Contains("task_delete_task", toolNames); + Assert.Contains("task_get_sub_tasks", toolNames); + } + + /// + /// 验证生成的工具携带正确的行为注解(ReadOnly/Destructive)。 + /// + [Fact] + public void WithDynamicApiTools_ToolsHaveCorrectBehaviorAnnotations() + { + // Arrange + var services = new ServiceCollection(); + services.AddScoped(); + + // Act + services.AddMcpServer() + .WithDynamicApiTools(); + + var provider = services.BuildServiceProvider(); + var tools = provider.GetServices().ToList(); + + // Assert + var getTool = tools.First(t => t.ProtocolTool.Name == "task_get_all_tasks"); + Assert.True(getTool.ProtocolTool.Annotations?.ReadOnlyHint == true); + + var deleteTool = tools.First(t => t.ProtocolTool.Name == "task_delete_task"); + Assert.True(deleteTool.ProtocolTool.Annotations?.DestructiveHint == true); + } + + #endregion + + #region 辅助方法(暴露 private 方法用于测试) + + private static string StripAsyncSuffix(string name) + => name.EndsWith("Async") ? name[..^5] : name; + + private static bool IsReadOnlyMethod(string name) + => name.StartsWith("Get") || name.StartsWith("List") + || name.StartsWith("Query") || name.StartsWith("Search"); + + private static bool IsDestructiveMethod(string name) + => name.StartsWith("Delete") || name.StartsWith("Remove"); + + private static string GetMethodDescription(MethodInfo method) + { + var descAttr = method.GetCustomAttribute(); + if (descAttr != null) return descAttr.Description; + + var name = method.Name; + if (name.StartsWith("Get")) return $"获取{StripAsyncSuffix(name[3..])}"; + if (name.StartsWith("Create")) return $"创建{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Update")) return $"更新{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Delete")) return $"删除{StripAsyncSuffix(name[6..])}"; + if (name.StartsWith("Toggle")) return $"切换{StripAsyncSuffix(name[6..])}"; + return StripAsyncSuffix(name); + } + + #endregion +} + +#region 测试用类型 + +/// +/// Mock 多词接口,用于测试 snake_case 转换。 +/// +public interface ITestMultiWordService : IDynamicApiService +{ + Task GetValueAsync(); +} + +/// +/// Mock 带 DescriptionAttribute 的接口。 +/// +public interface ITestAnnotatedService : IDynamicApiService +{ + [Description("执行自定义操作")] + Task DoSomething(); +} + +/// +/// Mock 动态 API 服务接口(含简单参数方法)。 +/// +public interface IMockTestService : IDynamicApiService +{ + [Description("获取数据")] + Task GetDataAsync(); + + [Description("更新数据")] + Task UpdateDataAsync(int id, string value); +} + +/// +/// Mock 动态 API 服务实现。 +/// +public class MockTestServiceImpl : IMockTestService +{ + public Task GetDataAsync() => Task.FromResult("mock data"); + public Task UpdateDataAsync(int id, string value) => Task.CompletedTask; +} + +/// +/// Mock ITaskService 实现(桩实现,仅用于验证工具注册,不执行实际逻辑)。 +/// +public class MockTaskService : ITaskService +{ + public Task> GetAllTasksAsync() => Task.FromResult(new List()); + public Task GetTaskByIdAsync(int id) => Task.FromResult(null); + public Task> GetActiveTasksAsync() => Task.FromResult(new List()); + public Task> GetCompletedTasksAsync() => Task.FromResult(new List()); + public Task CreateTaskAsync(CreateTaskDto dto) => Task.FromResult(new TaskDto()); + public Task UpdateTaskAsync(UpdateTaskDto dto) => Task.FromResult(new TaskDto()); + public Task ToggleCompleteAsync(int id) => Task.FromResult(new TaskDto()); + public Task DeleteTaskAsync(int id) => Task.CompletedTask; + public Task> GetSubTasksAsync(int parentTaskId) => Task.FromResult(new List()); +} + +#endregion diff --git a/src/Hua.Todo.Tests/Hua.Todo.Tests.csproj b/src/Hua.Todo.Tests/Hua.Todo.Tests.csproj index c101ce2..6eb6779 100644 --- a/src/Hua.Todo.Tests/Hua.Todo.Tests.csproj +++ b/src/Hua.Todo.Tests/Hua.Todo.Tests.csproj @@ -28,4 +28,12 @@ + + + + + + + + \ No newline at end of file