Compare commits

3 Commits

Author SHA1 Message Date
ShaoHua 9223ceca50 feat(mcp): 新增 MCP 服务基础设施,重构规则文件序号,新增 v1.3.0 工单文档
- 规则重组:全局/ 下 8 个规则合并为 6 个(01+02→01,05+06→04),序号顺延

- 新增项目规则 05-多入口功能同步规范(UI/语音入口覆盖检查)

- 新增 MCP 服务基础设施:Mcp/ 目录(DI 注册、端点扩展、动态工具描述符)、单元测试

- v1.3.0 工单文档:03 系列(会议任务拆分)、04(富文本描述与附件管理)

- MCP 接口与前端集成指南:docs/manual/08、09
2026-06-16 01:15:40 +08:00
ShaoHua aacc56e952 docs: 修正AI沟通记录规范为按主题归档,合并同主题多轮对话 2026-06-16 00:26:34 +08:00
ShaoHua 8b0b2cb197 docs: 新增AI沟通记录规范,清理旧规则文件引用
- 新增 08-AI沟通记录规范.md,约定沟通记录存储结构与生命周期

- 新增 docs/AI沟通记录/ 目录及首批沟通记录

- 删除已迁移的旧规则文件 commenting.md、documentation_sync.md

- 更新索引与智能体记忆文件
2026-06-16 00:22:59 +08:00
33 changed files with 4043 additions and 412 deletions
-33
View File
@@ -1,33 +0,0 @@
---
alwaysApply: true
description: 强制项目注释规范(C# / TypeScript):新增或修改代码必须补全必要注释,便于维护与跨平台开发。
---
# 注释规范(必须遵守)
## 通用
- 新增或修改的代码必须包含足够注释,使“不了解该模块的人”也能理解其职责、边界与关键决策。
- 优先使用 **XML 文档注释**`///`),而不是随意的行内注释。
- 不允许无意义注释(例如“初始化变量”“进入方法”)。注释必须解释“为什么/约束/边界/副作用”。
- 不允许出现“TODO/FIXME”但无上下文或无处理方案的注释。
## C#.NET / MAUI
- 所有 `public` / `protected`**类、接口、方法、属性** 必须提供 XML 文档注释,至少包含:
- `summary`:一句话说明用途
- 对关键参数/返回值:`param` / `returns`
- 对异常或副作用:在 `summary` 中明确说明(例如会注册系统钩子/会启动后台服务)
-**跨平台逻辑**
- 禁止在同一文件内混写多个平台的大段 `#if` 实现;应优先使用 `partial`、接口与平台目录分离。
- 平台分离后的公共入口处必须说明“平台差异在哪里、默认实现是什么、为什么这么做”。
-**异步/后台任务**
- 必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入。
-**安全/隐私**
- 禁止在日志或注释中输出密钥、Token、用户隐私信息。
## TypeScript / Vue(前端)
- 对导出的函数/类型必须有注释,解释用途与输入输出。
- 对“与后端/MAUI 交互”的协议字段(例如全局变量、事件名)必须注释说明来源与约束。
-34
View File
@@ -1,34 +0,0 @@
---
alwaysApply: true
description: 强制文档同步规范:每次变更代码(如新增功能、修改接口、调整架构等)必须同步更新 README.md 和 docs 目录下的相关文档。
---
# 文档同步规范(必须遵守)
## 通用原则
- **代码即文档,文档随代码**:文档不是静态的,它必须真实反映当前代码的状态。
- **及时性**:在提交代码变更的同时(或紧随其后),必须完成相关文档的更新。
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
- **协作友好(局部修改)**:当并行处理多个任务/需求时,更新文档应尽量只修改与本任务直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
## 更新范围
- **README.md**
- 如果变更涉及核心功能点(Features)、安装步骤(Installation)、快速开始(Quick Start)或 API 端点(API Endpoints),必须同步更新。
- 变更涉及技术栈调整或项目结构变化时需更新。
- **docs/ 目录文档**
- **接口变更**:若修改了 API,需同步更新 [技术设计文档](docs/技术设计文档.md) 中的接口部分。
- **功能新增/调整**:需在 [产品需求文档](docs/产品需求文档.md) 和 [技术栈与模块](docs/技术栈与模块.md) 中体现。
- **架构/模式变更**:需更新 [技术设计文档](docs/技术设计文档.md)。
- **代码规范**:若引入了新的编码模式或工具,需更新 [代码规范文档](docs/代码规范文档.md)。
- **版本记录**:所有非琐碎的变更必须在 [版本记录.md](docs/版本记录.md) 中添加记录。
## 检查清单
1. [ ] 是否有新增的 API 端点?(更新 README 和技术设计文档)
2. [ ] 是否修改了现有的业务逻辑或数据结构?(更新技术设计文档)
3. [ ] 是否有新增的功能模块?(更新产品需求文档和技术栈说明)
4. [ ] 是否调整了开发环境或依赖?(更新 README)
5. [ ] 是否在 [版本记录.md](docs/版本记录.md) 中记录了本次变更?
6. [ ] 文档变更是否保持“局部修改”,只影响与本任务相关的段落/小节?(避免无关重排/改写)
@@ -1,17 +1,46 @@
# 智能体记忆与规范同步规则(必须遵守) # 智能体记忆与存储规范(必须遵守)
> 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆与同步规范,与具体项目业务无关。 > 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆、存储与同步规范,与具体项目业务无关。
> >
> `.trae/` 整体目录结构与各子目录职责见 [.trae/索引.md](../../索引.md),本文件不再重复描述。 > `.trae/` 整体目录结构与各子目录职责见 [.trae/索引.md](../../索引.md),本文件不再重复描述。
## 记忆存储 ## 一、记忆存储
### 1.1 存储位置与组织
- 智能体的记忆必须存放在 `.trae/memory` 文件夹中 - 智能体的记忆必须存放在 `.trae/memory` 文件夹中
- 记忆应按对话日期或主题进行组织,便于后续查询和参考 - 记忆应按对话日期或主题进行组织,便于后续查询和参考
- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息 - 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息
- **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.trae/rules/项目/04-即时状态记忆.md`,便于其他智能体或开发者快速对齐 - **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.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` 目录下的对应文件中
- **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/` - **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/`
@@ -19,11 +48,11 @@
- 若涉及到新的规范或规则,应创建新的规则文件进行记录 - 若涉及到新的规范或规则,应创建新的规则文件进行记录
- 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践 - 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践
## 文件命名规则(强制) ## 三、文件命名规则(强制)
`.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规: `.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规:
- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `08-XXX规范.md` - **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `06-XXX规范.md`
- **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复 - **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复
- **入口/索引文件例外**`.trae/索引.md` 这类目录入口文件不带序号 - **入口/索引文件例外**`.trae/索引.md` 这类目录入口文件不带序号
- **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾 - **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾
@@ -34,18 +63,18 @@
| 子目录 | 当前最大序号 | 下一个可用 | | 子目录 | 当前最大序号 | 下一个可用 |
|---|---|---| |---|---|---|
| `rules/全局/` | 07 | 08 | | `rules/全局/` | 06 | 07 |
| `rules/项目/` | 04 | 05 | | `rules/项目/` | 05 | 06 |
| `memory/` | 01 | 02 | | `memory/` | 01 | 02 |
| `coordination/` | 02 | 03 | | `coordination/` | 02 | 03 |
## 实现要求 ## 四、实现要求
- 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致 - 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致
- 当发现规范冲突或需要调整时,应及时记录并通知相关人员 - 当发现规范冲突或需要调整时,应及时记录并通知相关人员
- 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程 - 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程
## 路径规范 ## 五、路径规范
- 所有 Markdown 文档中不应使用绝对路径,应使用相对路径 - 所有 Markdown 文档中不应使用绝对路径,应使用相对路径
- 相对路径应以项目根目录为基准,例如 `.trae/memory` 而非绝对路径 - 相对路径应以项目根目录为基准,例如 `.trae/memory` 而非绝对路径
@@ -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/` 章节同步追加链接
## 访问权限
- 记忆文件仅供开发团队内部参考使用
- 确保记忆文件中的敏感信息得到适当保护
- 遵循项目的版本控制和代码管理规范
@@ -14,7 +14,7 @@ description: 强制文档同步规范:每次变更代码(如新增功能、
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。 - **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
- **协作友好(局部修改)**:当并行处理多个研发工单/需求时,更新文档应尽量只修改与本工单直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。 - **协作友好(局部修改)**:当并行处理多个研发工单/需求时,更新文档应尽量只修改与本工单直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
> 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [05-研发工单规则.md](./05-研发工单规则.md)。 > 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [04-研发工单全流程规范.md](./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)
@@ -7,7 +7,7 @@ description:
> 适用范围:本规则属于 **全局规则**(跨项目通用)。 > 适用范围:本规则属于 **全局规则**(跨项目通用)。
> ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。 > ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。
> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。 > 详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。
> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。 > 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。
## 适用范围 ## 适用范围
-129
View File
@@ -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\<module>\<file>`
### 共享文件判定标准(满足其一即为共享)
- 项目入口 / 启动逻辑、依赖注入注册、全局路由
- 公共配置、公共协议与 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`"任务"歧义,禁用)
@@ -0,0 +1,79 @@
# AI沟通记录规范(必须遵守)
> 适用范围:本规则属于 **全局规则**(跨项目通用),约束用户与智能体之间沟通记录的存储与管理。
## 一、存储结构
- AI沟通记录存放在 `docs/AI沟通记录/` 目录下
- **按主题归档**:同一主题的多轮对话追加在同一个 `NN-主题摘要.md` 文件中,只有新主题才新建文件
- 文件命名格式:`NN-主题摘要.md``NN` 为两位序号)
- 序号从 `01` 开始,每次新建递增 +1,不得跳号或重复
- 智能体在新建沟通记录时,默认使用当前最大序号 +1
- 当对话属于已有主题时,应在对应文件的"详细记录"章节追加子主题,而非新建文件
## 二、目录结构
```
docs/AI沟通记录/
├── 01-首次项目分析.md
├── 02-架构讨论.md
└── NN-主题摘要.md
```
- 每个文件即一次对话的完整记录
- 如有附件或补充材料,可在同目录下新建 `NN-主题摘要/` 子文件夹存放
## 三、记录内容规范
每个沟通记录文件必须包含以下结构:
```markdown
# AI沟通记录:NN-主题摘要
- **日期**YYYY-MM-DD
- **参与者**:用户、AI 助手
- **会话序号**NN
## 讨论主题
[简要描述本次对话的核心议题]
## 关键决策
[记录对话中达成的重要决策]
## 待办事项
[对话中确认的后续行动项]
## 详细记录
### NN-子主题1
[内容]
### NN-子主题2
[内容]
```
- **详细记录中的子主题必须带序号前缀**,格式为 `### NN-子主题名称``NN` 为两位序号,从 01 开始)
- 子主题序号用于精确引用,例如"沟通记录 03 的 02-xxx"
## 四、序号管理与引用
- 智能体在对话中可通过序号引用历史沟通记录,例如"参见沟通记录 03"
- 用户也可通过序号要求智能体回顾某次对话内容
- 序号一旦分配不可变更,即使删除了某个记录文件夹,也不得复用其序号
## 五、与记忆系统的边界
| 系统 | 用途 | 更新频率 |
|---|---|---|
| `docs/AI沟通记录/` | 对话过程与决策的完整记录 | 每次对话新建 |
| `.trae/memory/` | 长期沉淀的关键决策与经验 | 按需追加 |
| `.trae/rules/项目/04-即时状态记忆.md` | 当前快照(进度、临时决策) | 高频更新 |
- 三者不要重复存放同一份信息
- `docs/AI沟通记录/` 偏向**对话过程的完整存档**
- `.trae/memory/` 偏向**长期沉淀的提炼结论**
## 六、检查清单
1. [ ] 新建沟通记录时,序号是否为当前最大值 +1?
2. [ ] 文件名是否为 `NN-主题摘要.md` 格式?
3. [ ] 记录内容是否包含日期、参与者、讨论主题等必要字段?
4. [ ] 是否与 `.trae/memory/` 和即时状态记忆避免重复存储?
@@ -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` 标注"已完成",并在"待验证表"里更新状态?
+1 -1
View File
@@ -3,7 +3,7 @@
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。 > 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
> >
> 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。 > 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。
> 全局术语规则参见 [.trae/rules/全局/05-研发工单规则.md](../全局/05-研发工单规则.md)。 > 全局术语规则参见 [.trae/rules/全局/04-研发工单全流程规范.md](../全局/04-研发工单全流程规范.md)。
## 一、术语对照(核心) ## 一、术语对照(核心)
+19 -5
View File
@@ -11,8 +11,9 @@
## 一、当前活跃版本 ## 一、当前活跃版本
- **进行中版本**v1.2.0 - **进行中版本**v1.2.0(收尾中)、v1.3.0(规划中)
- **研发工单总览**[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md) - **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) - **PRD**[docs/project/产品需求文档-1.2.0.md](../../../docs/project/产品需求文档-1.2.0.md)
## 二、v1.2.0 工单状态快照 ## 二、v1.2.0 工单状态快照
@@ -31,14 +32,27 @@
| 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` | | 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` |
| 09 - CloudSync 同步策略改进 | 已实现 | 待验证 | TaskEntity 继承 ABP 基类;软删除修复(SaveChangesAsync);前端类型和 cloudSync.ts 已更新;新增 guid.ts | | 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` 暴露。 - **MAUI 端不暴露云同步端点**:`MauiProgram.cs` 仅注册 `AddApplicationServices()`,不调 `AddCloudSyncServer()`。云同步端点只在 `Hua.Todo.Host` 暴露。
- **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。 - **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。
- **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。 - **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。
- **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。 - **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。
## 、已知未完结事项 / 待办 ## 、已知未完结事项 / 待办
- [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证 - [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证
- [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现 - [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现
@@ -46,7 +60,7 @@
- [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证" - [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证"
- [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试) - [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试)
## 、最近一次重大重构(如有) ## 、最近一次重大重构(如有)
- **术语统一与目录中文化**(2026-06): - **术语统一与目录中文化**(2026-06):
- `.trae/rules/` 全部中文文件名 + 拆分为 `全局/``项目/` 两个子目录 - `.trae/rules/` 全部中文文件名 + 拆分为 `全局/``项目/` 两个子目录
@@ -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) 的第七章"与其他工单的语音入口衔接"。
+11 -11
View File
@@ -8,9 +8,9 @@
> >
> 文件命名约定:每个子目录内文件以 `NN-名称.md` 格式编号(两位数字),编号反映**阅读优先级 / 依赖顺序**。索引文件本身不带序号。 > 文件命名约定:每个子目录内文件以 `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,13 +32,12 @@
| 文件 | 职责 | | 文件 | 职责 |
|---|---| |---|---|
| [01-智能体记忆.md](./rules/全局/01-智能体记忆.md) | 智能体记忆/规范同步机制(**不含目录总览,由本索引文件承担** | | [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md) | 智能体记忆/存储/规范同步机制 & `.trae/memory/` 使用方式 & 文件命名规则(含序号速查表 |
| [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md) | `.trae/memory/` 的使用方式 | | [02-注释规范.md](./rules/全局/02-注释规范.md) | C# / TypeScript / Vue 代码注释要求 |
| [03-注释规范.md](./rules/全局/03-注释规范.md) | C# / TypeScript / Vue 代码注释要求 | | [03-文档同步规范.md](./rules/全局/03-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 |
| [04-文档同步规范.md](./rules/全局/04-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 | | [04-研发工单全流程规范.md](./rules/全局/04-研发工单全流程规范.md) | 研发工单术语定义 + 拆分输出规范 + 新增工单约束(合并原 05/06/09) |
| [05-研发工单规则.md](./rules/全局/05-研发工单规则.md) | 研发工单术语与边界(编码工作项 vs Todo 待办项) | | [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
| [06-研发工单拆分规范.md](./rules/全局/06-研发工单拆分规范.md) | 工单拆分目录与文件结构 | | [06-AI沟通记录规范.md](./rules/全局/06-AI沟通记录规范.md) | 用户与智能体沟通记录的存储目录、序号管理与内容规范 |
| [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
--- ---
@@ -50,6 +49,7 @@
| [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 | | [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 |
| [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 | | [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 |
| [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 | | [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 |
| [05-多入口功能同步规范.md](./rules/项目/05-多入口功能同步规范.md) | 新增功能时必须同步确认 UI 入口与语音控制入口的覆盖情况 |
--- ---
@@ -57,7 +57,7 @@
- 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照 - 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照
- 与"项目即时状态"的边界:[04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) 偏当前快照、更新频率高;`memory/` 偏长期保留 - 与"项目即时状态"的边界:[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):依赖小版本号、历史决策、变更时间轴 - [01-project_memory.md](./memory/01-project_memory.md):依赖小版本号、历史决策、变更时间轴
@@ -66,7 +66,7 @@
## coordination/ — 多窗口协作运行时目录 ## coordination/ — 多窗口协作运行时目录
- 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**) - 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**)
- 协作协议详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) - 协作协议详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md)
- 文件: - 文件:
- [00-README.md](./coordination/00-README.md):目录约定 - [00-README.md](./coordination/00-README.md):目录约定
- [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记 - [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记
@@ -0,0 +1,57 @@
# AI沟通记录:01-规则沟通记录体系建立
- **日期**2026-06-16
- **参与者**:用户、AI 助手
- **会话序号**01
## 讨论主题
建立 AI 沟通记录体系:在全局规则中新增规范,在 `docs/` 下建立存储目录。
## 关键决策
1. AI沟通记录规则文件编号为 `08-AI沟通记录规范.md`,存放在 `.trae/rules/全局/`
2. AI沟通记录存放在 `docs/AI沟通记录/` 目录下
3. 按主题归档:同一主题的多轮对话追加在同一个 `NN-主题摘要.md` 文件中,而非每轮对话新建文件
4. 新主题才新建文件,序号从 01 递增,不可复用
5. 详细记录中的子主题必须带序号前缀(如 `01-xxx``02-xxx`),便于精确引用
## 待办事项
- [x] 创建 `.trae/rules/全局/08-AI沟通记录规范.md`
- [x] 创建 `docs/AI沟通记录/01-规则沟通记录体系建立.md`
- [x] 更新 `.trae/索引.md` 同步新增内容
- [x] 更新 `.trae/rules/全局/01-智能体记忆.md` 中的序号速查表
## 详细记录
### 01-用户初始请求
用户提出了两个明确要求:
1. 将AI沟通记录相关规则添加到全局规则目录
2. 建立 `docs/` 下的AI沟通记录存储体系,带序号编号,方便后续引用
### 02-过程中的纠正
AI 助手初始方向偏离,误以为需要读取源代码进行分析。用户明确纠正:此项工作与源代码无关,应直接操作规则文件等内容。
### 03-命名修正
用户指出"通信记录"名称不准确,应改为"AI沟通记录"。随即统一更新了规则文件名、目录名、索引引用及记录内容中的所有措辞。
### 04-目录结构修正
用户指出当前的子文件夹结构(`NN-主题摘要/记录.md`)不正确,应改为直接的单文件结构(`NN-主题摘要.md`)。
同时文件夹名"规则与沟通记录体系建立"应去掉"与"字,改为"规则沟通记录体系建立"。
### 05-归档方式修正
用户指出沟通记录应按主题归档,而非按对话次数新建文件。同一主题的多轮对话应追加在同一个文件中,只有新主题才新建文件。
此前错误地创建了 `02-沟通记录目录结构修正.md`,这属于同一主题的后续对话,应合并到 `01-规则沟通记录体系建立.md` 中。
### 06-最终方案
- 全局规则文件:`.trae/rules/全局/08-AI沟通记录规范.md`
- AI沟通记录目录:`docs/AI沟通记录/`
- 记录格式:`NN-主题摘要.md`(按主题归档,同主题多轮对话追加在同一文件)
- 引用方式:通过序号引用,如"参见沟通记录 01"
+389
View File
@@ -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-26Streamable 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
+210
View File
@@ -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/*`)基础上,新增了 MCPModel 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://<host>: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<Client> {
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<void> {
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()`
@@ -6,12 +6,13 @@
## 一、背景与目标 ## 一、背景与目标
Hua.Todo v1.3.0 版本聚焦于个核心能力的升级: Hua.Todo v1.3.0 版本聚焦于个核心能力的升级:
| 序号 | 能力 | 描述 | | 序号 | 能力 | 描述 |
|---|---|---| |---|---|---|
| 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCPModel Context Protocol)服务,提升服务调用效率与可扩展性 | | 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCPModel Context Protocol)服务,提升服务调用效率与可扩展性 |
| 2 | **语音交互** | 实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项 | | 2 | **语音控制与 AI 辅助** | 通过语音指令操作 Todo 待办项(CRUD + 子任务),通过 LLM 提供 AI 辅助任务拆分建议 |
| 3 | **会议任务拆分** | 以"会议"为入口记录会议内容(录音/文字),通过 AI 分析自动生成待办项拆分建议,用户确认后批量创建 |
--- ---
@@ -20,11 +21,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
### 2.1 并行工单(可同步执行) ### 2.1 并行工单(可同步执行)
| 工单编号 | 标题 | 负责人 | 状态 | | 工单编号 | 标题 | 负责人 | 状态 |
|---|---|---|---| |---|---|---|---|---|---|---|
| 01 | HTTP 服务转换为 MCP 服务 | - | 待开始 | | 01 | HTTP 服务转换为 MCP 服务 | - | 进行中 |
| 02 | 语音通话与语音控制功能 | - | 待开始 | | 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 服务可正常对外提供接口 - MCP 服务可正常对外提供接口
- 所有原有 HTTP API 功能在 MCP 服务中可正常使用 - 所有原有 HTTP API 功能在 MCP 服务中可正常使用
### 3.2 工单 02 - 语音通话与语音控制功能 ### 3.2 工单 02 - 语音控制与 AI 辅助
**目标**:实现语音通话数据传输与语音控制功能 **目标**:实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能
**核心需求** **核心需求**
- 语音通话数据传输能力 - 语音输入(STT):平台原生语音识别 → 文字
- 语音指令识别与解析 - 语音播报(TTS):执行结果语音反馈
- 语音控制 Todo 待办项操作(创建、编辑、删除、完成等 - 语音指令解析与执行(CRUD + 子任务 + 歧义处理
- 与现有业务系统对接 - AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
**不包含**
- 语音通话功能(场景不明确,本期不做)
**验收标准** **验收标准**
- 语音通话功能可正常使用 - 各平台 STT/TTS 可正常工作
- 语音控制可准确执行 Todo 业务操作 - 语音指令可准确执行 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 | MCP 服务可用性测试 | 待验证 | - | | 01 | MCP 服务可用性测试 | 待验证 | - |
| 01 | 原有 API 功能兼容性 | 待验证 | - | | 01 | 原有 API 功能兼容性 | 待验证 | - |
| 02 | 语音通话连接测试 | 待验证 | - | | 02 | STT/TTS 平台适配 | 待验证 | Windows 优先,其他平台后续 |
| 02 | 语音指令识别准确率 | 待验证 | - | | 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 服务框架 | | 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 | 已就绪 | 平台内置 | | TRAE MCP SDK | 已就绪 | 平台内置 |
| 语音识别服务 | 已就绪 | 平台内置 | | 各平台原生 STT/TTS API | 已就绪 | 平台内置 |
| Hua.Todo v1.2.0 | 已完成 | 上一版本 | | Hua.Todo v1.2.0 | 已完成 | 上一版本 |
| LLM APIAI 拆分) | 待确认 | Host 端调用 |
| 浏览器 MediaRecorder API | 已就绪 | 前端录音 |
| 工单 02 LlmClientService | 待实现 | 会议拆分复用 |
--- ---
@@ -99,9 +192,15 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
| 风险 | 影响 | 应对策略 | | 风险 | 影响 | 应对策略 |
|---|---|---| |---|---|---|
| MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 | | MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 |
| 语音识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 | | 平台 STT 识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 |
| Linux STT 可用性差 | Linux 语音控制不可用 | Web Speech API 降级;或 `vosk` 离线模型 |
| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,语音指令其他部分不受影响 |
| 会议录音文件过大 | 上传超时/存储压力 | 前端限制最长 2 小时;压缩音频格式 |
| 浏览器 MediaRecorder 兼容性 | 部分平台录音不可用 | 降级提示使用文字输入 |
| STT 转写准确率不足 | 会议纪要质量差 | 转写后支持用户编辑修正 |
--- ---
**创建日期**2026-06-15 **创建日期**2026-06-15
**修订日期**2026-06-16
**版本**v1.3.0 **版本**v1.3.0
@@ -1,23 +1,25 @@
# 研发工单 v1.3.0 - 02 语音通话与语音控制功能 # 研发工单 v1.3.0 - 02 语音控制与 AI 辅助
--- ---
## 一、目标与范围 ## 一、目标与范围
### 1.1 目标 ### 1.1 目标
实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项,提升用户交互体验 实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能,通过平台原生 STT/TTS 实现语音输入输出,通过 LLM 意图解析处理自然语言指令,离线降级到规则匹配,通过 LLM 提供 AI 拆分建议
### 1.2 范围 ### 1.2 范围
**包含** **包含**
- 语音通话数据传输能力 - 语音输入(STT):平台原生语音识别 → 文字
- 语音指令识别与解析 - 语音播报(TTS):执行结果语音反馈
- 语音控制 Todo 待办项操作 - 语音指令解析与执行(CRUD + 子任务操作
- 与现有业务系统对接 - 指令歧义处理(目标不唯一时返回候选列表)
- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
**不包含** **不包含**
- 语音通话 UI 界面设计(仅提供能力层 - 语音通话功能(场景不明确,本期不做
- 第三方语音服务集成(使用平台内置能力 - 前端语音 UI 设计(仅提供能力层与 API
- 第三方语音服务集成(使用平台原生能力)
--- ---
@@ -26,109 +28,426 @@
| 条件 | 说明 | | 条件 | 说明 |
|---|---| |---|---|
| Hua.Todo v1.2.0 | 已完成,提供基础业务能力 | | Hua.Todo v1.2.0 | 已完成,提供基础业务能力 |
| 平台语音服务 | 已就绪 | | 平台原生 STT/TTS | Windows`Windows.Media.SpeechRecognition`/`SpeechSynthesis`)、Android`SpeechRecognizer`/`TextToSpeech`)、iOS/macOS`SFSpeechRecognizer`/`AVSpeechSynthesizer`)、LinuxWebKitGTK Web Speech API / `vosk` 离线模型) |
| 工单 01(可选) | MCP 服务就绪后可通过 MCP 调用 | | LLM API | 在线模式意图解析 + AI 拆分共用;API Key 在 Host 端管理 |
| 工单 01(可选) | MCP 服务就绪后,语音指令与 AI 拆分也可通过 MCP 暴露 |
--- ---
## 三、需求规格 ## 三、架构设计
### 3.1 语音通话数据传输 ### 3.1 整体链路
**功能描述**:支持语音通话数据的实时传输
**接口设计**
```
工具名:startVoiceCall
参数:
- target: string - 通话目标标识
返回:
- callId: string - 通话 ID
- status: string - 通话状态(connected/disconnected
```
``` ```
工具名:endVoiceCall 平台 STT(语音→文字)
参数:
- callId: string - 通话 ID IVoiceInputServiceCore 接口,各平台实现)
返回: ↓ 回调文字到前端
- success: boolean - 是否成功结束 WebView → POST /api/voice/command { text: "帮我把那个开会的删了吧" }
Host API → IVoiceIntentParser(双策略)
├─ 在线:LlmIntentParser(调 LLM,输出结构化意图+参数)
└─ 离线:RuleIntentParser(关键词规则匹配,覆盖高频指令)
意图 + 参数 → 判断歧义
├─ 无歧义 → 调用 TaskService 执行 → TTS 播报结果
├─ 有歧义 → 返回候选列表 → 前端展示 → 用户确认 → 再执行
└─ UNKNOWN → TTS 播报"没听懂,请再说一次"
``` ```
### 3.2 语音指令控制 ### 3.2 STT/TTS 分层策略
**功能描述**:支持通过语音指令操作 Todo 待办项 遵循与全局快捷键相同的平台分离模式(接口+平台目录):
**支持的语音指令** | 层 | 职责 | 位置 |
| 指令类型 | 示例指令 | 对应操作 |
|---|---|---| |---|---|---|
| 创建任务 | "创建任务 开会" | 创建标题为"开会"的任务 | | `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 方案)
| 添加子任务 | "给任务开会添加子任务 准备PPT" | 为任务添加子任务 |
采用 **LLM 主解析 + 规则降级** 的混合方案:
#### 策略 A:在线 — LLM 意图解析(LlmIntentParser
在线模式下,将用户原始文字发送给 LLM,通过 system prompt 约束输出为结构化 JSON
**接口设计**
``` ```
工具名:executeVoiceCommand System Prompt(精简版):
参数 你是 Hua.Todo 的语音指令解析器。根据用户输入,输出以下 JSON 格式
- command: string - 语音指令文本 {
返回: "intent": "CREATE|UPDATE|DELETE|COMPLETE|UNCOMPLETE|QUERY|ADD_SUBTASK|AI_BREAKDOWN|UNKNOWN",
- success: boolean - 是否执行成功 "params": { ... },
- message: string - 执行结果描述 "confidence": 0.0-1.0
- data: object - 返回数据(如任务列表) }
意图说明:
- 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 输入:"帮我把那个开会的任务删了吧"
参数:无 LLM 输出:
返回: {
- isListening: boolean - 是否正在监听 "intent": "DELETE",
- isSpeaking: boolean - 是否正在播报 "params": { "targetTitle": "开会" },
"confidence": 0.95
}
``` ```
``` ```
工具名:speakText 输入:"这个项目太大了,帮我拆一下"
参数 LLM 输出
- text: string - 要播报的文本 {
返回: "intent": "AI_BREAKDOWN",
- success: boolean - 是否成功 "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
/// <summary>语音意图解析器接口</summary>
public interface IVoiceIntentParser
{
/// <summary>解析语音指令文本,返回意图与参数</summary>
Task<VoiceIntentResult> ParseAsync(string text, CancellationToken ct = default);
}
/// <summary>双策略解析器:在线走 LLM,离线走规则</summary>
public class HybridVoiceIntentParser : IVoiceIntentParser
{
private readonly LlmIntentParser _llmParser;
private readonly RuleIntentParser _ruleParser;
private readonly IConnectivityService _connectivity;
public async Task<VoiceIntentResult> 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
├─ 在线:LlmIntentParserLLM 结构化输出)
└─ 离线/降级:RuleIntentParser(关键词正则匹配)
意图 + 参数 + confidence
├─ confidence >= 阈值 且 目标唯一 → 执行 → TTS 播报
├─ 目标匹配多个 → 返回候选列表 → 用户确认 → 执行
├─ confidence < 阈值 → TTS "您是要...吗?" → 用户确认
└─ UNKNOWN → TTS "没听懂,请再说一次"
``` ```
--- ---
## 四、验收标准 ## 四、需求规格
### 4.1 语音输入(STT
各平台通过原生 API 将语音转为文字,通过 `IVoiceInputService` 接口统一暴露:
```csharp
/// <summary>语音输入服务接口,各平台实现原生 STT</summary>
public interface IVoiceInputService
{
/// <summary>是否正在监听</summary>
bool IsListening { get; }
/// <summary>开始语音识别,识别结果通过回调返回</summary>
Task StartListeningAsync(Action<string> onResult, Action<string>? onError = null);
/// <summary>停止语音识别</summary>
Task StopListeningAsync();
}
```
### 4.2 语音播报(TTS
```csharp
/// <summary>语音输出服务接口,各平台实现原生 TTS</summary>
public interface IVoiceOutputService
{
/// <summary>是否正在播报</summary>
bool IsSpeaking { get; }
/// <summary>播报文本</summary>
Task SpeakAsync(string text);
/// <summary>停止播报</summary>
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 工具
---
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 | | 验收项 | 验证方法 | 预期结果 |
|---|---|---| |---|---|---|
| 创建任务指令 | 说出"创建任务 测试" | 成功创建标题为"测试"的任务 | | STT 语音输入 | 说话后检查回调文字 | 文字识别正确 |
| 创建带优先级任务 | 说出"创建高优先级任务 紧急" | 成功创建高优先级任务 | | TTS 语音播报 | 执行指令后检查播报 | 播报内容与执行结果一致 |
| 完成任务指令 | 说出"完成任务 测试" | 任务状态变为已完成 | | 创建任务(在线) | "帮我把开会的任务加上" | LLM 解析为 CREATE,成功创建 |
| 删除任务指令 | 说出"删除任务 测试" | 任务被成功删除 | | 创建任务(离线降级) | "创建任务 测试" | 规则匹配为 CREATE,成功创建 |
| 查询任务指令 | 说出"查询未完成任务" | 返回未完成任务列表 | | 创建带优先级任务 | "建一个高优先级任务 紧急" | 成功创建高优先级任务 |
| 语音播报 | 调用 speakText | 成功播放指定文本 | | 完成任务指令 | "把那个开会的事标完成" | LLM 解析为 COMPLETE,执行成功 |
| 语音通话 | 调用 startVoiceCall | 通话连接成功 | | 取消完成指令 | "取消完成 测试" | 任务状态恢复为未完成 |
| 删除任务指令 | "帮我把测试删了" | LLM 解析为 DELETE,删除成功 |
| 更新任务指令 | "把测试改成验收" | 任务标题更新 |
| 查询任务指令 | "有哪些没做完的" | LLM 解析为 QUERY,返回列表 |
| 添加子任务指令 | "给开会加个子任务 准备PPT" | 子任务创建成功 |
| 歧义处理 | 多个任务含"开会"时说"完成开会" | 返回候选列表,不直接执行 |
| AI 拆分建议 | "帮我拆分 开会" | 返回子任务建议列表 |
| AI 拆分确认 | 用户勾选后确认 | 仅创建勾选的子任务 |
| 离线 AI 拆分 | 离线时说"帮我拆分" | 播报"离线模式不支持 AI 拆分" |
| 无法识别指令 | 说出无关内容 | 播报"没听懂,请再说一次" |
| LLM 降级 | 断网时使用语音 | 自动降级到规则匹配,基本 CRUD 可用 |
--- ---
## 、Touch List ## 、Touch List
| 文件路径 | 修改类型 | 说明 | | 文件路径 | 修改类型 | 说明 |
|---|---|---| |---|---|---|
| `src/Hua.Todo.Application/Voice/` | 新增 | 语音服务目录 | | `src/Hua.Todo.Core/Services/IVoiceInputService.cs` | 新增 | STT 接口定义 |
| `src/Hua.Todo.Application/Voice/VoiceService.cs` | 新增 | 语音服务实现 | | `src/Hua.Todo.Core/Services/IVoiceOutputService.cs` | 新增 | TTS 接口定义 |
| `src/Hua.Todo.Application/Voice/VoiceCommandParser.cs` | 新增 | 语音指令解析器 | | `src/Hua.Todo.Core/Services/IVoiceIntentParser.cs` | 新增 | 意图解析器接口 |
| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO | | `src/Hua.Todo.Application/Voice/LlmIntentParser.cs` | 新增 | LLM 意图解析器(在线策略) |
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 服务注册扩展 | | `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/` | 新增 | 语音相关 DTOVoiceIntentResult、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 **工单编号**02
**标题**:语音通话与语音控制功能 **标题**:语音控制与 AI 辅助
**版本**v1.3.0 **版本**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)。
@@ -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 基类迁移(可选) | 若工单 09v1.2.0)已完成,TaskEntity 已转为 ABP 基类,字段新增方式不同 |
## 四、详细规格
### 4.1 TaskType 枚举
```csharp
/// <summary>待办项类型,区分普通待办项与会议</summary>
public enum TaskType
{
/// <summary>普通待办项(默认)</summary>
Normal = 0,
/// <summary>会议:包含录音/纪要,支持 AI 任务拆分</summary>
Meeting = 1
}
```
文件位置:`src/Hua.Todo.Core/Entities/TaskType.cs`
### 4.2 TaskEntity 新增字段
```csharp
/// <summary>待办项类型</summary>
public TaskType TaskType { get; set; } = TaskType.Normal;
/// <summary>会议纪要/转写文字(仅 Meeting 类型有值)</summary>
[MaxLength(20000)]
public string? MeetingNotes { get; set; }
/// <summary>录音时长(秒),仅 Meeting 类型有值</summary>
public double? AudioDuration { get; set; }
```
### 4.3 EF Core 配置(TodoDbContext.cs
```csharp
builder.Entity<TaskEntity>(b =>
{
// ... 现有配置 ...
b.Property(x => x.TaskType)
.HasDefaultValue(TaskType.Normal)
.HasConversion<int>(); // 枚举存为整数
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
/// <summary>待办项类型(0=Normal, 1=Meeting),默认 Normal</summary>
public TaskType TaskType { get; set; } = TaskType.Normal;
```
**TaskDto** 新增:
```csharp
/// <summary>待办项类型</summary>
public TaskType TaskType { get; set; }
/// <summary>会议纪要/转写文字</summary>
public string? MeetingNotes { get; set; }
/// <summary>录音时长(秒)</summary>
public double? AudioDuration { get; set; }
```
### 4.6 会议 DTO
```csharp
/// <summary>转写请求</summary>
public class TranscribeRequest
{
public IFormFile Audio { get; set; } = null!;
public string? Format { get; set; }
}
/// <summary>转写响应</summary>
public class TranscribeResponse
{
public int TaskId { get; set; }
public string Transcript { get; set; } = string.Empty;
public double AudioDuration { get; set; }
}
/// <summary>保存会议纪要请求</summary>
public class MeetingNotesRequest
{
public string Notes { get; set; } = string.Empty;
}
/// <summary>会议纪要响应</summary>
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
/// <summary>会议业务服务</summary>
public class MeetingService
{
private readonly ITaskRepository _taskRepo;
public MeetingService(ITaskRepository taskRepo)
{
_taskRepo = taskRepo;
}
/// <summary>验证 taskId 对应的待办项存在且为 Meeting 类型</summary>
public async Task<TaskEntity> 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;
}
/// <summary>保存会议纪要</summary>
public async Task<TaskEntity> SaveNotes(int taskId, string notes)
{
var task = await GetMeetingTaskOrThrow(taskId);
task.MeetingNotes = notes;
task.UpdatedAt = DateTime.UtcNow;
await _taskRepo.UpdateAsync(task);
return task;
}
/// <summary>保存转写结果与音频时长</summary>
public async Task<TaskEntity> 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
@@ -0,0 +1,199 @@
# 研发工单 v1.3.0 - 03-02 音频录制与转写
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01API 契约)
---
## 一、目标
实现前端音频录制(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
/// <summary>音频录制器组合式函数,封装 MediaRecorder API</summary>
export function useAudioRecorder() {
// 状态
const isRecording = ref(false)
const isPaused = ref(false)
const duration = ref(0) // 秒
const audioBlob = ref<Blob | null>(null)
const audioUrl = ref<string | null>(null) // 用于预览播放
// 方法
async function startRecording(): Promise<void> // 请求麦克风权限,开始录制
function stopRecording(): void // 停止录制,生成 Blob
function resetRecording(): void // 重置状态
function getAudioBlob(): Blob | null // 获取录制结果
// 内部
let mediaRecorder: MediaRecorder | null = null
let timerInterval: number | null = null
// 音频格式:webmChrome/Firefox)、mp4Safari
// 时长限制:最长 2 小时(7200 秒)
return { isRecording, isPaused, duration, audioBlob, audioUrl,
startRecording, stopRecording, resetRecording, getAudioBlob }
}
```
### 4.3 前端 API 模块
```typescript
// src/Hua.Todo.Web/src/api/meeting.ts
/// <summary>上传音频文件并请求转写</summary>
async function transcribeAudio(taskId: number, audioBlob: Blob): Promise<TranscribeResponse>
{
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
/// <summary>语音转写服务接口</summary>
public interface ISttService
{
/// <summary>将音频文件转写为文字</summary>
/// <returns>转写文字</returns>
Task<string> 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
@@ -0,0 +1,281 @@
# 研发工单 v1.3.0 - 03-03 AI 任务拆分服务
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01API 契约)、工单 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
/// <summary>会议 AI 拆分服务,将会议文字内容分析为待办项建议</summary>
public class MeetingAiBreakdownService
{
private readonly ILlmClientService _llmClient;
private readonly MeetingService _meetingService;
private readonly ITaskService _taskService;
public MeetingAiBreakdownService(
ILlmClientService llmClient,
MeetingService meetingService,
ITaskService taskService) { ... }
/// <summary>分析会议内容,返回待办项建议列表</summary>
public async Task<List<MeetingTaskSuggestion>> 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);
}
/// <summary>确认建议并批量创建子任务</summary>
public async Task<BatchCreateResult> ConfirmAndCreateAsync(
int taskId, List<SubTaskCreateItem> subTasks, CancellationToken ct = default)
{
// 1. 验证 taskId 为 Meeting 类型
await _meetingService.GetMeetingTaskOrThrow(taskId);
// 2. 批量创建子任务
var created = new List<TaskDto>();
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
/// <summary>会议任务拆分建议</summary>
public class MeetingTaskSuggestion
{
/// <summary>建议的待办项标题</summary>
public string Title { get; set; } = string.Empty;
/// <summary>建议优先级</summary>
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
/// <summary>拆分原因(LLM 输出,用于 UI 展示)</summary>
public string Reason { get; set; } = string.Empty;
}
/// <summary>拆分请求</summary>
public class BreakdownRequest
{
/// <summary>会议纪要文字(可选,不传则使用已保存的 meetingNotes</summary>
public string? Notes { get; set; }
}
/// <summary>拆分响应</summary>
public class BreakdownResponse
{
public int TaskId { get; set; }
public List<MeetingTaskSuggestion> Suggestions { get; set; } = new();
}
/// <summary>批量创建请求中的单个子任务</summary>
public class SubTaskCreateItem
{
public string Title { get; set; } = string.Empty;
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
}
/// <summary>批量确认请求</summary>
public class ConfirmBreakdownRequest
{
public List<SubTaskCreateItem> SubTasks { get; set; } = new();
}
/// <summary>批量创建结果</summary>
public class BatchCreateResult
{
public int CreatedCount { get; set; }
public List<TaskDto> SubTasks { get; set; } = new();
}
```
### 4.4 API 端点
#### POST /api/meeting/{taskId}/breakdown
- 接收:`BreakdownRequest`
- 返回:`ApiResponse<BreakdownResponse>`
- 处理:
1. 获取会议文字(参数优先于数据库)
2. 调用 `MeetingAiBreakdownService.AnalyzeAsync()`
3. 返回建议列表
- 错误:
- 内容为空 → 400 "会议内容为空"
- 离线 → 503 "AI 拆分仅在线可用"
- taskId 非 Meeting 类型 → 400
#### POST /api/meeting/{taskId}/breakdown/confirm
- 接收:`ConfirmBreakdownRequest`
- 返回:`ApiResponse<BatchCreateResult>`
- 处理:
1. 验证 taskId 为 Meeting 类型
2. 逐条创建子任务(通过 `TaskService.CreateTaskAsync`
3. 返回创建结果
### 4.5 LLM 响应解析
```csharp
/// <summary>解析 LLM 返回的 JSON 为建议列表</summary>
private List<MeetingTaskSuggestion> ParseSuggestions(string llmResponse)
{
// 1. 清理 LLM 响应(移除可能的 markdown 代码块包裹、前后空白)
var json = CleanJsonResponse(llmResponse);
// 2. 反序列化
var result = JsonSerializer.Deserialize<LlBreakdownResponse>(json);
// 3. 验证
if (result?.Suggestions == null || result.Suggestions.Count == 0)
return new List<MeetingTaskSuggestion>();
// 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();
}
/// <summary>LLM 原始响应结构</summary>
private class LlBreakdownResponse
{
public List<MeetingTaskSuggestion> 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
@@ -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
/// <summary>待办项类型</summary>
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 中的类型
/// <summary>会议任务拆分建议</summary>
interface MeetingTaskSuggestion {
title: string;
priority: TaskPriority;
reason: string;
}
/// <summary>拆分响应</summary>
interface BreakdownResponse {
taskId: number;
suggestions: MeetingTaskSuggestion[];
}
/// <summary>待确认的子任务项</summary>
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
@@ -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
/// <summary>待办项类型枚举</summary>
public enum TaskType
{
/// <summary>普通待办项(默认)</summary>
Normal = 0,
/// <summary>会议(可包含录音、纪要,支持 AI 任务拆分)</summary>
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
@@ -0,0 +1,554 @@
# 研发工单 v1.3.0 - 04 富文本描述、附件与外部链接
> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。
---
## 一、目标与范围
### 1.1 目标
当前 Todo 待办项仅包含单行标题(`Title`),无法满足"一个待办项关联更多上下文信息"的需求。本工单为 Todo 待办项新增三项能力:
| 能力 | 描述 |
|---|---|
| **多行描述(Description** | 每个待办项可附带一段多行文字描述,用于记录详细说明、步骤、备注等 |
| **文件附件(Attachments** | 可为一个待办项关联多个本地文件,支持上传、查看、下载、删除 |
| **外部链接与程序启动** | 附件可以是外部 URL(点击在系统默认浏览器打开)或本地文件路径/可执行文件(点击通过系统关联程序打开) |
### 1.2 平台范围
| 平台 | 是否支持 | 说明 |
|---|---|---|
| WindowsMAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 |
| LinuxAvalonia | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 |
| macOSMAUI | 待定 | 视为 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 文件选择 | 前端 `<input type="file">` 能力 | 已就绪 |
| 桌面程序启动 | 通过后端 `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
/// <summary>
/// 多行描述文本(可为空)。
/// 用于记录待办项相关的详细说明、步骤或备注。
/// </summary>
public string? Description { get; set; }
```
| 字段 | 类型 | 默认值 | 约束 | 说明 |
|---|---|---|---|---|
| `Description` | `string?` | null | 最大 5000 字符 | 多行描述文本 |
### 4.2 AttachmentEntity(新增)
`Hua.Todo.Core/Entities/` 下新增 `AttachmentEntity.cs`
```csharp
/// <summary>附件类型枚举</summary>
public enum AttachmentType
{
/// <summary>本地文件(已复制到应用数据目录)</summary>
LocalFile = 0,
/// <summary>外部链接(URL</summary>
ExternalLink = 1
}
/// <summary>附件实体,表示待办项关联的文件或外部链接</summary>
public class AttachmentEntity
{
/// <summary>附件唯一标识符</summary>
public int Id { get; set; }
/// <summary>所属待办项ID</summary>
public int TaskId { get; set; }
/// <summary>显示名称(用户可见的文件名或链接标题)</summary>
public string FileName { get; set; } = string.Empty;
/// <summary>存储路径(本地文件的相对路径)或外部URL</summary>
public string FilePath { get; set; } = string.Empty;
/// <summary>文件大小(字节),外部链接为 0</summary>
public long FileSize { get; set; }
/// <summary>MIME 类型(如 text/plain、application/pdf),外部链接为空字符串</summary>
public string ContentType { get; set; } = string.Empty;
/// <summary>附件类型</summary>
public AttachmentType AttachmentType { get; set; } = AttachmentType.LocalFile;
/// <summary>创建时间(UTC</summary>
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<AttachmentEntity> 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-openLinux
// - 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 平台差异:外部程序启动
| 平台 | 实现方式 |
|---|---|
| WindowsMAUI | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` |
| LinuxAvalonia | `System.Diagnostics.Process.Start("xdg-open", path)` |
通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。
### 7.3 附件文件管理
- 上传的本地文件复制到 `<AppData>/Hua.Todo/Attachments/{attachmentId}_{originalFileName}`
- 删除附件时同步删除磁盘文件
- 删除待办项时级联删除其所有附件(实体 + 文件)
- 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示)
### 7.4 安全约束
| 约束 | 说明 |
|---|---|
| 文件类型白名单 | 不限制(本地工具场景,用户自行管理) |
| 文件大小上限 | 50MB(可配置) |
| 路径遍历防护 | 文件名去除 `../``..\` 等危险路径片段 |
| 外部链接验证 | 仅允许 `http://``https://` 协议 |
---
## 八、子工单拆分
本工单体积适中,不拆分子工单,单文件完整实现。
如需与工单 03 并行推进,注意以下共享文件:
| 共享文件 | Writer | 说明 |
|---|---|---|
| `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration04 后续追加 Description/Attachments 导航属性 |
| `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 |
| `task.ts` | **工单 03** 先写入 | 03 新增 taskType04 后追加 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
@@ -13,12 +13,23 @@
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'"> <ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">
<Compile Remove="DynamicApi\\**\\*.cs" /> <Compile Remove="DynamicApi\\**\\*.cs" />
<Compile Remove="Mcp\\**\\*.cs" />
</ItemGroup>
<!-- 测试用:-p:SkipCloudSync=true 排除 CloudSync 编译(其依赖 TaskEntity ABP 重构未完成) -->
<ItemGroup Condition="'$(SkipCloudSync)' == 'true'">
<Compile Remove="CloudSync\\**\\*.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" /> <ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
</ItemGroup> </ItemGroup>
@@ -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;
/// <summary>
/// 从 <see cref="IDynamicApiService"/> 接口自动生成 MCP 工具的扩展方法。
/// <para>
/// 新增 API 端点后无需手动添加 MCP 工具 —— 启动时自动扫描所有
/// <c>IDynamicApiService</c> 接口的公共方法,将其注册为 MCP 工具。
/// 工具名从接口名 + 方法名按约定推导(如 <c>ITaskService.GetAllTasksAsync</c>
/// → <c>task_get_all_tasks</c>),与 DynamicApi 路由推导逻辑保持一致。
/// </para>
/// </summary>
public static class DynamicMcpToolExtensions
{
/// <summary>
/// 扫描程序集中所有 IDynamicApiService 接口,
/// 将其公共方法自动注册为 MCP 工具。
/// </summary>
/// <param name="builder">MCP Server 构建器。</param>
/// <returns>MCP Server 构建器。</returns>
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;
}
/// <summary>
/// 将单个服务接口的所有公共方法注册为 MCP 工具。
/// 每个方法通过 McpServerTool.Create(MethodInfo, createTargetFunc) 注册,
/// MCP SDK 自动处理参数反序列化(包括复杂 DTO 类型)和返回值序列化。
/// </summary>
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<CallToolRequestParams> 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
/// <summary>
/// 从接口名推导服务名前缀,与 DynamicApi 中间件逻辑一致:
/// ITaskService → task, ICloudAuthService → cloud_auth
/// </summary>
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);
}
/// <summary>
/// PascalCase → snake_case。
/// </summary>
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();
}
/// <summary>
/// 去掉方法名的 Async 后缀用于工具命名。
/// </summary>
private static string StripAsyncSuffix(string name)
=> name.EndsWith("Async") ? name[..^5] : name;
/// <summary>
/// 从方法的 Description 特性或方法名前缀推导中文描述。
/// </summary>
private static string GetMethodDescription(MethodInfo method)
{
var descAttr = method.GetCustomAttribute<DescriptionAttribute>();
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
/// <summary>
/// 根据方法名前缀判断是否为只读操作。
/// </summary>
private static bool IsReadOnlyMethod(string name)
=> name.StartsWith("Get") || name.StartsWith("List")
|| name.StartsWith("Query") || name.StartsWith("Search");
/// <summary>
/// 根据方法名前缀判断是否为破坏性操作。
/// </summary>
private static bool IsDestructiveMethod(string name)
=> name.StartsWith("Delete") || name.StartsWith("Remove");
#endregion
}
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
namespace Hua.Todo.Application.Mcp;
/// <summary>
/// MCP Server 端点映射扩展方法。
/// 仅限 net10.0 目标(需要 ASP.NET Core),非 net10.0 目标时此文件不参与编译。
/// </summary>
public static class McpEndpointExtensions
{
/// <summary>
/// 将 MCP Server 端点映射到指定路径。
/// 默认路径为 /mcp,客户端通过 Streamable HTTP 协议连接。
/// </summary>
/// <param name="endpoints">端点路由构建器。</param>
/// <param name="pattern">路由路径,默认 /mcp。</param>
/// <returns>端点路由构建器。</returns>
public static IEndpointRouteBuilder MapMcpServer(this IEndpointRouteBuilder endpoints, string pattern = "/mcp")
{
endpoints.MapMcp(pattern);
return endpoints;
}
}
@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Server;
namespace Hua.Todo.Application.Mcp;
/// <summary>
/// MCP Server 依赖注入扩展方法。
/// 仅限 net10.0 目标(需要 ASP.NET Core),非 net10.0 目标时此文件不参与编译。
/// </summary>
public static class McpServiceCollectionExtensions
{
/// <summary>
/// 注册 MCP Server,并自动从所有 IDynamicApiService 接口生成 MCP 工具。
/// 调用前须已注册 AddApplicationServices。
/// <para>
/// 工具为动态关联:新增 IDynamicApiService 接口或方法后,
/// 无需手动添加 MCP 工具,重启即可自动暴露。
/// </para>
/// </summary>
/// <param name="services">服务集合。</param>
/// <returns>服务集合。</returns>
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;
}
}
+7
View File
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Hua.Todo.Application; using Hua.Todo.Application;
using Hua.Todo.Application.DynamicApi; using Hua.Todo.Application.DynamicApi;
using Hua.Todo.Application.Interfaces; using Hua.Todo.Application.Interfaces;
using Hua.Todo.Application.Mcp;
using Hua.Todo.Application.Models; using Hua.Todo.Application.Models;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -13,6 +14,9 @@ builder.Services.AddAuthorization();
builder.Services.AddApplicationServices("Data Source=Hua.Todo.db"); builder.Services.AddApplicationServices("Data Source=Hua.Todo.db");
// 注册 MCP Server(依赖 ITaskService,须在 AddApplicationServices 之后调用)
builder.Services.AddMcpServerServices();
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("AllowAll", policy => options.AddPolicy("AllowAll", policy =>
@@ -43,4 +47,7 @@ app.UseCors("AllowAll");
app.UseAuthorization(); app.UseAuthorization();
app.UseDynamicApi(); app.UseDynamicApi();
// 映射 MCP Server 端点(Streamable HTTP,路径 /mcp
app.MapMcpServer();
app.Run(); app.Run();
@@ -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;
/// <summary>
/// DynamicMcpToolExtensions 单元测试。
/// 重点验证命名推导、描述生成、行为注解等纯逻辑,以及动态工具注册。
/// </summary>
public class DynamicMcpToolExtensionsTests
{
#region
/// <summary>
/// DeriveServiceNameITaskService → task
/// </summary>
[Fact]
public void DeriveServiceName_ITaskService_ReturnsTask()
{
var result = DynamicMcpToolExtensions.DeriveServiceName(typeof(ITaskService));
Assert.Equal("task", result);
}
/// <summary>
/// DeriveServiceName:复合接口名转 snake_case。
/// </summary>
[Fact]
public void DeriveServiceName_MultiWordInterface_ReturnsSnakeCase()
{
// 假设存在 ICloudSyncService → cloud_sync
// 用动态类型模拟
var result = DynamicMcpToolExtensions.DeriveServiceName(typeof(ITestMultiWordService));
Assert.Equal("test_multi_word", result);
}
/// <summary>
/// ToSnakeCasePascalCase → snake_case
/// </summary>
[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
/// <summary>
/// 完整工具名:prefix + 方法名 = task_get_all_tasks
/// 验证 StripAsyncSuffix 和 ToSnakeCase 的组合效果。
/// </summary>
[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);
}
/// <summary>
/// StripAsyncSuffix:去掉 Async 后缀。
/// </summary>
[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
/// <summary>
/// GetMethodDescription:方法名 → 中文描述
/// </summary>
[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));
}
/// <summary>
/// GetMethodDescription:带 DescriptionAttribute 时优先使用。
/// </summary>
[Fact]
public void GetMethodDescription_UsesDescriptionAttribute()
{
var method = typeof(ITestAnnotatedService).GetMethod(nameof(ITestAnnotatedService.DoSomething))!;
var desc = GetMethodDescription(method);
Assert.Equal("执行自定义操作", desc);
}
#endregion
#region
/// <summary>
/// IsReadOnlyMethodGet/List/Query/Search 前缀返回 true。
/// </summary>
[Fact]
public void IsReadOnlyMethod_GetPrefix_ReturnsTrue()
{
Assert.True(IsReadOnlyMethod("GetAllTasksAsync"));
Assert.True(IsReadOnlyMethod("ListSubTodosAsync"));
Assert.True(IsReadOnlyMethod("QueryByDateAsync"));
Assert.True(IsReadOnlyMethod("SearchByKeywordAsync"));
}
/// <summary>
/// IsReadOnlyMethod:非只读前缀返回 false。
/// </summary>
[Fact]
public void IsReadOnlyMethod_NonReadPrefix_ReturnsFalse()
{
Assert.False(IsReadOnlyMethod("CreateTaskAsync"));
Assert.False(IsReadOnlyMethod("UpdateTaskAsync"));
Assert.False(IsReadOnlyMethod("DeleteTaskAsync"));
Assert.False(IsReadOnlyMethod("ToggleCompleteAsync"));
}
/// <summary>
/// IsDestructiveMethodDelete/Remove 前缀返回 true。
/// </summary>
[Fact]
public void IsDestructiveMethod_DeletePrefix_ReturnsTrue()
{
Assert.True(IsDestructiveMethod("DeleteTaskAsync"));
Assert.True(IsDestructiveMethod("RemoveItemAsync"));
}
/// <summary>
/// IsDestructiveMethod:非破坏性操作返回 false。
/// </summary>
[Fact]
public void IsDestructiveMethod_NonDestructive_ReturnsFalse()
{
Assert.False(IsDestructiveMethod("GetAllTasksAsync"));
Assert.False(IsDestructiveMethod("CreateTaskAsync"));
Assert.False(IsDestructiveMethod("UpdateTaskAsync"));
}
#endregion
#region
/// <summary>
/// 验证 WithDynamicApiTools 扫描 IDynamicApiService 所在程序集,能发现 ITaskService 并生成工具。
/// 工具数量 = ITaskService 的公共方法数(9 个)。
/// </summary>
[Fact]
public void WithDynamicApiTools_ScansAssemblyAndRegistersTools()
{
// Arrange
var services = new ServiceCollection();
services.AddScoped<ITaskService, MockTaskService>();
// Act - 仅注册工具到 DI,不绑定传输层
services.AddMcpServer()
.WithDynamicApiTools();
var provider = services.BuildServiceProvider();
// Assert:验证 McpServerTool 实例已注册到 DI
var tools = provider.GetServices<McpServerTool>().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);
}
/// <summary>
/// 验证生成的工具携带正确的行为注解(ReadOnly/Destructive)。
/// </summary>
[Fact]
public void WithDynamicApiTools_ToolsHaveCorrectBehaviorAnnotations()
{
// Arrange
var services = new ServiceCollection();
services.AddScoped<ITaskService, MockTaskService>();
// Act
services.AddMcpServer()
.WithDynamicApiTools();
var provider = services.BuildServiceProvider();
var tools = provider.GetServices<McpServerTool>().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<DescriptionAttribute>();
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
/// <summary>
/// Mock 多词接口,用于测试 snake_case 转换。
/// </summary>
public interface ITestMultiWordService : IDynamicApiService
{
Task<int> GetValueAsync();
}
/// <summary>
/// Mock 带 DescriptionAttribute 的接口。
/// </summary>
public interface ITestAnnotatedService : IDynamicApiService
{
[Description("执行自定义操作")]
Task<int> DoSomething();
}
/// <summary>
/// Mock 动态 API 服务接口(含简单参数方法)。
/// </summary>
public interface IMockTestService : IDynamicApiService
{
[Description("获取数据")]
Task<string> GetDataAsync();
[Description("更新数据")]
Task UpdateDataAsync(int id, string value);
}
/// <summary>
/// Mock 动态 API 服务实现。
/// </summary>
public class MockTestServiceImpl : IMockTestService
{
public Task<string> GetDataAsync() => Task.FromResult("mock data");
public Task UpdateDataAsync(int id, string value) => Task.CompletedTask;
}
/// <summary>
/// Mock ITaskService 实现(桩实现,仅用于验证工具注册,不执行实际逻辑)。
/// </summary>
public class MockTaskService : ITaskService
{
public Task<List<TaskDto>> GetAllTasksAsync() => Task.FromResult(new List<TaskDto>());
public Task<TaskDto?> GetTaskByIdAsync(int id) => Task.FromResult<TaskDto?>(null);
public Task<List<TaskDto>> GetActiveTasksAsync() => Task.FromResult(new List<TaskDto>());
public Task<List<TaskDto>> GetCompletedTasksAsync() => Task.FromResult(new List<TaskDto>());
public Task<TaskDto> CreateTaskAsync(CreateTaskDto dto) => Task.FromResult(new TaskDto());
public Task<TaskDto> UpdateTaskAsync(UpdateTaskDto dto) => Task.FromResult(new TaskDto());
public Task<TaskDto> ToggleCompleteAsync(int id) => Task.FromResult(new TaskDto());
public Task DeleteTaskAsync(int id) => Task.CompletedTask;
public Task<List<TaskDto>> GetSubTasksAsync(int parentTaskId) => Task.FromResult(new List<TaskDto>());
}
#endregion
+8
View File
@@ -28,4 +28,12 @@
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" /> <ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
</ItemGroup> </ItemGroup>
<!-- 测试用:-p:SkipCloudSync=true 排除 CloudSync 测试文件(其依赖 CloudSync 命名空间在排除后不可用) -->
<ItemGroup Condition="'$(SkipCloudSync)' == 'true'">
<Compile Remove="CloudSyncDtoTests.cs" />
<Compile Remove="CloudTaskSyncServiceTests.cs" />
<Compile Remove="CloudTaskSyncServiceSqliteTests.cs" />
<Compile Remove="TaskEntityTests.cs" />
</ItemGroup>
</Project> </Project>