Compare commits
3 Commits
dev1.2.0
...
9223ceca50
| Author | SHA1 | Date | |
|---|---|---|---|
| 9223ceca50 | |||
| aacc56e952 | |||
| 8b0b2cb197 |
@@ -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 交互”的协议字段(例如全局变量、事件名)必须注释说明来源与约束。
|
||||
|
||||
@@ -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),本文件不再重复描述。
|
||||
|
||||
## 记忆存储
|
||||
## 一、记忆存储
|
||||
|
||||
### 1.1 存储位置与组织
|
||||
|
||||
- 智能体的记忆必须存放在 `.trae/memory` 文件夹中
|
||||
- 记忆应按对话日期或主题进行组织,便于后续查询和参考
|
||||
- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息
|
||||
- **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.trae/rules/项目/04-即时状态记忆.md`,便于其他智能体或开发者快速对齐
|
||||
|
||||
## 规范同步
|
||||
### 1.2 内容规范
|
||||
|
||||
- 记忆文件应保持简洁明了,重点记录重要的开发决策和规范变更
|
||||
- 避免存储冗余信息,只记录对项目有价值的内容
|
||||
- 定期清理过时的记忆文件,保持存储空间的合理使用
|
||||
|
||||
### 1.3 与"项目即时状态"的边界
|
||||
|
||||
- `.trae/memory/` 偏向**长期保留**的对话产物与决策记录
|
||||
- `.trae/rules/项目/04-即时状态记忆.md` 偏向**当前快照**(实现进度、未完结事项),更新频率高
|
||||
- 二者不要重复存放同一份信息;以"是否需要长期沉淀"为判定标准
|
||||
|
||||
### 1.4 memory/ 生命周期
|
||||
|
||||
研发工单验收完成后,对 `memory/` 的处理遵循以下原则:
|
||||
|
||||
- **追加而非覆盖**:将本次工单中产生的、值得**长期沉淀**的内容(架构决策、关键避坑经验、引入的新依赖与版本)追加到对应文件
|
||||
- **不存放过程信息**:实现进度、待办勾选、临时决策这些短期信息应留在 [04-即时状态记忆.md](../项目/04-即时状态记忆.md),不进 `memory/`
|
||||
- **不删长期内容**:除非内容已被证伪或过时,否则不删除既有条目;过时内容用"已废弃 / 已被 XX 取代"的形式保留语义而非物理删除
|
||||
- **新增文件序号化**:当主题足够独立时新建 `NN-名称.md`(序号紧接当前最大值),并在 [.trae/索引.md](../../索引.md) 的 `memory/` 章节同步追加链接
|
||||
|
||||
### 1.5 访问权限
|
||||
|
||||
- 记忆文件仅供开发团队内部参考使用
|
||||
- 确保记忆文件中的敏感信息得到适当保护
|
||||
- 遵循项目的版本控制和代码管理规范
|
||||
|
||||
## 二、规范同步
|
||||
|
||||
- 每次对话中涉及到的语法或规范相关内容,必须同步整理到 `.trae/rules` 目录下的对应文件中
|
||||
- **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/`
|
||||
@@ -19,11 +48,11 @@
|
||||
- 若涉及到新的规范或规则,应创建新的规则文件进行记录
|
||||
- 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践
|
||||
|
||||
## 文件命名规则(强制)
|
||||
## 三、文件命名规则(强制)
|
||||
|
||||
`.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规:
|
||||
|
||||
- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `08-XXX规范.md`
|
||||
- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `06-XXX规范.md`
|
||||
- **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复
|
||||
- **入口/索引文件例外**:`.trae/索引.md` 这类目录入口文件不带序号
|
||||
- **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾
|
||||
@@ -34,18 +63,18 @@
|
||||
|
||||
| 子目录 | 当前最大序号 | 下一个可用 |
|
||||
|---|---|---|
|
||||
| `rules/全局/` | 07 | 08 |
|
||||
| `rules/项目/` | 04 | 05 |
|
||||
| `rules/全局/` | 06 | 07 |
|
||||
| `rules/项目/` | 05 | 06 |
|
||||
| `memory/` | 01 | 02 |
|
||||
| `coordination/` | 02 | 03 |
|
||||
|
||||
## 实现要求
|
||||
## 四、实现要求
|
||||
|
||||
- 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致
|
||||
- 当发现规范冲突或需要调整时,应及时记录并通知相关人员
|
||||
- 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程
|
||||
|
||||
## 路径规范
|
||||
## 五、路径规范
|
||||
|
||||
- 所有 Markdown 文档中不应使用绝对路径,应使用相对路径
|
||||
- 相对路径应以项目根目录为基准,例如 `.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 待办项」是两个完全不同的概念。
|
||||
> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。
|
||||
> 详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。
|
||||
> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。
|
||||
|
||||
## 适用范围
|
||||
@@ -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` 标注"已完成",并在"待验证表"里更新状态?
|
||||
@@ -3,7 +3,7 @@
|
||||
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
||||
>
|
||||
> 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。
|
||||
> 全局术语规则参见 [.trae/rules/全局/05-研发工单规则.md](../全局/05-研发工单规则.md)。
|
||||
> 全局术语规则参见 [.trae/rules/全局/04-研发工单全流程规范.md](../全局/04-研发工单全流程规范.md)。
|
||||
|
||||
## 一、术语对照(核心)
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
|
||||
## 一、当前活跃版本
|
||||
|
||||
- **进行中版本**:v1.2.0
|
||||
- **研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md)
|
||||
- **进行中版本**:v1.2.0(收尾中)、v1.3.0(规划中)
|
||||
- **v1.2.0 研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md)
|
||||
- **v1.3.0 研发工单总览**:[docs/project/研发工单-v1.3.0/00-工单总览.md](../../../docs/project/研发工单-v1.3.0/00-工单总览.md)
|
||||
- **PRD**:[docs/project/产品需求文档-1.2.0.md](../../../docs/project/产品需求文档-1.2.0.md)
|
||||
|
||||
## 二、v1.2.0 工单状态快照
|
||||
@@ -31,14 +32,27 @@
|
||||
| 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` |
|
||||
| 09 - CloudSync 同步策略改进 | 已实现 | 待验证 | TaskEntity 继承 ABP 基类;软删除修复(SaveChangesAsync);前端类型和 cloudSync.ts 已更新;新增 guid.ts |
|
||||
|
||||
## 三、关键临时决策
|
||||
## 三、v1.3.0 工单状态快照
|
||||
|
||||
| 子工单 | 实现状态 | 验证状态 | 简要说明 |
|
||||
|---|---|---|---|
|
||||
| 01 - HTTP 服务转换 MCP 服务 | 进行中 | 待验证 | 将现有 HTTP API 映射为 MCP 工具描述符 |
|
||||
| 02 - 语音控制与 AI 辅助 | 待开始 | 待验证 | STT/TTS + 语音指令解析 + AI 辅助任务拆分 |
|
||||
| 03 - 会议任务拆分 | 待开始 | 待验证 | 会议类型入口 + 录音/文字输入 + AI 拆分建议 + 确认批量创建 |
|
||||
| 03-01 - 会议数据模型与 API | 待开始 | 待验证 | TaskType 枚举、MeetingNotes/AudioDuration 字段、MeetingController |
|
||||
| 03-02 - 音频录制与转写 | 待开始 | 待验证 | 前端 MediaRecorder 录音 + 后端 STT 转写 |
|
||||
| 03-03 - AI 任务拆分服务 | 待开始 | 待验证 | 会议专用 LLM prompt + 批量创建子任务 |
|
||||
| 03-04 | 任务建议与确认 UI | 待开始 | 待验证 | 录音/纪要/审阅对话框 + Meeting 类型条件渲染 |
|
||||
| 04 | 富文本描述、附件与外部链接 | 待开始 | 待验证 | 多行描述 + 附件上传下载删除 + 外部链接 + 桌面端 Process.Start/xdg-open |
|
||||
|
||||
## 四、关键临时决策
|
||||
|
||||
- **MAUI 端不暴露云同步端点**:`MauiProgram.cs` 仅注册 `AddApplicationServices()`,不调 `AddCloudSyncServer()`。云同步端点只在 `Hua.Todo.Host` 暴露。
|
||||
- **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。
|
||||
- **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。
|
||||
- **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。
|
||||
|
||||
## 四、已知未完结事项 / 待办
|
||||
## 五、已知未完结事项 / 待办
|
||||
|
||||
- [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证
|
||||
- [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现
|
||||
@@ -46,7 +60,7 @@
|
||||
- [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证")
|
||||
- [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试)
|
||||
|
||||
## 五、最近一次重大重构(如有)
|
||||
## 六、最近一次重大重构(如有)
|
||||
|
||||
- **术语统一与目录中文化**(2026-06):
|
||||
- `.trae/rules/` 全部中文文件名 + 拆分为 `全局/` 和 `项目/` 两个子目录
|
||||
|
||||
@@ -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
@@ -8,9 +8,9 @@
|
||||
>
|
||||
> 文件命名约定:每个子目录内文件以 `NN-名称.md` 格式编号(两位数字),编号反映**阅读优先级 / 依赖顺序**。索引文件本身不带序号。
|
||||
>
|
||||
> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-智能体记忆.md](./rules/全局/01-智能体记忆.md#文件命名规则强制)。
|
||||
> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#三文件命名规则强制)。
|
||||
>
|
||||
> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md#memory-生命周期)。
|
||||
> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#14-memory-生命周期)。
|
||||
|
||||
---
|
||||
|
||||
@@ -32,13 +32,12 @@
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| [01-智能体记忆.md](./rules/全局/01-智能体记忆.md) | 智能体记忆/规范同步机制(**不含目录总览,由本索引文件承担**) |
|
||||
| [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md) | `.trae/memory/` 的使用方式 |
|
||||
| [03-注释规范.md](./rules/全局/03-注释规范.md) | C# / TypeScript / Vue 代码注释要求 |
|
||||
| [04-文档同步规范.md](./rules/全局/04-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 |
|
||||
| [05-研发工单规则.md](./rules/全局/05-研发工单规则.md) | 研发工单术语与边界(编码工作项 vs Todo 待办项) |
|
||||
| [06-研发工单拆分规范.md](./rules/全局/06-研发工单拆分规范.md) | 工单拆分目录与文件结构 |
|
||||
| [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
|
||||
| [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md) | 智能体记忆/存储/规范同步机制 & `.trae/memory/` 使用方式 & 文件命名规则(含序号速查表) |
|
||||
| [02-注释规范.md](./rules/全局/02-注释规范.md) | C# / TypeScript / Vue 代码注释要求 |
|
||||
| [03-文档同步规范.md](./rules/全局/03-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 |
|
||||
| [04-研发工单全流程规范.md](./rules/全局/04-研发工单全流程规范.md) | 研发工单术语定义 + 拆分输出规范 + 新增工单约束(合并原 05/06/09) |
|
||||
| [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
|
||||
| [06-AI沟通记录规范.md](./rules/全局/06-AI沟通记录规范.md) | 用户与智能体沟通记录的存储目录、序号管理与内容规范 |
|
||||
|
||||
---
|
||||
|
||||
@@ -50,6 +49,7 @@
|
||||
| [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 |
|
||||
| [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 |
|
||||
| [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 |
|
||||
| [05-多入口功能同步规范.md](./rules/项目/05-多入口功能同步规范.md) | 新增功能时必须同步确认 UI 入口与语音控制入口的覆盖情况 |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
- 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照
|
||||
- 与"项目即时状态"的边界:[04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) 偏当前快照、更新频率高;`memory/` 偏长期保留
|
||||
- 维护规约:详见 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md)
|
||||
- 维护规约:详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md)
|
||||
- 文件:
|
||||
- [01-project_memory.md](./memory/01-project_memory.md):依赖小版本号、历史决策、变更时间轴
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
## coordination/ — 多窗口协作运行时目录
|
||||
|
||||
- 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**)
|
||||
- 协作协议详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md)
|
||||
- 协作协议详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md)
|
||||
- 文件:
|
||||
- [00-README.md](./coordination/00-README.md):目录约定
|
||||
- [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,389 @@
|
||||
# MCP 服务接口文档
|
||||
|
||||
> Hua.Todo MCP Server 接口规范,供外部系统(AI 客户端、第三方集成)通过 MCP 协议接入 Hua.Todo 待办项管理能力。
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Hua.Todo 提供了基于 **Model Context Protocol (MCP)** 的服务端,允许外部 MCP 客户端通过 Streamable HTTP 传输协议发现并调用待办项管理工具。
|
||||
|
||||
### 1.1 协议与传输
|
||||
|
||||
| 项目 | 说明 |
|
||||
|---|---|
|
||||
| 协议版本 | MCP 2025-03-26(Streamable HTTP) |
|
||||
| 传输方式 | Streamable HTTP(无状态模式) |
|
||||
| 内容格式 | JSON-RPC 2.0 |
|
||||
| 端点路径 | `/mcp` |
|
||||
| 认证 | 暂无(本地模式);生产环境建议通过反向代理添加认证 |
|
||||
|
||||
### 1.2 服务端信息
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Hua.Todo MCP Server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 连接地址
|
||||
|
||||
| 运行模式 | 默认地址 |
|
||||
|---|---|
|
||||
| Hua.Todo.Host(独立服务端) | `http://localhost:5173/mcp` |
|
||||
| MAUI / Avalonia(嵌入式) | `http://localhost:5057/mcp` |
|
||||
|
||||
> 实际端口以部署配置为准。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接入方式
|
||||
|
||||
### 2.1 MCP 客户端配置示例
|
||||
|
||||
**Claude Desktop / Cursor / 其他 MCP 客户端** 配置文件(`mcp_servers.json` 或等效):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hua-todo": {
|
||||
"url": "http://localhost:5173/mcp",
|
||||
"transport": "streamable-http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 手动调用示例(curl)
|
||||
|
||||
MCP 协议基于 JSON-RPC 2.0,可通过 HTTP POST 手动调用:
|
||||
|
||||
#### 初始化连接
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26",
|
||||
"capabilities": {},
|
||||
"clientInfo": { "name": "my-client", "version": "1.0.0" }
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
#### 列出可用工具
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
#### 调用工具
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "CreateTodo",
|
||||
"arguments": {
|
||||
"title": "完成项目报告",
|
||||
"priority": "High"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具清单
|
||||
|
||||
### 3.1 查询类工具
|
||||
|
||||
#### ListAllTodos
|
||||
|
||||
获取所有待办项列表(含已完成和未完成)。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 无 | - | - | - |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```
|
||||
所有待办项(共 3 项):
|
||||
[1] 完成项目报告 | 优先级:High | 进行中
|
||||
[2] 购买办公用品 | 优先级:Medium | 已完成
|
||||
[3] 整理会议纪要 | 优先级:Low | 进行中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### ListActiveTodos
|
||||
|
||||
获取未完成的待办项列表。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 无 | - | - | - |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```
|
||||
未完成待办项(共 2 项):
|
||||
[1] 完成项目报告 | 优先级:High | 进行中
|
||||
[3] 整理会议纪要 | 优先级:Low | 进行中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### ListCompletedTodos
|
||||
|
||||
获取已完成的待办项列表。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 无 | - | - | - |
|
||||
|
||||
---
|
||||
|
||||
#### GetTodoById
|
||||
|
||||
根据 ID 获取单个待办项详情(含子任务)。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `int` | 是 | 待办项 ID |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "完成项目报告",
|
||||
"priority": "High",
|
||||
"isCompleted": false,
|
||||
"createdAt": "2026-06-16T08:30:00Z",
|
||||
"updatedAt": "2026-06-16T08:30:00Z",
|
||||
"parentTaskId": null,
|
||||
"subTasks": [
|
||||
{
|
||||
"id": 4,
|
||||
"title": "收集数据",
|
||||
"priority": "Medium",
|
||||
"isCompleted": true,
|
||||
"createdAt": "2026-06-16T08:35:00Z",
|
||||
"updatedAt": "2026-06-16T10:00:00Z",
|
||||
"parentTaskId": 1,
|
||||
"subTasks": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
|
||||
|
||||
---
|
||||
|
||||
#### ListSubTodos
|
||||
|
||||
获取指定父待办项下的所有子待办项。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `parentTaskId` | `int` | 是 | 父待办项 ID |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 写入类工具
|
||||
|
||||
#### CreateTodo
|
||||
|
||||
创建新的待办项。
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `title` | `string` | 是 | - | 待办项标题 |
|
||||
| `priority` | `string` | 否 | `"Medium"` | 优先级:`Low` / `Medium` / `High` |
|
||||
| `parentTaskId` | `int` | 否 | `null` | 父待办项 ID(创建子任务时传入) |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```
|
||||
已创建待办项:
|
||||
{
|
||||
"id": 5,
|
||||
"title": "完成项目报告",
|
||||
"priority": "High",
|
||||
"isCompleted": false,
|
||||
"createdAt": "2026-06-16T10:00:00Z",
|
||||
"updatedAt": "2026-06-16T10:00:00Z",
|
||||
"parentTaskId": null,
|
||||
"subTasks": []
|
||||
}
|
||||
```
|
||||
|
||||
**优先级无效时**:返回文本 `"无效的优先级 'xxx',有效值为:Low、Medium、High"`
|
||||
|
||||
---
|
||||
|
||||
#### UpdateTodo
|
||||
|
||||
更新已有待办项的标题或优先级。
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `id` | `int` | 是 | - | 待办项 ID |
|
||||
| `title` | `string` | 否 | `null` | 新标题 |
|
||||
| `priority` | `string` | 否 | `null` | 新优先级:`Low` / `Medium` / `High` |
|
||||
|
||||
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
|
||||
|
||||
---
|
||||
|
||||
#### ToggleTodoComplete
|
||||
|
||||
切换待办项的完成状态(已完成 ↔ 未完成)。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `int` | 是 | 待办项 ID |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```
|
||||
已切换完成状态:
|
||||
{
|
||||
"id": 1,
|
||||
"title": "完成项目报告",
|
||||
"priority": "High",
|
||||
"isCompleted": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### DeleteTodo
|
||||
|
||||
删除指定待办项。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `int` | 是 | 待办项 ID |
|
||||
|
||||
**成功时**:返回文本 `"已删除待办项 {id}"`
|
||||
|
||||
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据类型定义
|
||||
|
||||
### 4.1 TaskDto(待办项)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | `int` | 唯一标识符 |
|
||||
| `title` | `string` | 标题 |
|
||||
| `priority` | `string` | 优先级枚举值:`"Low"` / `"Medium"` / `"High"` |
|
||||
| `isCompleted` | `bool` | 是否已完成 |
|
||||
| `createdAt` | `string` | 创建时间(ISO 8601 UTC) |
|
||||
| `updatedAt` | `string` | 更新时间(ISO 8601 UTC) |
|
||||
| `parentTaskId` | `int?` | 父待办项 ID(顶层任务为 null) |
|
||||
| `subTasks` | `TaskDto[]` | 子任务列表(递归结构) |
|
||||
|
||||
### 4.2 优先级枚举
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|---|
|
||||
| `"Low"` | 低优先级 |
|
||||
| `"Medium"` | 中优先级(默认) |
|
||||
| `"High"` | 高优先级 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
MCP 工具调用中的错误通过返回文本内容表达(而非 MCP 协议级错误),常见情况:
|
||||
|
||||
| 场景 | 返回内容 |
|
||||
|---|---|
|
||||
| 待办项不存在 | `"未找到 ID 为 {id} 的待办项"` |
|
||||
| 优先级值无效 | `"无效的优先级 'xxx',有效值为:Low、Medium、High"` |
|
||||
| 列表为空 | `"没有{label}"` |
|
||||
|
||||
MCP 协议级错误(如方法不存在、参数格式错误)遵循 JSON-RPC 2.0 标准:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid Request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 HTTP API 的对照
|
||||
|
||||
MCP 工具与原有 HTTP Dynamic API 的对应关系:
|
||||
|
||||
| MCP 工具 | HTTP API | 说明 |
|
||||
|---|---|---|
|
||||
| `ListAllTodos` | `GET /api/task` | 获取全部 |
|
||||
| `ListActiveTodos` | `GET /api/task/active` | 获取未完成 |
|
||||
| `ListCompletedTodos` | `GET /api/task/completed` | 获取已完成 |
|
||||
| `GetTodoById` | `GET /api/task/{id}` | 按 ID 查询 |
|
||||
| `CreateTodo` | `POST /api/task` | 创建 |
|
||||
| `UpdateTodo` | `PUT /api/task` | 更新 |
|
||||
| `ToggleTodoComplete` | `PATCH /api/task/{id}/toggle` | 切换完成 |
|
||||
| `DeleteTodo` | `DELETE /api/task/{id}` | 删除 |
|
||||
| `ListSubTodos` | `GET /api/task/{parentTaskId}/subtasks` | 子任务 |
|
||||
|
||||
> 两套 API 共享同一 `ITaskService` 实现,数据完全一致,可根据场景选择使用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全建议(生产部署)
|
||||
|
||||
1. **网络隔离**:MCP 端点默认无认证,建议仅在可信网络内暴露,或通过反向代理添加 API Key / Bearer Token 认证
|
||||
2. **CORS 限制**:生产环境应将 CORS 策略从 `AllowAll` 改为指定来源
|
||||
3. **HTTPS**:生产环境必须启用 HTTPS
|
||||
4. **速率限制**:建议对 MCP 端点添加请求速率限制
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### Q: MCP 端点和 HTTP API 可以同时使用吗?
|
||||
可以。两者共享相同的数据源和服务层,不存在冲突。
|
||||
|
||||
### Q: Streamable HTTP 无状态模式下支持服务端通知吗?
|
||||
不支持。无状态模式下服务端无法向客户端主动推送通知。如需通知能力,需改为有状态模式(`Stateless = false`),但需要会话亲和。
|
||||
|
||||
### Q: 如何验证 MCP 服务是否正常运行?
|
||||
发送 `initialize` 请求(见 2.2 节),若返回服务端信息则表示正常。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:1.0.0
|
||||
**最后更新**:2026-06-16
|
||||
@@ -0,0 +1,210 @@
|
||||
# MCP 前端集成指南(Hua.Todo 内部使用)
|
||||
|
||||
> 适用范围:Hua.Todo 前端(Vue / TypeScript)团队,通过 MCP 协议与后端交互。
|
||||
> 外部系统接入请参阅 [08-MCP服务接口文档.md](./08-MCP服务接口文档.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
v1.3.0 起,Hua.Todo 后端在原有 Dynamic API(`/api/*`)基础上,新增了 MCP(Model Context Protocol)服务端点。前端可根据场景选择:
|
||||
|
||||
| 通道 | 端点 | 适用场景 |
|
||||
|---|---|---|
|
||||
| Dynamic API | `/api/task/*` | WebView 内常规 CRUD、已有逻辑兼容 |
|
||||
| MCP | `/mcp` | AI 辅助、语音指令、外部工具集成 |
|
||||
|
||||
两套通道共享同一 `ITaskService` 业务层,数据一致。
|
||||
|
||||
---
|
||||
|
||||
## 二、连接方式
|
||||
|
||||
### 2.1 嵌入式模式(MAUI / Avalonia + WebView)
|
||||
|
||||
```
|
||||
MCP 端点:http://localhost:5057/mcp
|
||||
```
|
||||
|
||||
当前嵌入式宿主**仅暴露 Dynamic API**,MCP 端点在嵌入式模式下同样可用(`AddMcpServerServices` 已注册)。
|
||||
|
||||
### 2.2 Host 模式(独立服务端)
|
||||
|
||||
```
|
||||
MCP 端点:http://<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 服务转换为 MCP(Model Context Protocol)服务,提升服务调用效率与可扩展性 |
|
||||
| 2 | **语音交互** | 实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项 |
|
||||
| 2 | **语音控制与 AI 辅助** | 通过语音指令操作 Todo 待办项(CRUD + 子任务),通过 LLM 提供 AI 辅助任务拆分建议 |
|
||||
| 3 | **会议任务拆分** | 以"会议"为入口记录会议内容(录音/文字),通过 AI 分析自动生成待办项拆分建议,用户确认后批量创建 |
|
||||
|
||||
---
|
||||
|
||||
@@ -20,11 +21,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
### 2.1 并行工单(可同步执行)
|
||||
|
||||
| 工单编号 | 标题 | 负责人 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 01 | HTTP 服务转换为 MCP 服务 | - | 待开始 |
|
||||
| 02 | 语音通话与语音控制功能 | - | 待开始 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 01 | HTTP 服务转换为 MCP 服务 | - | 进行中 |
|
||||
| 02 | 语音控制与 AI 辅助 | - | 待开始 |
|
||||
| 03 | 会议任务拆分 | - | 待开始 |
|
||||
| 04 | 富文本描述、附件与外部链接 | - | 待开始 |
|
||||
|
||||
### 2.2 串行工单(依赖前置工单完成)
|
||||
### 2.2 03 子工单拆分
|
||||
|
||||
| 子工单 | 标题 | 依赖 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 03-01 | 会议数据模型与 API | 无 | 待开始 |
|
||||
| 03-02 | 音频录制与转写 | 03-01 | 待开始 |
|
||||
| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 待开始 |
|
||||
| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 待开始 |
|
||||
|
||||
### 2.3 串行工单(依赖前置工单完成)
|
||||
|
||||
当前版本暂无串行工单依赖。
|
||||
|
||||
@@ -45,19 +57,71 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
- MCP 服务可正常对外提供接口
|
||||
- 所有原有 HTTP API 功能在 MCP 服务中可正常使用
|
||||
|
||||
### 3.2 工单 02 - 语音通话与语音控制功能
|
||||
### 3.2 工单 02 - 语音控制与 AI 辅助
|
||||
|
||||
**目标**:实现语音通话数据传输与语音控制功能
|
||||
**目标**:实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能
|
||||
|
||||
**核心需求**:
|
||||
- 语音通话数据传输能力
|
||||
- 语音指令识别与解析
|
||||
- 语音控制 Todo 待办项操作(创建、编辑、删除、完成等)
|
||||
- 与现有业务系统对接
|
||||
- 语音输入(STT):平台原生语音识别 → 文字
|
||||
- 语音播报(TTS):执行结果语音反馈
|
||||
- 语音指令解析与执行(CRUD + 子任务 + 歧义处理)
|
||||
- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
|
||||
|
||||
**不包含**:
|
||||
- 语音通话功能(场景不明确,本期不做)
|
||||
|
||||
**验收标准**:
|
||||
- 语音通话功能可正常使用
|
||||
- 语音控制可准确执行 Todo 业务操作
|
||||
- 各平台 STT/TTS 可正常工作
|
||||
- 语音指令可准确执行 Todo 业务操作
|
||||
- 歧义场景返回候选列表而非直接执行
|
||||
- AI 拆分建议需用户确认后才创建子任务
|
||||
|
||||
### 3.3 工单 03 - 会议任务拆分
|
||||
|
||||
**目标**:以"会议"为入口,通过录音/文字记录会议内容,AI 自动提取行动项生成待办项建议
|
||||
|
||||
**核心需求**:
|
||||
- 新增"会议"类型标记(TaskType.Meeting)
|
||||
- 录音:前端 MediaRecorder → 后端 STT 转写
|
||||
- 文字:直接输入/粘贴会议纪要
|
||||
- AI 会议拆分(LLM 分析会议内容 → 待办项建议列表)
|
||||
- 建议审阅与确认 UI(勾选、编辑、批量创建子任务)
|
||||
|
||||
**不包含**:
|
||||
- 实时语音转写(本期不做)
|
||||
- 音频持久存储(转写完成后删除音频)
|
||||
|
||||
**验收标准**:
|
||||
- 可通过"会议"类型创建待办项,显示会议图标
|
||||
- 录音可正常录制并提交转写
|
||||
- 文字纪要可保存/编辑
|
||||
- AI 拆分返回 3-10 条结构化建议,含优先级和原因
|
||||
- 用户可审阅、勾选、编辑建议后批量创建为子任务
|
||||
- 离线模式拒绝 AI 拆分(提示降级)
|
||||
|
||||
### 3.4 工单 04 - 富文本描述、附件与外部链接
|
||||
|
||||
**目标**:为 Todo 待办项新增多行描述、文件附件管理与外部程序/链接启动能力(仅桌面端)
|
||||
|
||||
**核心需求**:
|
||||
- `TaskEntity` 新增 `Description` 多行描述字段
|
||||
- 新增 `AttachmentEntity` 数据模型,支持本地文件上传、下载、删除
|
||||
- 支持外部链接(URL)作为附件,点击在默认浏览器打开
|
||||
- 桌面端通过系统关联程序打开本地附件(`Process.Start` / `xdg-open`)
|
||||
- 前端编辑对话框扩展描述 textarea + 附件管理区域
|
||||
|
||||
**不包含**:
|
||||
- 移动端附件管理(本期仅 Windows/Linux 桌面端)
|
||||
- 附件云同步(后续版本规划)
|
||||
- 附件预览(如图片缩略图,本期不做)
|
||||
- 富文本编辑器(本期仅纯文本)
|
||||
|
||||
**验收标准**:
|
||||
- 描述字段可正常编辑和保存
|
||||
- 附件可上传、下载、删除,文件完整性校验
|
||||
- 外部链接可添加并在浏览器中打开
|
||||
- 本地附件可通过系统关联程序打开
|
||||
- 附件数量(20个)和大小(50MB)限制生效
|
||||
|
||||
---
|
||||
|
||||
@@ -68,9 +132,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
| 01 | MCP 服务契约文档生成 | 待验证 | - |
|
||||
| 01 | MCP 服务可用性测试 | 待验证 | - |
|
||||
| 01 | 原有 API 功能兼容性 | 待验证 | - |
|
||||
| 02 | 语音通话连接测试 | 待验证 | - |
|
||||
| 02 | STT/TTS 平台适配 | 待验证 | Windows 优先,其他平台后续 |
|
||||
| 02 | 语音指令识别准确率 | 待验证 | - |
|
||||
| 02 | Todo 业务操作覆盖度 | 待验证 | - |
|
||||
| 02 | 歧义处理正确性 | 待验证 | - |
|
||||
| 02 | AI 拆分建议质量 | 待验证 | - |
|
||||
| 02 | Todo 业务操作覆盖度 | 待验证 | CRUD + 子任务 + 取消完成 |
|
||||
| 03 | 会议数据模型迁移 | 待验证 | TaskType 字段 + DB 迁移 |
|
||||
| 03 | 录音与转写链路 | 待验证 | 录制 → 上传 → 转写 → 保存 |
|
||||
| 03 | AI 会议拆分质量 | 待验证 | 建议含标题+优先级+原因 |
|
||||
| 03 | 建议审阅与批量创建 | 待验证 | 勾选/编辑/确认后创建子任务 |
|
||||
| 04 | 描述字段编辑与保存 | 待验证 | - |
|
||||
| 04 | 附件上传/下载/删除 | 待验证 | - |
|
||||
| 04 | 外部链接添加与打开 | 待验证 | - |
|
||||
| 04 | 本地文件通过系统程序打开 | 待验证 | Process.Start / xdg-open |
|
||||
| 04 | 附件数量/大小限制 | 待验证 | 20 个 / 50MB |
|
||||
| 04 | 待办项删除时附件级联清理 | 待验证 | - |
|
||||
| 04 | 跨平台编辑安全(移动端不覆盖描述/附件) | 待验证 | 桌面设值 → 移动端改标题 → 桌面验证不丢失 |
|
||||
|
||||
---
|
||||
|
||||
@@ -79,8 +156,21 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
| 决策点 | 结论 |
|
||||
|---|---|
|
||||
| MCP 框架选择 | 使用 TRAE 平台内置的 MCP 服务框架 |
|
||||
| 语音识别方案 | 集成平台语音识别能力 |
|
||||
| 服务注册方式 | 遵循平台标准注册流程 |
|
||||
| STT/TTS 分层 | 接口在 Core,实现在各平台目录(同全局快捷键模式) |
|
||||
| 语音指令解析 | A+C 混合方案:在线走 LLM 意图解析(LlmIntentParser),离线降级到规则匹配(RuleIntentParser),双策略通过 HybridVoiceIntentParser 自动切换 |
|
||||
| 歧义处理策略 | 目标不唯一时返回候选列表;LLM confidence < 0.8 时触发确认;confidence < 0.5 按 UNKNOWN 处理 |
|
||||
| AI 拆分安全性 | LLM 调用在 Host 端,建议需用户确认后才创建 |
|
||||
| LLM 复用 | 意图解析与 AI 拆分共用同一 LLM 基础设施(LlmClientService) |
|
||||
| 语音通话 | 本期不做,场景不明确 |
|
||||
| 会议类型 | 新增 `TaskType` 枚举区分普通待办项与会议,会议有专属录音/纪要/拆分 UI |
|
||||
| 录音存储 | 转写完成后删除原始音频文件,节省空间 |
|
||||
| 音频转写 | 优先服务端 Whisper API;后续补各平台原生 STT |
|
||||
| LLM 拆分 prompt | 会议专用 prompt,侧重"提取行动项",输出优先级+原因 |
|
||||
| 工单 02 复用 | `LlmClientService` 直接复用;会议拆分 prompt 独立于语音意图解析 |
|
||||
| 多入口覆盖规则 | 后续每项新增功能必须在需求阶段确认 UI 入口 + 语音控制入口覆盖情况,详见 [05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md) |
|
||||
| 附件存储策略 | 附件存储在应用数据目录 `Attachments/` 子目录;外部链接不复制文件仅存 URL;单文件 50MB / 每待办项 20 个上限 |
|
||||
| 外部程序启动 | 通过 `IPlatformAttachmentOpener` 接口实现平台差异:Windows 用 `Process.Start`,Linux 用 `xdg-open` |
|
||||
| 工单 04 与 03 共享文件 | `TaskEntity.cs`、`TodoDbContext.cs`、`task.ts`、`TaskEditDialog.vue` 为共享文件,工单 03 先写入,04 后续追加 |
|
||||
|
||||
---
|
||||
|
||||
@@ -89,8 +179,11 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
| 依赖项 | 状态 | 来源 |
|
||||
|---|---|---|
|
||||
| TRAE MCP SDK | 已就绪 | 平台内置 |
|
||||
| 语音识别服务 | 已就绪 | 平台内置 |
|
||||
| 各平台原生 STT/TTS API | 已就绪 | 平台内置 |
|
||||
| Hua.Todo v1.2.0 | 已完成 | 上一版本 |
|
||||
| LLM API(AI 拆分) | 待确认 | Host 端调用 |
|
||||
| 浏览器 MediaRecorder API | 已就绪 | 前端录音 |
|
||||
| 工单 02 LlmClientService | 待实现 | 会议拆分复用 |
|
||||
|
||||
---
|
||||
|
||||
@@ -99,9 +192,15 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|---|---|---|
|
||||
| MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 |
|
||||
| 语音识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 |
|
||||
| 平台 STT 识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 |
|
||||
| Linux STT 可用性差 | Linux 语音控制不可用 | Web Speech API 降级;或 `vosk` 离线模型 |
|
||||
| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,语音指令其他部分不受影响 |
|
||||
| 会议录音文件过大 | 上传超时/存储压力 | 前端限制最长 2 小时;压缩音频格式 |
|
||||
| 浏览器 MediaRecorder 兼容性 | 部分平台录音不可用 | 降级提示使用文字输入 |
|
||||
| STT 转写准确率不足 | 会议纪要质量差 | 转写后支持用户编辑修正 |
|
||||
|
||||
---
|
||||
|
||||
**创建日期**:2026-06-15
|
||||
**修订日期**:2026-06-16
|
||||
**版本**:v1.3.0
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
# 研发工单 v1.3.0 - 02 语音通话与语音控制功能
|
||||
# 研发工单 v1.3.0 - 02 语音控制与 AI 辅助
|
||||
|
||||
---
|
||||
|
||||
## 一、目标与范围
|
||||
|
||||
### 1.1 目标
|
||||
实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项,提升用户交互体验。
|
||||
实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能,通过平台原生 STT/TTS 实现语音输入输出,通过 LLM 意图解析处理自然语言指令,离线降级到规则匹配,通过 LLM 提供 AI 拆分建议。
|
||||
|
||||
### 1.2 范围
|
||||
|
||||
**包含**:
|
||||
- 语音通话数据传输能力
|
||||
- 语音指令识别与解析
|
||||
- 语音控制 Todo 待办项操作
|
||||
- 与现有业务系统对接
|
||||
- 语音输入(STT):平台原生语音识别 → 文字
|
||||
- 语音播报(TTS):执行结果语音反馈
|
||||
- 语音指令解析与执行(CRUD + 子任务操作)
|
||||
- 指令歧义处理(目标不唯一时返回候选列表)
|
||||
- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
|
||||
|
||||
**不包含**:
|
||||
- 语音通话 UI 界面设计(仅提供能力层)
|
||||
- 第三方语音服务集成(使用平台内置能力)
|
||||
- 语音通话功能(场景不明确,本期不做)
|
||||
- 前端语音 UI 设计(仅提供能力层与 API)
|
||||
- 第三方语音服务集成(使用平台原生能力)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,109 +28,426 @@
|
||||
| 条件 | 说明 |
|
||||
|---|---|
|
||||
| Hua.Todo v1.2.0 | 已完成,提供基础业务能力 |
|
||||
| 平台语音服务 | 已就绪 |
|
||||
| 工单 01(可选) | MCP 服务就绪后可通过 MCP 调用 |
|
||||
| 各平台原生 STT/TTS | Windows(`Windows.Media.SpeechRecognition`/`SpeechSynthesis`)、Android(`SpeechRecognizer`/`TextToSpeech`)、iOS/macOS(`SFSpeechRecognizer`/`AVSpeechSynthesizer`)、Linux(WebKitGTK Web Speech API / `vosk` 离线模型) |
|
||||
| LLM API | 在线模式意图解析 + AI 拆分共用;API Key 在 Host 端管理 |
|
||||
| 工单 01(可选) | MCP 服务就绪后,语音指令与 AI 拆分也可通过 MCP 暴露 |
|
||||
|
||||
---
|
||||
|
||||
## 三、需求规格
|
||||
## 三、架构设计
|
||||
|
||||
### 3.1 语音通话数据传输
|
||||
|
||||
**功能描述**:支持语音通话数据的实时传输
|
||||
|
||||
**接口设计**:
|
||||
```
|
||||
工具名:startVoiceCall
|
||||
参数:
|
||||
- target: string - 通话目标标识
|
||||
返回:
|
||||
- callId: string - 通话 ID
|
||||
- status: string - 通话状态(connected/disconnected)
|
||||
```
|
||||
### 3.1 整体链路
|
||||
|
||||
```
|
||||
工具名:endVoiceCall
|
||||
参数:
|
||||
- callId: string - 通话 ID
|
||||
返回:
|
||||
- success: boolean - 是否成功结束
|
||||
平台 STT(语音→文字)
|
||||
↓
|
||||
IVoiceInputService(Core 接口,各平台实现)
|
||||
↓ 回调文字到前端
|
||||
WebView → POST /api/voice/command { text: "帮我把那个开会的删了吧" }
|
||||
↓
|
||||
Host API → IVoiceIntentParser(双策略)
|
||||
├─ 在线:LlmIntentParser(调 LLM,输出结构化意图+参数)
|
||||
└─ 离线:RuleIntentParser(关键词规则匹配,覆盖高频指令)
|
||||
↓
|
||||
意图 + 参数 → 判断歧义
|
||||
├─ 无歧义 → 调用 TaskService 执行 → TTS 播报结果
|
||||
├─ 有歧义 → 返回候选列表 → 前端展示 → 用户确认 → 再执行
|
||||
└─ UNKNOWN → TTS 播报"没听懂,请再说一次"
|
||||
```
|
||||
|
||||
### 3.2 语音指令控制
|
||||
### 3.2 STT/TTS 分层策略
|
||||
|
||||
**功能描述**:支持通过语音指令操作 Todo 待办项
|
||||
遵循与全局快捷键相同的平台分离模式(接口+平台目录):
|
||||
|
||||
**支持的语音指令**:
|
||||
|
||||
| 指令类型 | 示例指令 | 对应操作 |
|
||||
| 层 | 职责 | 位置 |
|
||||
|---|---|---|
|
||||
| 创建任务 | "创建任务 开会" | 创建标题为"开会"的任务 |
|
||||
| 创建任务(带优先级) | "创建高优先级任务 提交报告" | 创建高优先级任务 |
|
||||
| 完成任务 | "完成任务 开会" | 标记任务"开会"为已完成 |
|
||||
| 删除任务 | "删除任务 开会" | 删除任务"开会" |
|
||||
| 更新任务 | "更新任务 开会 改为 团队会议" | 更新任务标题 |
|
||||
| 查询任务 | "查询未完成任务" | 获取未完成任务列表 |
|
||||
| 添加子任务 | "给任务开会添加子任务 准备PPT" | 为任务添加子任务 |
|
||||
| `IVoiceInputService` / `IVoiceOutputService` | 接口定义 | `src/Hua.Todo.Core/Services/` |
|
||||
| MAUI 平台实现 | 各平台原生 STT/TTS | `src/Hua.Todo.Maui/Platforms/{Windows,Android,macOS,iOS}/VoiceInputService.cs` |
|
||||
| Avalonia 平台实现 | Linux 桌面 STT/TTS | `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` |
|
||||
| Web 降级方案 | 浏览器 Web Speech API | 前端 `voiceInput.ts` |
|
||||
|
||||
### 3.3 意图解析双策略(A+C 方案)
|
||||
|
||||
采用 **LLM 主解析 + 规则降级** 的混合方案:
|
||||
|
||||
#### 策略 A:在线 — LLM 意图解析(LlmIntentParser)
|
||||
|
||||
在线模式下,将用户原始文字发送给 LLM,通过 system prompt 约束输出为结构化 JSON:
|
||||
|
||||
**接口设计**:
|
||||
```
|
||||
工具名:executeVoiceCommand
|
||||
参数:
|
||||
- command: string - 语音指令文本
|
||||
返回:
|
||||
- success: boolean - 是否执行成功
|
||||
- message: string - 执行结果描述
|
||||
- data: object - 返回数据(如任务列表)
|
||||
System Prompt(精简版):
|
||||
你是 Hua.Todo 的语音指令解析器。根据用户输入,输出以下 JSON 格式:
|
||||
{
|
||||
"intent": "CREATE|UPDATE|DELETE|COMPLETE|UNCOMPLETE|QUERY|ADD_SUBTASK|AI_BREAKDOWN|UNKNOWN",
|
||||
"params": { ... },
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
|
||||
意图说明:
|
||||
- CREATE: 创建任务,params: { title, priority? }
|
||||
- UPDATE: 修改任务,params: { targetTitle, newTitle }
|
||||
- DELETE: 删除任务,params: { targetTitle }
|
||||
- COMPLETE: 完成任务,params: { targetTitle }
|
||||
- UNCOMPLETE: 取消完成,params: { targetTitle }
|
||||
- QUERY: 查询任务,params: { filter? }
|
||||
- ADD_SUBTASK: 添加子任务,params: { parentTitle, subTitle }
|
||||
- AI_BREAKDOWN: AI辅助拆分,params: { targetTitle }
|
||||
- UNKNOWN: 无法识别
|
||||
|
||||
只输出 JSON,不要解释。
|
||||
```
|
||||
|
||||
### 3.3 语音状态管理
|
||||
|
||||
**接口设计**:
|
||||
**示例调用**:
|
||||
```
|
||||
工具名:getVoiceStatus
|
||||
参数:无
|
||||
返回:
|
||||
- isListening: boolean - 是否正在监听
|
||||
- isSpeaking: boolean - 是否正在播报
|
||||
输入:"帮我把那个开会的任务删了吧"
|
||||
LLM 输出:
|
||||
{
|
||||
"intent": "DELETE",
|
||||
"params": { "targetTitle": "开会" },
|
||||
"confidence": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
工具名:speakText
|
||||
参数:
|
||||
- text: string - 要播报的文本
|
||||
返回:
|
||||
- success: boolean - 是否成功
|
||||
输入:"这个项目太大了,帮我拆一下"
|
||||
LLM 输出:
|
||||
{
|
||||
"intent": "AI_BREAKDOWN",
|
||||
"params": { "targetTitle": "这个项目" },
|
||||
"confidence": 0.88
|
||||
}
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 无需维护同义词表,LLM 天然理解各种自然语言表述
|
||||
- 扩展新意图只需修改 prompt,不改代码
|
||||
- 歧义场景 LLM 也能判断(如 confidence < 阈值时触发确认)
|
||||
|
||||
#### 策略 C:离线 — 规则匹配降级(RuleIntentParser)
|
||||
|
||||
离线/嵌入式模式下,降级到关键词规则匹配,只覆盖高频指令:
|
||||
|
||||
| 意图 | 匹配规则 | 示例 |
|
||||
|---|---|---|
|
||||
| `CREATE` | 正则:`/^(创建|新增|添加|加)\s*(任务|待办)?\s*(.+)/` | "创建任务 开会" |
|
||||
| `DELETE` | 正则:`/^(删除|删掉|移除)\s*(任务|待办)?\s*(.+)/` | "删除任务 开会" |
|
||||
| `COMPLETE` | 正则:`/^(完成|做完)\s*(任务|待办)?\s*(.+)/` | "完成任务 开会" |
|
||||
| `UNCOMPLETE` | 正则:`/^(取消完成|重新打开|恢复)\s*(.+)/` | "取消完成 开会" |
|
||||
| `UPDATE` | 正则:`/^(修改|更新|编辑)\s*(.+?)\s*(改为|改成|改成)\s*(.+)/` | "修改任务 开会 改为 团队会议" |
|
||||
| `QUERY` | 正则:`/^(查询|查看|列出|显示)\s*(.+)/` | "查询未完成任务" |
|
||||
| `ADD_SUBTASK` | 正则:`/^(给|为)\s*(.+?)\s*(添加|加)\s*(子任务|子项)?\s*(.+)/` | "给任务开会添加子任务 准备PPT" |
|
||||
| `AI_BREAKDOWN` | 正则:`/^(帮我拆分|AI拆分|智能拆分)\s*(.+)/` | "帮我拆分 开会" |
|
||||
|
||||
**降级策略**:
|
||||
- 离线模式下 `AI_BREAKDOWN` 意图不可用(需要 LLM),TTS 播报"离线模式不支持 AI 拆分"
|
||||
- 规则匹配失败时返回 `UNKNOWN`
|
||||
|
||||
#### 策略切换逻辑
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
├─ 在线:LlmIntentParser(LLM 结构化输出)
|
||||
└─ 离线/降级: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 工具
|
||||
|
||||
---
|
||||
|
||||
## 五、验收标准
|
||||
|
||||
| 验收项 | 验证方法 | 预期结果 |
|
||||
|---|---|---|
|
||||
| 创建任务指令 | 说出"创建任务 测试" | 成功创建标题为"测试"的任务 |
|
||||
| 创建带优先级任务 | 说出"创建高优先级任务 紧急" | 成功创建高优先级任务 |
|
||||
| 完成任务指令 | 说出"完成任务 测试" | 任务状态变为已完成 |
|
||||
| 删除任务指令 | 说出"删除任务 测试" | 任务被成功删除 |
|
||||
| 查询任务指令 | 说出"查询未完成任务" | 返回未完成任务列表 |
|
||||
| 语音播报 | 调用 speakText | 成功播放指定文本 |
|
||||
| 语音通话 | 调用 startVoiceCall | 通话连接成功 |
|
||||
| STT 语音输入 | 说话后检查回调文字 | 文字识别正确 |
|
||||
| TTS 语音播报 | 执行指令后检查播报 | 播报内容与执行结果一致 |
|
||||
| 创建任务(在线) | "帮我把开会的任务加上" | LLM 解析为 CREATE,成功创建 |
|
||||
| 创建任务(离线降级) | "创建任务 测试" | 规则匹配为 CREATE,成功创建 |
|
||||
| 创建带优先级任务 | "建一个高优先级任务 紧急" | 成功创建高优先级任务 |
|
||||
| 完成任务指令 | "把那个开会的事标完成" | LLM 解析为 COMPLETE,执行成功 |
|
||||
| 取消完成指令 | "取消完成 测试" | 任务状态恢复为未完成 |
|
||||
| 删除任务指令 | "帮我把测试删了" | LLM 解析为 DELETE,删除成功 |
|
||||
| 更新任务指令 | "把测试改成验收" | 任务标题更新 |
|
||||
| 查询任务指令 | "有哪些没做完的" | LLM 解析为 QUERY,返回列表 |
|
||||
| 添加子任务指令 | "给开会加个子任务 准备PPT" | 子任务创建成功 |
|
||||
| 歧义处理 | 多个任务含"开会"时说"完成开会" | 返回候选列表,不直接执行 |
|
||||
| AI 拆分建议 | "帮我拆分 开会" | 返回子任务建议列表 |
|
||||
| AI 拆分确认 | 用户勾选后确认 | 仅创建勾选的子任务 |
|
||||
| 离线 AI 拆分 | 离线时说"帮我拆分" | 播报"离线模式不支持 AI 拆分" |
|
||||
| 无法识别指令 | 说出无关内容 | 播报"没听懂,请再说一次" |
|
||||
| LLM 降级 | 断网时使用语音 | 自动降级到规则匹配,基本 CRUD 可用 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Touch List
|
||||
## 六、Touch List
|
||||
|
||||
| 文件路径 | 修改类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `src/Hua.Todo.Application/Voice/` | 新增 | 语音服务目录 |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceService.cs` | 新增 | 语音服务实现 |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceCommandParser.cs` | 新增 | 语音指令解析器 |
|
||||
| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 服务注册扩展 |
|
||||
| `src/Hua.Todo.Core/Services/IVoiceInputService.cs` | 新增 | STT 接口定义 |
|
||||
| `src/Hua.Todo.Core/Services/IVoiceOutputService.cs` | 新增 | TTS 接口定义 |
|
||||
| `src/Hua.Todo.Core/Services/IVoiceIntentParser.cs` | 新增 | 意图解析器接口 |
|
||||
| `src/Hua.Todo.Application/Voice/LlmIntentParser.cs` | 新增 | LLM 意图解析器(在线策略) |
|
||||
| `src/Hua.Todo.Application/Voice/RuleIntentParser.cs` | 新增 | 规则匹配解析器(离线降级策略) |
|
||||
| `src/Hua.Todo.Application/Voice/HybridVoiceIntentParser.cs` | 新增 | 双策略解析器(在线走 LLM,离线走规则) |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceCommandIntent.cs` | 新增 | 意图枚举定义 |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceCommandExecutor.cs` | 新增 | 意图执行器(分发到 TaskService) |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceController.cs` | 新增 | 语音指令 API 端点(`/api/voice/command`、`/api/voice/ai-breakdown`) |
|
||||
| `src/Hua.Todo.Application/Voice/AiBreakdownService.cs` | 新增 | AI 辅助拆分服务(调用 LLM 生成建议) |
|
||||
| `src/Hua.Todo.Application/Voice/LlmClientService.cs` | 新增 | LLM 调用封装(意图解析 + AI 拆分共用) |
|
||||
| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO(VoiceIntentResult、VoiceCommandRequest 等) |
|
||||
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 语音服务 DI 注册 |
|
||||
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceInputService.cs` | 新增 | Windows STT 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceOutputService.cs` | 新增 | Windows TTS 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/Android/VoiceInputService.cs` | 新增 | Android STT 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/Android/VoiceOutputService.cs` | 新增 | Android TTS 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceInputService.cs` | 新增 | macOS STT 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceOutputService.cs` | 新增 | macOS TTS 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceInputService.cs` | 新增 | iOS STT 实现 |
|
||||
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceOutputService.cs` | 新增 | iOS TTS 实现 |
|
||||
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` | 新增 | Linux STT 实现 |
|
||||
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceOutputService.cs` | 新增 | Linux TTS 实现 |
|
||||
| `src/Hua.Todo.Web/src/api/voice.ts` | 新增 | 前端语音 API 模块 |
|
||||
| `src/Hua.Todo.Web/src/composables/useVoiceInput.ts` | 新增 | 前端语音输入组合式函数(含 Web Speech API 降级) |
|
||||
|
||||
---
|
||||
|
||||
**工单编号**:02
|
||||
**标题**:语音通话与语音控制功能
|
||||
**标题**:语音控制与 AI 辅助
|
||||
**版本**:v1.3.0
|
||||
**修订日期**:2026-06-16
|
||||
|
||||
---
|
||||
|
||||
## 七、与其他工单的语音入口衔接
|
||||
|
||||
### 7.1 背景
|
||||
|
||||
Hua.Todo 目前有两个功能入口:**UI 入口**(WebView / 前端界面)和**语音控制入口**(本工单产出)。后续每项新增功能在需求阶段都必须确认这两个入口的覆盖情况。
|
||||
|
||||
### 7.2 工单 03(会议任务拆分)的语音入口
|
||||
|
||||
工单 03 的会议功能需要通过语音控制入口提供以下指令:
|
||||
|
||||
| 语音指令 | 意图 | 参数 | 说明 |
|
||||
|---|---|---|---|
|
||||
| "新建会议 项目评审" | `CREATE_MEETING` | `title`(必填)、`priority`(可选) | 创建会议类型任务(TaskType.Meeting) |
|
||||
| "记录会议内容" | `RECORD_MEETING` | `targetTitle`(必填) | 开始录制当前会议 |
|
||||
| "停止录制" | `STOP_RECORDING` | 无 | 停止录制,触发转写 |
|
||||
| "帮我拆分这个会议" | `AI_BREAKDOWN` | `targetTitle`(必填,目标为会议类型) | 对会议内容进行 AI 拆分(复用 AI_BREAKDOWN 意图,参数携带 TaskType=Meeting) |
|
||||
| "查看会议待办项" | `QUERY` | `filter="会议"` | 查询所有会议类型任务 |
|
||||
|
||||
**关键点**:
|
||||
- `AI_BREAKDOWN` 意图需扩展以区分普通任务拆分和会议拆分(通过 `TaskType` 字段),会议拆分的 prompt 侧重"提取行动项"
|
||||
- `CREATE_MEETING` 是 `CREATE` 意图的子类型,创建时字段 `taskType = Meeting`,需要 LLM intent parser 的 prompt 增加说明
|
||||
- 录音控制(`RECORD_MEETING` / `STOP_RECORDING`)是新意图,对应前端 MediaRecorder 的启动/停止
|
||||
|
||||
### 7.3 语音入口覆盖检查表
|
||||
|
||||
后续每个新增功能/工单,智能体必须在需求讨论阶段输出以下检查表:
|
||||
|
||||
| 检查项 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| UI 入口是否已规划 | ✅ / ❌ | 描述 UI 入口 |
|
||||
| 语音控制入口是否已规划 | ✅ / ❌ | 描述对应的语音指令与意图 |
|
||||
| 当前不可覆盖的入口 | 说明原因 | 如:语音控制暂不支持 XX 操作(选项过多/需可视化交互) |
|
||||
|
||||
此规则已固化为项目规范,详见 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md)。
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# 研发工单 v1.3.0 - 03-01 会议数据模型与 API
|
||||
|
||||
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、目标
|
||||
|
||||
为"会议任务拆分"功能建立数据模型:扩展 `TaskEntity` 新增 `TaskType` 枚举和 `MeetingNotes`/`AudioDuration` 字段,定义完整的会议相关 API 端点与 DTO,完成数据库迁移。
|
||||
|
||||
## 二、范围
|
||||
|
||||
**包含**:
|
||||
- 新增 `TaskType` 枚举(Normal / Meeting)
|
||||
- `TaskEntity` 新增字段:`TaskType`、`MeetingNotes`、`AudioDuration`
|
||||
- EF Core 数据库迁移
|
||||
- DTO 扩展:`CreateTaskDto`、`UpdateTaskDto`、`TaskDto` 新增对应字段
|
||||
- 会议 API 端点骨架:`MeetingController` + `MeetingService`
|
||||
- API 路径:`/api/meeting/{taskId}/transcribe`、`/api/meeting/{taskId}/notes`
|
||||
- 会议相关 DTO 定义:`TranscribeRequest`、`TranscribeResponse`、`MeetingNotesRequest`
|
||||
|
||||
**不包含**:
|
||||
- AI 拆分逻辑(由 03-03 完成)
|
||||
- STT 转写实现(由 03-02 完成)
|
||||
- 前端组件(由 03-04 完成)
|
||||
|
||||
## 三、前置条件
|
||||
|
||||
| 条件 | 说明 |
|
||||
|---|---|
|
||||
| Hua.Todo v1.2.0 | 基础 Task CRUD 能力、父子任务、DynamicApi 中间件 |
|
||||
| ABP 基类迁移(可选) | 若工单 09(v1.2.0)已完成,TaskEntity 已转为 ABP 基类,字段新增方式不同 |
|
||||
|
||||
## 四、详细规格
|
||||
|
||||
### 4.1 TaskType 枚举
|
||||
|
||||
```csharp
|
||||
/// <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-01(API 契约)
|
||||
|
||||
---
|
||||
|
||||
## 一、目标
|
||||
|
||||
实现前端音频录制(MediaRecorder API)和后端音频转写(STT)能力,打通"录音 → 上传 → 转写文字 → 保存为会议纪要"的完整链路。
|
||||
|
||||
## 二、范围
|
||||
|
||||
**包含**:
|
||||
- 前端录音控件(`AudioRecorder.vue`)
|
||||
- 前端录音组合式函数(`useAudioRecorder.ts`)
|
||||
- 录音状态管理(录制中/暂停/停止/时长显示)
|
||||
- 音频上传 API 交互(POST multipart/form-data)
|
||||
- 后端音频文件临时存储
|
||||
- 后端 STT 转写服务(`SttService.cs`)
|
||||
- 转写结果回写 `MeetingNotes`
|
||||
|
||||
**不包含**:
|
||||
- 实时转写(边录边转)
|
||||
- 音频持久存储(转写完成后删除音频文件)
|
||||
- 音频云同步
|
||||
- 语音指令输入(属于工单 02)
|
||||
|
||||
## 三、前置条件
|
||||
|
||||
| 条件 | 说明 |
|
||||
|---|---|
|
||||
| 03-01 完成 | `MeetingController`、`MeetingService` 基础骨架就绪 |
|
||||
|
||||
## 四、详细规格
|
||||
|
||||
### 4.1 前端录音控件
|
||||
|
||||
**AudioRecorder.vue**:
|
||||
|
||||
```
|
||||
┌────────────────────────────────┐
|
||||
│ 🎤 会议录音 │
|
||||
│ │
|
||||
│ ● 录制中... 00:15:23 │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ ▁▃▂▄▅▂▁▃▄▅▃▂▁▂▄▅▃▁ │ │ ← 简易波形
|
||||
│ └──────────────────────────┘ │
|
||||
│ │
|
||||
│ [⏹ 停止录音] │
|
||||
│ │
|
||||
│ 录音时长限制:最长 2 小时 │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
状态:
|
||||
- **就绪**:显示录音按钮
|
||||
- **录制中**:显示停止按钮 + 时长计时 + 简易波形条
|
||||
- **已停止**:显示"提交转写" / "重新录制"
|
||||
|
||||
### 4.2 useAudioRecorder 组合式函数
|
||||
|
||||
```typescript
|
||||
/// <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
|
||||
|
||||
// 音频格式:webm(Chrome/Firefox)、mp4(Safari)
|
||||
// 时长限制:最长 2 小时(7200 秒)
|
||||
|
||||
return { isRecording, isPaused, duration, audioBlob, audioUrl,
|
||||
startRecording, stopRecording, resetRecording, getAudioBlob }
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 前端 API 模块
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/api/meeting.ts
|
||||
|
||||
/// <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-01(API 契约)、工单 02(`LlmClientService`)
|
||||
|
||||
---
|
||||
|
||||
## 一、目标
|
||||
|
||||
实现基于 LLM 的会议内容分析服务:接收会议纪要文字,通过 LLM prompt 工程提取行动项,生成结构化的待办项建议列表(含标题、优先级、理由)。
|
||||
|
||||
## 二、范围
|
||||
|
||||
**包含**:
|
||||
- `MeetingAiBreakdownService`:会议文本 → LLM → 结构化建议列表
|
||||
- 会议拆分专用 prompt 设计与迭代
|
||||
- AI 拆分 API 端点:`POST /api/meeting/{taskId}/breakdown`
|
||||
- 批量创建确认端点:`POST /api/meeting/{taskId}/breakdown/confirm`
|
||||
- 在线可用性检测(离线时拒绝 AI 拆分请求)
|
||||
- 复用工单 02 的 `LlmClientService`
|
||||
|
||||
**不包含**:
|
||||
- 单任务拆分(属于工单 02 `AI_BREAKDOWN` 意图)
|
||||
- LLM 基础设施搭建(由工单 02 提供)
|
||||
- 前端审阅 UI(由 03-04 完成)
|
||||
|
||||
## 三、前置条件
|
||||
|
||||
| 条件 | 说明 |
|
||||
|---|---|
|
||||
| 03-01 完成 | `MeetingController` 骨架就绪 |
|
||||
| 工单 02 `LlmClientService` | LLM 调用封装可用 |
|
||||
|
||||
## 四、详细规格
|
||||
|
||||
### 4.1 MeetingAiBreakdownService
|
||||
|
||||
```csharp
|
||||
/// <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 平台范围
|
||||
|
||||
| 平台 | 是否支持 | 说明 |
|
||||
|---|---|---|
|
||||
| Windows(MAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 |
|
||||
| Linux(Avalonia) | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 |
|
||||
| macOS(MAUI) | 待定 | 视为 Windows 同级,但本期不单独投入 |
|
||||
| iOS / Android | ❌ 不支持 | 移动端不提供附件/描述编辑入口,但**移动端编辑待办项时不得覆盖/清空已有描述和附件数据**(详见 1.4 节) |
|
||||
|
||||
### 1.3 范围
|
||||
|
||||
**包含**:
|
||||
|
||||
- `TaskEntity` 新增 `Description` 字段(多行文本)
|
||||
- 新增 `AttachmentEntity` 数据模型(附件元数据)
|
||||
- 附件 CRUD API(上传、列表、下载、删除、打开)
|
||||
- 前端编辑对话框扩展(描述 textarea + 附件管理区域)
|
||||
- 前端待办项列表/详情展示(描述预览、附件数量角标)
|
||||
- 桌面端通过系统关联程序打开附件(URL → 浏览器,文件路径 → 关联应用)
|
||||
|
||||
**不包含**:
|
||||
|
||||
- 附件云同步(本期不涉及,后续版本规划)
|
||||
- 附件预览(如图片缩略图、PDF 内嵌预览,本期不做)
|
||||
- 移动端附件管理(本期仅桌面端)
|
||||
- 富文本编辑器(如 Markdown 渲染,本期仅纯文本多行)
|
||||
|
||||
### 1.4 跨平台数据安全:移动端编辑不得破坏已有数据(强制)
|
||||
|
||||
**背景**:移动端(iOS/Android)不提供描述/附件的编辑 UI,但用户仍可在移动端修改待办项标题、优先级、完成状态等基础字段。如果没有防护,移动端发起更新请求时会用"空值"覆盖掉桌面端已设置的 `Description` 和 `Attachments`,导致数据丢失。
|
||||
|
||||
**约束**:
|
||||
|
||||
| 约束项 | 要求 |
|
||||
|---|---|
|
||||
| **后端 API 必须支持真正的部分更新** | `PUT /api/task/{id}` 的请求体中,**未传递的字段保持原值不变**,不得将缺失字段视为"置空"。即:`description` 不在 JSON 中 → 不修改数据库中的 `Description`;`description` 为 `null` → 清空 |
|
||||
| **UpdateTaskDto 所有扩展字段均为 optional** | `description`、`attachments` 相关字段在 DTO 中均标记为可选(`string?` / `null`),与必填字段(`id`)区分 |
|
||||
| **移动端前端不传递未知字段** | 移动端构建 `UpdateTaskDto` 时只传 `id`、`title`、`priority`、`isCompleted`,不传 `description`、`attachments`。后端对这些字段视为"不修改" |
|
||||
| **Attachment 实体独立于 Task 更新** | 附件 CRUD 走独立端点(`/api/task/{id}/attachments`),不通过 `PUT /api/task/{id}` 携带。移动端不调附件端点,自然不会破坏附件数据 |
|
||||
|
||||
**验证方式**:
|
||||
1. 桌面端创建待办项,添加描述 + 上传附件
|
||||
2. 移动端编辑同一待办项(只改标题),保存
|
||||
3. 回到桌面端查看 → 描述和附件完整保留,未被覆盖
|
||||
|
||||
---
|
||||
|
||||
## 二、前置条件
|
||||
|
||||
| 条件 | 说明 | 状态 |
|
||||
|---|---|---|
|
||||
| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 |
|
||||
| 工单 03 TaskType 枚举 | `TaskEntity` 已有字段扩展模式可参考 | 待实现(03 先于 04 或并行) |
|
||||
| 桌面 WebView 文件选择 | 前端 `<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-open(Linux)
|
||||
// - AttachmentType.ExternalLink → Process.Start(url)(在默认浏览器打开)
|
||||
```
|
||||
|
||||
### 5.7 删除附件
|
||||
|
||||
```
|
||||
DELETE /api/task/{taskId}/attachments/{attachmentId}
|
||||
|
||||
响应体:
|
||||
{
|
||||
"success": true,
|
||||
"message": "附件已删除"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端设计
|
||||
|
||||
### 6.1 类型扩展
|
||||
|
||||
`src/Hua.Todo.Web/src/types/task.ts` 新增 / 修改:
|
||||
|
||||
```typescript
|
||||
// Task 接口新增字段
|
||||
export interface Task {
|
||||
// ... 既有字段
|
||||
description?: string; // 多行描述
|
||||
attachments?: AttachmentItem[]; // 附件列表
|
||||
attachmentCount?: number; // 附件数量(列表视图用,不传完整列表)
|
||||
}
|
||||
|
||||
export interface AttachmentItem {
|
||||
id: number;
|
||||
taskId: number;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
fileSize: number;
|
||||
contentType: string;
|
||||
attachmentType: AttachmentType; // 0=本地文件, 1=外部链接
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type AttachmentType = 0 | 1;
|
||||
```
|
||||
|
||||
### 6.2 组件变更
|
||||
|
||||
| 组件 | 变更类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `TaskEditDialog.vue` | 修改 | 新增描述 textarea + 附件管理区域(上传按钮、附件列表) |
|
||||
| `TaskItem.vue` | 修改 | 卡片底部显示描述预览(单行截断)+ 附件数量角标 |
|
||||
| `AttachmentList.vue` | 新增 | 附件列表组件:文件名、大小、类型图标、删除按钮、打开按钮 |
|
||||
| `LinkInputDialog.vue` | 新增 | 外部链接输入弹出框(URL + 显示名称) |
|
||||
|
||||
### 6.3 新增 API 模块
|
||||
|
||||
`src/Hua.Todo.Web/src/api/attachments.ts`:
|
||||
|
||||
```typescript
|
||||
// uploadAttachment(taskId, file) — POST multipart/form-data
|
||||
// addLink(taskId, url, fileName?) — POST /api/task/{id}/attachments/link
|
||||
// getAttachments(taskId) — GET
|
||||
// deleteAttachment(taskId, attId) — DELETE
|
||||
// openAttachment(attId) — POST /api/attachments/{id}/open
|
||||
// downloadAttachment(attId) — GET /api/attachments/{id}/download
|
||||
```
|
||||
|
||||
### 6.4 交互流程
|
||||
|
||||
```
|
||||
编辑待办项对话框
|
||||
├─ 标题输入框(现有)
|
||||
├─ 优先级选择(现有)
|
||||
├─ 描述 textarea(新增:多行文本,placeholder "添加备注、步骤说明...")
|
||||
├─ 附件区域(新增)
|
||||
│ ├─ [+ 上传文件] 按钮 → 触发文件选择器 → 上传至后端
|
||||
│ ├─ [+ 添加链接] 按钮 → 弹出 URL 输入框 → 保存为外部链接附件
|
||||
│ └─ 附件列表(每项显示:图标 + 文件名 + 大小 + [打开] [删除])
|
||||
│ ├─ 本地文件:[打开] → 调用后端 open API → 系统关联程序打开
|
||||
│ ├─ 外部链接:[打开] → 调用后端 open API → 浏览器打开
|
||||
│ └─ [删除] → 确认 → 删除附件
|
||||
└─ [取消] [保存]
|
||||
```
|
||||
|
||||
### 6.5 列表卡片展示
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ☐ 整理产品需求文档 │
|
||||
│ 高优先级 │
|
||||
│ 需要在本周五前完成需求文档的整理... │ ← 描述预览(单行,超长截断)
|
||||
│ 📎 2 个附件 │ ← 附件数量角标
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、后端架构
|
||||
|
||||
### 7.1 新增服务
|
||||
|
||||
| 服务/类 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `AttachmentEntity` | `Hua.Todo.Core/Entities/` | 附件实体 |
|
||||
| `AttachmentType` | `Hua.Todo.Core/Entities/` | 附件类型枚举 |
|
||||
| `IAttachmentRepository` | `Hua.Todo.Core/Repositories/` | 仓储接口 |
|
||||
| `AttachmentRepository` | `Hua.Todo.Application/Data/` | EF Core 仓储实现 |
|
||||
| `AttachmentService` | `Hua.Todo.Application/Services/` | 附件业务逻辑(上传、下载、打开、清理) |
|
||||
| `IAttachmentService` | `Hua.Todo.Application/Services/` | 服务接口 |
|
||||
| `AttachmentController` | `Hua.Todo.Application/DynamicApi/` 或独立 Controller | 附件动态 API 端点(或手动 Controller) |
|
||||
| `PlatformAttachmentOpener` | `Hua.Todo.{Maui,Avalonia}/Services/` | 平台特定文件/链接打开实现(`Process.Start` / `xdg-open`) |
|
||||
|
||||
### 7.2 平台差异:外部程序启动
|
||||
|
||||
| 平台 | 实现方式 |
|
||||
|---|---|
|
||||
| Windows(MAUI) | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` |
|
||||
| Linux(Avalonia) | `System.Diagnostics.Process.Start("xdg-open", path)` |
|
||||
|
||||
通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。
|
||||
|
||||
### 7.3 附件文件管理
|
||||
|
||||
- 上传的本地文件复制到 `<AppData>/Hua.Todo/Attachments/{attachmentId}_{originalFileName}`
|
||||
- 删除附件时同步删除磁盘文件
|
||||
- 删除待办项时级联删除其所有附件(实体 + 文件)
|
||||
- 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示)
|
||||
|
||||
### 7.4 安全约束
|
||||
|
||||
| 约束 | 说明 |
|
||||
|---|---|
|
||||
| 文件类型白名单 | 不限制(本地工具场景,用户自行管理) |
|
||||
| 文件大小上限 | 50MB(可配置) |
|
||||
| 路径遍历防护 | 文件名去除 `../`、`..\` 等危险路径片段 |
|
||||
| 外部链接验证 | 仅允许 `http://`、`https://` 协议 |
|
||||
|
||||
---
|
||||
|
||||
## 八、子工单拆分
|
||||
|
||||
本工单体积适中,不拆分子工单,单文件完整实现。
|
||||
|
||||
如需与工单 03 并行推进,注意以下共享文件:
|
||||
|
||||
| 共享文件 | Writer | 说明 |
|
||||
|---|---|---|
|
||||
| `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration;04 后续追加 Description/Attachments 导航属性 |
|
||||
| `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 |
|
||||
| `task.ts` | **工单 03** 先写入 | 03 新增 taskType;04 后追加 description、attachments |
|
||||
| `TaskEditDialog.vue` | 协调 | 03 新增 MeetingType 条件渲染;04 追加描述 + 附件区域 |
|
||||
|
||||
> 执行顺序:优先完成 03-01(数据模型),再开始 04 的后端模型部分。前端组件部分可独立并行。
|
||||
|
||||
---
|
||||
|
||||
## 九、验收标准
|
||||
|
||||
| 验收项 | 验证方法 | 预期结果 |
|
||||
|---|---|---|
|
||||
| 描述编辑 | 编辑待办项,在描述 textarea 输入多行文字,保存 | 再次打开编辑框,描述内容完整保留 |
|
||||
| 描述显示 | 查看待办项列表/详情 | 列表卡片显示描述预览(单行截断),详情显示完整描述 |
|
||||
| 附件上传 | 编辑待办项,点击上传文件,选择一个本地文件 | 文件上传成功,附件列表显示文件名和大小 |
|
||||
| 附件下载 | 在附件列表点击下载按钮 | 文件以原始文件名下载到本地 |
|
||||
| 附件删除 | 点击附件删除按钮 | 附件从列表移除,磁盘文件同步清理 |
|
||||
| 外部链接添加 | 点击添加链接,输入 URL | 链接保存为附件,类型标记为"外部链接" |
|
||||
| 打开本地文件 | 点击本地附件"打开" | 系统关联程序打开该文件(如 PDF → PDF 阅读器) |
|
||||
| 打开外部链接 | 点击外部链接附件"打开" | 系统默认浏览器打开该 URL |
|
||||
| 打开可执行文件 | 附件为 `.exe`(Windows)或可执行脚本(Linux) | 系统正常运行该程序 |
|
||||
| 附件数量限制 | 超过 20 个附件后继续上传 | 提示"每个待办项最多 20 个附件" |
|
||||
| 文件大小限制 | 上传超过 50MB 的文件 | 提示"文件大小不能超过 50MB" |
|
||||
| 待办项删除级联 | 删除有附件的待办项 | 附件记录和磁盘文件同步清理 |
|
||||
| 空描述 | 创建待办项时不填描述 | 正常创建,列表中不显示描述行 |
|
||||
| 跨平台安全 | 桌面端设置描述+附件 → 移动端只改标题并保存 → 桌面端查看 | 描述和附件完整保留,未被覆盖(详见 1.4 节) |
|
||||
|
||||
---
|
||||
|
||||
## 十、风险与回滚
|
||||
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|---|---|---|
|
||||
| `Process.Start` 在不同 Linux 发行版行为差异 | 部分 Linux 文件打开失败 | 用 `xdg-open` 兜底;提供错误提示"无法打开文件:{原因}" |
|
||||
| 附件文件被用户手动删除 | 数据库有记录但文件不存在 | 启动时校验,缺失文件标记为"失效",UI 用灰色 + 感叹号提示 |
|
||||
| 大文件上传耗尽磁盘空间 | 应用数据目录磁盘满 | 单文件 50MB + 每待办项 20 个上限 = 最多 1GB;检查和提示剩余空间 |
|
||||
| 并发编辑附件冲突 | 多窗口同时删除/添加附件 | 附件操作为即时保存(非批量提交),利用数据库事务隔离 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、Touch List
|
||||
|
||||
| 文件路径 | 修改类型 | 是否共享 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是(与 03 共享) | 新增 Description 字段 + Attachments 导航属性 |
|
||||
| `src/Hua.Todo.Core/Entities/AttachmentEntity.cs` | 新增 | 否 | 附件实体 |
|
||||
| `src/Hua.Todo.Core/Entities/AttachmentType.cs` | 新增 | 否 | 附件类型枚举 |
|
||||
| `src/Hua.Todo.Core/Repositories/IAttachmentRepository.cs` | 新增 | 否 | 附件仓储接口 |
|
||||
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是(与 03 共享) | 新增 Attachments DbSet + 关系映射 |
|
||||
| `src/Hua.Todo.Application/Services/IAttachmentService.cs` | 新增 | 否 | 附件服务接口 |
|
||||
| `src/Hua.Todo.Application/Services/AttachmentService.cs` | 新增 | 否 | 附件业务逻辑 |
|
||||
| `src/Hua.Todo.Application/Controllers/AttachmentController.cs` | 新增 | 否 | 附件 API 端点 |
|
||||
| `src/Hua.Todo.Application/Models/AttachmentModels.cs` | 新增 | 否 | 附件 DTO |
|
||||
| `src/Hua.Todo.Core/Services/IPlatformAttachmentOpener.cs` | 新增 | 否 | 平台文件打开接口 |
|
||||
| `src/Hua.Todo.Maui/Services/Platforms/MauiAttachmentOpener.cs` | 新增 | 否 | Windows MAUI 文件打开实现 |
|
||||
| `src/Hua.Todo.Avalonia/Services/Platforms/AvaloniaAttachmentOpener.cs` | 新增 | 否 | Linux Avalonia 文件打开实现 |
|
||||
| `src/Hua.Todo.Web/src/api/attachments.ts` | 新增 | 否 | 前端附件 API 模块 |
|
||||
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是(与 03 共享) | 新增 description、AttachmentItem 等类型 |
|
||||
| `src/Hua.Todo.Web/src/components/TaskEditDialog.vue` | 修改 | 是(与 03 共享) | 新增描述 textarea + 附件管理区域 |
|
||||
| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改 | 否 | 描述预览 + 附件角标 |
|
||||
| `src/Hua.Todo.Web/src/components/AttachmentList.vue` | 新增 | 否 | 附件列表组件 |
|
||||
| `src/Hua.Todo.Web/src/composables/useAttachments.ts` | 新增 | 否 | 附件管理组合式函数 |
|
||||
|
||||
---
|
||||
|
||||
## 十二、与工单 03 的交互
|
||||
|
||||
| 维度 | 工单 03 | 工单 04 |
|
||||
|---|---|---|
|
||||
| 描述文本 | `MeetingNotes`(会议纪要,仅会议类型) | `Description`(通用多行描述,所有类型) |
|
||||
| 附件 | 不涉及 | 通用附件管理 |
|
||||
| 外部链接 | 不涉及 | 通用外部链接 |
|
||||
|
||||
> `MeetingNotes` 与 `Description` 是两个独立字段:
|
||||
> - `MeetingNotes`:会议转写/文字纪要,可能很长(数千字),仅在会议类型下有值
|
||||
> - `Description`:通用简短备注(上限 5000 字符),所有类型的待办项都可以有
|
||||
>
|
||||
> 两者可共存:会议类型的待办项可以同时有 `MeetingNotes`(AI 拆分的输入)和 `Description`(用户的简短备注)。
|
||||
|
||||
---
|
||||
|
||||
**工单编号**:04
|
||||
**标题**:富文本描述、附件与外部链接
|
||||
**版本**:v1.3.0
|
||||
**创建日期**:2026-06-16
|
||||
@@ -13,12 +13,23 @@
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">
|
||||
<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>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Hua.Todo.Application;
|
||||
using Hua.Todo.Application.DynamicApi;
|
||||
using Hua.Todo.Application.Interfaces;
|
||||
using Hua.Todo.Application.Mcp;
|
||||
using Hua.Todo.Application.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -13,6 +14,9 @@ builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddApplicationServices("Data Source=Hua.Todo.db");
|
||||
|
||||
// 注册 MCP Server(依赖 ITaskService,须在 AddApplicationServices 之后调用)
|
||||
builder.Services.AddMcpServerServices();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
@@ -43,4 +47,7 @@ app.UseCors("AllowAll");
|
||||
app.UseAuthorization();
|
||||
app.UseDynamicApi();
|
||||
|
||||
// 映射 MCP Server 端点(Streamable HTTP,路径 /mcp)
|
||||
app.MapMcpServer();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -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>
|
||||
/// DeriveServiceName:ITaskService → 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>
|
||||
/// ToSnakeCase:PascalCase → 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>
|
||||
/// IsReadOnlyMethod:Get/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>
|
||||
/// IsDestructiveMethod:Delete/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
|
||||
@@ -28,4 +28,12 @@
|
||||
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
|
||||
</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>
|
||||
Reference in New Issue
Block a user