feat(mcp): 新增 MCP 服务基础设施,重构规则文件序号,新增 v1.3.0 工单文档

- 规则重组:全局/ 下 8 个规则合并为 6 个(01+02→01,05+06→04),序号顺延

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

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

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

- MCP 接口与前端集成指南:docs/manual/08、09
This commit is contained in:
ShaoHua
2026-06-16 01:15:40 +08:00
parent aacc56e952
commit 9223ceca50
30 changed files with 3907 additions and 346 deletions
+389
View File
@@ -0,0 +1,389 @@
# MCP 服务接口文档
> Hua.Todo MCP Server 接口规范,供外部系统(AI 客户端、第三方集成)通过 MCP 协议接入 Hua.Todo 待办项管理能力。
## 1. 概述
Hua.Todo 提供了基于 **Model Context Protocol (MCP)** 的服务端,允许外部 MCP 客户端通过 Streamable HTTP 传输协议发现并调用待办项管理工具。
### 1.1 协议与传输
| 项目 | 说明 |
|---|---|
| 协议版本 | MCP 2025-03-26Streamable HTTP |
| 传输方式 | Streamable HTTP(无状态模式) |
| 内容格式 | JSON-RPC 2.0 |
| 端点路径 | `/mcp` |
| 认证 | 暂无(本地模式);生产环境建议通过反向代理添加认证 |
### 1.2 服务端信息
```json
{
"name": "Hua.Todo MCP Server",
"version": "1.0.0"
}
```
### 1.3 连接地址
| 运行模式 | 默认地址 |
|---|---|
| Hua.Todo.Host(独立服务端) | `http://localhost:5173/mcp` |
| MAUI / Avalonia(嵌入式) | `http://localhost:5057/mcp` |
> 实际端口以部署配置为准。
---
## 2. 接入方式
### 2.1 MCP 客户端配置示例
**Claude Desktop / Cursor / 其他 MCP 客户端** 配置文件(`mcp_servers.json` 或等效):
```json
{
"mcpServers": {
"hua-todo": {
"url": "http://localhost:5173/mcp",
"transport": "streamable-http"
}
}
}
```
### 2.2 手动调用示例(curl
MCP 协议基于 JSON-RPC 2.0,可通过 HTTP POST 手动调用:
#### 初始化连接
```bash
curl -X POST http://localhost:5173/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-03-26" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}'
```
#### 列出可用工具
```bash
curl -X POST http://localhost:5173/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-03-26" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}'
```
#### 调用工具
```bash
curl -X POST http://localhost:5173/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-03-26" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "CreateTodo",
"arguments": {
"title": "完成项目报告",
"priority": "High"
}
}
}'
```
---
## 3. 工具清单
### 3.1 查询类工具
#### ListAllTodos
获取所有待办项列表(含已完成和未完成)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 无 | - | - | - |
**返回示例**
```
所有待办项(共 3 项):
[1] 完成项目报告 | 优先级:High | 进行中
[2] 购买办公用品 | 优先级:Medium | 已完成
[3] 整理会议纪要 | 优先级:Low | 进行中
```
---
#### ListActiveTodos
获取未完成的待办项列表。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 无 | - | - | - |
**返回示例**
```
未完成待办项(共 2 项):
[1] 完成项目报告 | 优先级:High | 进行中
[3] 整理会议纪要 | 优先级:Low | 进行中
```
---
#### ListCompletedTodos
获取已完成的待办项列表。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 无 | - | - | - |
---
#### GetTodoById
根据 ID 获取单个待办项详情(含子任务)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `int` | 是 | 待办项 ID |
**返回示例**
```json
{
"id": 1,
"title": "完成项目报告",
"priority": "High",
"isCompleted": false,
"createdAt": "2026-06-16T08:30:00Z",
"updatedAt": "2026-06-16T08:30:00Z",
"parentTaskId": null,
"subTasks": [
{
"id": 4,
"title": "收集数据",
"priority": "Medium",
"isCompleted": true,
"createdAt": "2026-06-16T08:35:00Z",
"updatedAt": "2026-06-16T10:00:00Z",
"parentTaskId": 1,
"subTasks": []
}
]
}
```
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
---
#### ListSubTodos
获取指定父待办项下的所有子待办项。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `parentTaskId` | `int` | 是 | 父待办项 ID |
---
### 3.2 写入类工具
#### CreateTodo
创建新的待办项。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `title` | `string` | 是 | - | 待办项标题 |
| `priority` | `string` | 否 | `"Medium"` | 优先级:`Low` / `Medium` / `High` |
| `parentTaskId` | `int` | 否 | `null` | 父待办项 ID(创建子任务时传入) |
**返回示例**
```
已创建待办项:
{
"id": 5,
"title": "完成项目报告",
"priority": "High",
"isCompleted": false,
"createdAt": "2026-06-16T10:00:00Z",
"updatedAt": "2026-06-16T10:00:00Z",
"parentTaskId": null,
"subTasks": []
}
```
**优先级无效时**:返回文本 `"无效的优先级 'xxx',有效值为:Low、Medium、High"`
---
#### UpdateTodo
更新已有待办项的标题或优先级。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `id` | `int` | 是 | - | 待办项 ID |
| `title` | `string` | 否 | `null` | 新标题 |
| `priority` | `string` | 否 | `null` | 新优先级:`Low` / `Medium` / `High` |
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
---
#### ToggleTodoComplete
切换待办项的完成状态(已完成 ↔ 未完成)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `int` | 是 | 待办项 ID |
**返回示例**
```
已切换完成状态:
{
"id": 1,
"title": "完成项目报告",
"priority": "High",
"isCompleted": true,
...
}
```
---
#### DeleteTodo
删除指定待办项。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `int` | 是 | 待办项 ID |
**成功时**:返回文本 `"已删除待办项 {id}"`
**未找到时**:返回文本 `"未找到 ID 为 {id} 的待办项"`
---
## 4. 数据类型定义
### 4.1 TaskDto(待办项)
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | `int` | 唯一标识符 |
| `title` | `string` | 标题 |
| `priority` | `string` | 优先级枚举值:`"Low"` / `"Medium"` / `"High"` |
| `isCompleted` | `bool` | 是否已完成 |
| `createdAt` | `string` | 创建时间(ISO 8601 UTC |
| `updatedAt` | `string` | 更新时间(ISO 8601 UTC |
| `parentTaskId` | `int?` | 父待办项 ID(顶层任务为 null) |
| `subTasks` | `TaskDto[]` | 子任务列表(递归结构) |
### 4.2 优先级枚举
| 值 | 说明 |
|---|---|
| `"Low"` | 低优先级 |
| `"Medium"` | 中优先级(默认) |
| `"High"` | 高优先级 |
---
## 5. 错误处理
MCP 工具调用中的错误通过返回文本内容表达(而非 MCP 协议级错误),常见情况:
| 场景 | 返回内容 |
|---|---|
| 待办项不存在 | `"未找到 ID 为 {id} 的待办项"` |
| 优先级值无效 | `"无效的优先级 'xxx',有效值为:Low、Medium、High"` |
| 列表为空 | `"没有{label}"` |
MCP 协议级错误(如方法不存在、参数格式错误)遵循 JSON-RPC 2.0 标准:
```json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32600,
"message": "Invalid Request"
}
}
```
---
## 6. 与 HTTP API 的对照
MCP 工具与原有 HTTP Dynamic API 的对应关系:
| MCP 工具 | HTTP API | 说明 |
|---|---|---|
| `ListAllTodos` | `GET /api/task` | 获取全部 |
| `ListActiveTodos` | `GET /api/task/active` | 获取未完成 |
| `ListCompletedTodos` | `GET /api/task/completed` | 获取已完成 |
| `GetTodoById` | `GET /api/task/{id}` | 按 ID 查询 |
| `CreateTodo` | `POST /api/task` | 创建 |
| `UpdateTodo` | `PUT /api/task` | 更新 |
| `ToggleTodoComplete` | `PATCH /api/task/{id}/toggle` | 切换完成 |
| `DeleteTodo` | `DELETE /api/task/{id}` | 删除 |
| `ListSubTodos` | `GET /api/task/{parentTaskId}/subtasks` | 子任务 |
> 两套 API 共享同一 `ITaskService` 实现,数据完全一致,可根据场景选择使用。
---
## 7. 安全建议(生产部署)
1. **网络隔离**:MCP 端点默认无认证,建议仅在可信网络内暴露,或通过反向代理添加 API Key / Bearer Token 认证
2. **CORS 限制**:生产环境应将 CORS 策略从 `AllowAll` 改为指定来源
3. **HTTPS**:生产环境必须启用 HTTPS
4. **速率限制**:建议对 MCP 端点添加请求速率限制
---
## 8. 常见问题
### Q: MCP 端点和 HTTP API 可以同时使用吗?
可以。两者共享相同的数据源和服务层,不存在冲突。
### Q: Streamable HTTP 无状态模式下支持服务端通知吗?
不支持。无状态模式下服务端无法向客户端主动推送通知。如需通知能力,需改为有状态模式(`Stateless = false`),但需要会话亲和。
### Q: 如何验证 MCP 服务是否正常运行?
发送 `initialize` 请求(见 2.2 节),若返回服务端信息则表示正常。
---
**文档版本**1.0.0
**最后更新**2026-06-16
+210
View File
@@ -0,0 +1,210 @@
# MCP 前端集成指南(Hua.Todo 内部使用)
> 适用范围:Hua.Todo 前端(Vue / TypeScript)团队,通过 MCP 协议与后端交互。
> 外部系统接入请参阅 [08-MCP服务接口文档.md](./08-MCP服务接口文档.md)。
---
## 一、背景
v1.3.0 起,Hua.Todo 后端在原有 Dynamic API`/api/*`)基础上,新增了 MCPModel Context Protocol)服务端点。前端可根据场景选择:
| 通道 | 端点 | 适用场景 |
|---|---|---|
| Dynamic API | `/api/task/*` | WebView 内常规 CRUD、已有逻辑兼容 |
| MCP | `/mcp` | AI 辅助、语音指令、外部工具集成 |
两套通道共享同一 `ITaskService` 业务层,数据一致。
---
## 二、连接方式
### 2.1 嵌入式模式(MAUI / Avalonia + WebView
```
MCP 端点:http://localhost:5057/mcp
```
当前嵌入式宿主**仅暴露 Dynamic API**,MCP 端点在嵌入式模式下同样可用(`AddMcpServerServices` 已注册)。
### 2.2 Host 模式(独立服务端)
```
MCP 端点:http://<host>:5173/mcp
```
开发环境通过 Vite proxy 可直接访问 `/mcp`
---
## 三、前端 MCP 客户端选型
### 3.1 推荐方案:`@modelcontextprotocol/sdk`
官方 TypeScript MCP SDK,支持 Streamable HTTP 传输。
```bash
npm install @modelcontextprotocol/sdk
```
### 3.2 连接示例
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:5057/mcp")
);
const client = new Client({
name: "hua-todo-frontend",
version: "1.3.0",
});
await client.connect(transport);
// 列出所有可用工具
const tools = await client.listTools();
console.log("可用工具:", tools);
// 调用工具
const result = await client.callTool({
name: "ListActiveTodos",
arguments: {},
});
console.log("未完成待办项:", result);
```
### 3.3 封装建议
`src/Hua.Todo.Web/src/api/` 下新增 `mcpClient.ts`
```typescript
// src/api/mcpClient.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js";
const MCP_BASE_URL = window.__API_BASE_URL__
? window.__API_BASE_URL__.replace("/api", "/mcp")
: "/mcp";
let client: Client | null = null;
/** 获取或创建 MCP 客户端单例 */
export async function getMcpClient(): Promise<Client> {
if (!client) {
const transport = new StreamableHTTPClientTransport(
new URL(MCP_BASE_URL, window.location.origin)
);
client = new Client({
name: "hua-todo-frontend",
version: "1.3.0",
});
await client.connect(transport);
}
return client;
}
/** 断开 MCP 连接 */
export async function disconnectMcp(): Promise<void> {
if (client) {
await client.close();
client = null;
}
}
```
---
## 四、工具调用映射(MCP vs Dynamic API
| 业务操作 | Dynamic API | MCP Tool | MCP 参数 |
|---|---|---|---|
| 获取所有待办项 | `GET /api/task` | `ListAllTodos` | 无 |
| 获取未完成待办项 | `GET /api/task/active` | `ListActiveTodos` | 无 |
| 获取已完成待办项 | `GET /api/task/completed` | `ListCompletedTodos` | 无 |
| 获取单个待办项 | `GET /api/task/{id}` | `GetTodoById` | `id: number` |
| 创建待办项 | `POST /api/task` | `CreateTodo` | `title, priority?, parentTaskId?` |
| 更新待办项 | `PUT /api/task` | `UpdateTodo` | `id, title?, priority?` |
| 切换完成状态 | `PATCH /api/task/{id}/toggle` | `ToggleTodoComplete` | `id: number` |
| 删除待办项 | `DELETE /api/task/{id}` | `DeleteTodo` | `id: number` |
| 获取子待办项 | `GET /api/task/{pid}/subtasks` | `ListSubTodos` | `parentTaskId: number` |
---
## 五、返回格式差异
### Dynamic API 返回
```json
{
"success": true,
"data": [{ "id": 1, "title": "...", ... }],
"message": "",
"errors": []
}
```
### MCP Tool 返回
MCP 工具返回 `string` 类型,分两种格式:
**列表类**(可读文本):
```
未完成待办项(共 2 项):
[1] 完成报告 | 优先级:High | 进行中
[3] 买菜 | 优先级:Low | 进行中 | 父ID:2
```
**单条/创建/更新类**JSON):
```json
{
"id": 1,
"title": "完成报告",
"priority": "High",
"isCompleted": false,
"createdAt": "2026-06-16T08:00:00Z",
"updatedAt": "2026-06-16T08:00:00Z",
"parentTaskId": null,
"subTasks": []
}
```
> 前端如果需要结构化数据,建议仍使用 Dynamic API;MCP 通道主要用于 AI 场景和文本交互。
---
## 六、典型场景
### 6.1 AI 对话式操作
用户通过 AI 助手(接入 MCP 的 LLM 客户端)用自然语言操作待办项:
```
用户:帮我看看还有哪些事没做完
AI:→ 调用 ListActiveTodos
AI:您有 2 项未完成的待办:[1] 完成报告 [3] 买菜
用户:把"完成报告"标为已完成
AI:→ 调用 ToggleTodoComplete(id=1)
AI:已将"完成报告"标记为完成
```
### 6.2 语音指令
语音 → STT → 文本 → LLM 解析意图 → MCP Tool 调用 → TTS 播报结果。
### 6.3 外部工具集成
IDE 插件、自动化脚本等通过 MCP 协议直接操作待办项,无需理解 HTTP API 细节。
---
## 七、注意事项
1. **MCP 无状态模式**:当前配置为 Stateless,不支持服务端→客户端通知;如需实时推送仍走 Dynamic API 或 WebSocket
2. **CORS**:开发环境 `AllowAll` 策略已覆盖 `/mcp`;生产环境需按需配置
3. **认证**:当前 MCP 端点未接入认证中间件;如需鉴权,需在 `MapMcpServer` 后追加 `.RequireAuthorization()`
4. **生命周期**:MCP 客户端连接为长连接,建议在组件 `onUnmounted` 时调用 `disconnectMcp()`
@@ -6,12 +6,13 @@
## 一、背景与目标
Hua.Todo v1.3.0 版本聚焦于个核心能力的升级:
Hua.Todo v1.3.0 版本聚焦于个核心能力的升级:
| 序号 | 能力 | 描述 |
|---|---|---|
| 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCPModel Context Protocol)服务,提升服务调用效率与可扩展性 |
| 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 APIAI 拆分) | 待确认 | 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`)、LinuxWebKitGTK 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(语音→文字)
IVoiceInputServiceCore 接口,各平台实现)
↓ 回调文字到前端
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
├─ 在线:LlmIntentParserLLM 结构化输出)
└─ 离线/降级:RuleIntentParser(关键词正则匹配)
意图 + 参数 + confidence
├─ confidence >= 阈值 且 目标唯一 → 执行 → TTS 播报
├─ 目标匹配多个 → 返回候选列表 → 用户确认 → 执行
├─ confidence < 阈值 → TTS "您是要...吗?" → 用户确认
└─ UNKNOWN → TTS "没听懂,请再说一次"
```
---
## 四、验收标准
## 四、需求规格
### 4.1 语音输入(STT
各平台通过原生 API 将语音转为文字,通过 `IVoiceInputService` 接口统一暴露:
```csharp
/// <summary>语音输入服务接口,各平台实现原生 STT</summary>
public interface IVoiceInputService
{
/// <summary>是否正在监听</summary>
bool IsListening { get; }
/// <summary>开始语音识别,识别结果通过回调返回</summary>
Task StartListeningAsync(Action<string> onResult, Action<string>? onError = null);
/// <summary>停止语音识别</summary>
Task StopListeningAsync();
}
```
### 4.2 语音播报(TTS
```csharp
/// <summary>语音输出服务接口,各平台实现原生 TTS</summary>
public interface IVoiceOutputService
{
/// <summary>是否正在播报</summary>
bool IsSpeaking { get; }
/// <summary>播报文本</summary>
Task SpeakAsync(string text);
/// <summary>停止播报</summary>
Task StopSpeakingAsync();
}
```
### 4.3 语音指令解析
#### 意图定义
| 意图 (Intent) | 参数 | 说明 |
|---|---|---|
| `CREATE` | `title`(必填)、`priority`(可选:High/Medium/Low | 创建任务 |
| `UPDATE` | `targetTitle`(必填)、`newTitle`(必填) | 修改任务标题 |
| `DELETE` | `targetTitle`(必填) | 删除任务 |
| `COMPLETE` | `targetTitle`(必填) | 标记完成 |
| `UNCOMPLETE` | `targetTitle`(必填) | 取消完成 |
| `QUERY` | `filter`(可选:如"未完成"、"高优先级" | 查询任务 |
| `ADD_SUBTASK` | `parentTitle`(必填)、`subTitle`(必填) | 添加子任务 |
| `AI_BREAKDOWN` | `targetTitle`(必填) | AI 辅助拆分(仅在线) |
| `UNKNOWN` | 无 | 无法识别 |
#### 歧义处理规则
当指令中 `targetTitle` 匹配到多个 Todo 待办项时:
1. **不直接执行**,返回歧义结果(包含匹配的候选列表)
2. 前端展示候选项,用户点选或语音确认后再执行
3. 匹配规则:标题完全匹配优先,包含匹配次之
#### 置信度阈值
- LLM 返回 `confidence >= 0.8`:直接执行
- `0.5 <= confidence < 0.8`:确认后执行(TTS "您是要...吗?"
- `confidence < 0.5`:按 UNKNOWN 处理
### 4.4 语音指令 API
```
POST /api/voice/command
请求体:
{
"text": "帮我把那个开会的删了吧" // STT 识别后的原始文字
}
响应体:
{
"intent": "DELETE", // 解析出的意图
"params": { // 提取的参数
"targetTitle": "开会"
},
"confidence": 0.95, // 置信度
"result": { // 执行结果
"success": true,
"message": "已删除任务:开会",
"data": null
}
}
```
歧义响应:
```json
{
"intent": "COMPLETE",
"params": { "targetTitle": "开会" },
"confidence": 0.92,
"result": {
"success": false,
"message": "找到多个匹配的任务,请确认",
"ambiguity": true,
"candidates": [
{ "id": 1, "title": "开会" },
{ "id": 5, "title": "开会讨论方案" }
]
}
}
```
用户确认后二次请求:
```
POST /api/voice/command/confirm
请求体:
{
"intent": "COMPLETE",
"targetId": 5 // 用户选择的候选 ID
}
```
### 4.5 AI 辅助任务拆分
```
POST /api/voice/ai-breakdown
请求体:
{
"taskId": 5 // 要拆分的目标任务 ID
}
响应体:
{
"suggestions": [ // LLM 生成的子任务建议
{ "title": "准备会议议程", "priority": "Medium" },
{ "title": "发送会议邀请", "priority": "High" },
{ "title": "预定会议室", "priority": "Low" }
]
}
```
用户确认后批量创建:
```
POST /api/voice/ai-breakdown/confirm
请求体:
{
"parentTaskId": 5,
"subTasks": [ // 用户勾选后的子任务列表
{ "title": "准备会议议程", "priority": "Medium" },
{ "title": "发送会议邀请", "priority": "High" }
]
}
```
**关键约束**
- LLM 调用在 Host 端执行,API Key 不暴露到客户端
- 意图解析与 AI 拆分共用同一 LLM 基础设施
- 建议≠直接执行,必须用户确认后才创建子任务
- 离线模式下 AI 拆分不可用,TTS 播报"离线模式不支持 AI 拆分"
- MCP 服务就绪后,`aiBreakdown` 可同步暴露为 MCP 工具
---
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 创建任务指令 | 说出"创建任务 测试" | 成功创建标题为"测试"的任务 |
| 创建带优先级任务 | 说出"创建高优先级任务 紧急" | 成功创建高优先级任务 |
| 完成任务指令 | 说出"完成任务 测试" | 任务状态变为已完成 |
| 删除任务指令 | 说出"删除任务 测试" | 任务被成功删除 |
| 查询任务指令 | 说出"查询未完成任务" | 返回未完成任务列表 |
| 语音播报 | 调用 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/` | 新增 | 语音相关 DTOVoiceIntentResult、VoiceCommandRequest 等) |
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 语音服务 DI 注册 |
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceInputService.cs` | 新增 | Windows STT 实现 |
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceOutputService.cs` | 新增 | Windows TTS 实现 |
| `src/Hua.Todo.Maui/Platforms/Android/VoiceInputService.cs` | 新增 | Android STT 实现 |
| `src/Hua.Todo.Maui/Platforms/Android/VoiceOutputService.cs` | 新增 | Android TTS 实现 |
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceInputService.cs` | 新增 | macOS STT 实现 |
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceOutputService.cs` | 新增 | macOS TTS 实现 |
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceInputService.cs` | 新增 | iOS STT 实现 |
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceOutputService.cs` | 新增 | iOS TTS 实现 |
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` | 新增 | Linux STT 实现 |
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceOutputService.cs` | 新增 | Linux TTS 实现 |
| `src/Hua.Todo.Web/src/api/voice.ts` | 新增 | 前端语音 API 模块 |
| `src/Hua.Todo.Web/src/composables/useVoiceInput.ts` | 新增 | 前端语音输入组合式函数(含 Web Speech API 降级) |
---
**工单编号**02
**标题**:语音通话与语音控制功能
**标题**:语音控制与 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 基类迁移(可选) | 若工单 09v1.2.0)已完成,TaskEntity 已转为 ABP 基类,字段新增方式不同 |
## 四、详细规格
### 4.1 TaskType 枚举
```csharp
/// <summary>待办项类型,区分普通待办项与会议</summary>
public enum TaskType
{
/// <summary>普通待办项(默认)</summary>
Normal = 0,
/// <summary>会议:包含录音/纪要,支持 AI 任务拆分</summary>
Meeting = 1
}
```
文件位置:`src/Hua.Todo.Core/Entities/TaskType.cs`
### 4.2 TaskEntity 新增字段
```csharp
/// <summary>待办项类型</summary>
public TaskType TaskType { get; set; } = TaskType.Normal;
/// <summary>会议纪要/转写文字(仅 Meeting 类型有值)</summary>
[MaxLength(20000)]
public string? MeetingNotes { get; set; }
/// <summary>录音时长(秒),仅 Meeting 类型有值</summary>
public double? AudioDuration { get; set; }
```
### 4.3 EF Core 配置(TodoDbContext.cs
```csharp
builder.Entity<TaskEntity>(b =>
{
// ... 现有配置 ...
b.Property(x => x.TaskType)
.HasDefaultValue(TaskType.Normal)
.HasConversion<int>(); // 枚举存为整数
b.Property(x => x.MeetingNotes)
.HasMaxLength(20000); // 最多约 20000 字符
b.Property(x => x.AudioDuration)
.IsRequired(false);
});
```
### 4.4 数据库迁移
```
dotnet ef migrations add AddMeetingFieldsToTasks
```
迁移应在 `Hua.Todo.Application/Migrations/` 目录生成。
### 4.5 DTO 扩展
**CreateTaskDto** 新增:
```csharp
/// <summary>待办项类型(0=Normal, 1=Meeting),默认 Normal</summary>
public TaskType TaskType { get; set; } = TaskType.Normal;
```
**TaskDto** 新增:
```csharp
/// <summary>待办项类型</summary>
public TaskType TaskType { get; set; }
/// <summary>会议纪要/转写文字</summary>
public string? MeetingNotes { get; set; }
/// <summary>录音时长(秒)</summary>
public double? AudioDuration { get; set; }
```
### 4.6 会议 DTO
```csharp
/// <summary>转写请求</summary>
public class TranscribeRequest
{
public IFormFile Audio { get; set; } = null!;
public string? Format { get; set; }
}
/// <summary>转写响应</summary>
public class TranscribeResponse
{
public int TaskId { get; set; }
public string Transcript { get; set; } = string.Empty;
public double AudioDuration { get; set; }
}
/// <summary>保存会议纪要请求</summary>
public class MeetingNotesRequest
{
public string Notes { get; set; } = string.Empty;
}
/// <summary>会议纪要响应</summary>
public class MeetingNotesResponse
{
public int TaskId { get; set; }
public string MeetingNotes { get; set; } = string.Empty;
}
```
### 4.7 API 端点
#### POST /api/meeting/{taskId}/transcribe
- 接收:multipart/form-data(音频文件)
- 返回:`TranscribeResponse`
- 业务:音频上传后异步转写,结果存回 `TaskEntity.MeetingNotes`
- 当前骨架:返回占位文字,具体转写逻辑由 03-02 实现
#### POST /api/meeting/{taskId}/notes
- 接收:`MeetingNotesRequest`
- 返回:`MeetingNotesResponse`
- 业务:保存/更新会议纪要
### 4.8 MeetingService 骨架
```csharp
/// <summary>会议业务服务</summary>
public class MeetingService
{
private readonly ITaskRepository _taskRepo;
public MeetingService(ITaskRepository taskRepo)
{
_taskRepo = taskRepo;
}
/// <summary>验证 taskId 对应的待办项存在且为 Meeting 类型</summary>
public async Task<TaskEntity> GetMeetingTaskOrThrow(int taskId)
{
var task = await _taskRepo.GetAsync(taskId);
if (task == null)
throw new NotFoundException($"任务 {taskId} 不存在");
if (task.TaskType != TaskType.Meeting)
throw new BusinessException($"任务 {taskId} 不是会议类型");
return task;
}
/// <summary>保存会议纪要</summary>
public async Task<TaskEntity> SaveNotes(int taskId, string notes)
{
var task = await GetMeetingTaskOrThrow(taskId);
task.MeetingNotes = notes;
task.UpdatedAt = DateTime.UtcNow;
await _taskRepo.UpdateAsync(task);
return task;
}
/// <summary>保存转写结果与音频时长</summary>
public async Task<TaskEntity> SaveTranscript(int taskId, string transcript, double audioDuration)
{
var task = await GetMeetingTaskOrThrow(taskId);
task.MeetingNotes = transcript;
task.AudioDuration = audioDuration;
task.UpdatedAt = DateTime.UtcNow;
await _taskRepo.UpdateAsync(task);
return task;
}
}
```
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| TaskType 枚举可用 | 编译通过 | `TaskType.Normal` / `TaskType.Meeting` 可正常赋值 |
| DB 迁移可执行 | `dotnet ef database update` | `Tasks` 表新增 `TaskType`/`MeetingNotes`/`AudioDuration` 列 |
| 创建 Meeting 类型任务 | `POST /api/task``taskType:1` | 返回的 TaskDto 中 `taskType=1` |
| 保存会议纪要 | `POST /api/meeting/{id}/notes` | `meetingNotes` 字段更新成功 |
| 非 Meeting 类型调用会议 API | 普通任务调 `/api/meeting/{id}/notes` | 返回业务异常 |
| 现有代码兼容 | 运行所有已有测试 | 不破坏现有业务 |
## 六、风险
| 风险 | 影响 | 应对 |
|---|---|---|
| TaskEntity 已重构为 ABP 基类 | 字段新增方式不同 | 通过 ABP 的 `ExtraProperties` 或标准字段新增,迁移方式略有调整 |
| 字段长度限制 | 超长会议纪要截断 | `MeetingNotes``MaxLength(20000)`,前端也做长度限制 |
## 七、Touch List
| 文件路径 | 修改类型 | 是否共享 |
|---|---|---|
| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 |
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 |
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 |
| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 |
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 |
| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 |
| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 新增 | 否 |
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 |
| `Migrations/AddMeetingFieldsToTasks.cs` | 新增 | 是 |
---
**工单编号**03-01
**标题**:会议数据模型与 API
**版本**v1.3.0
**创建日期**2026-06-16
@@ -0,0 +1,199 @@
# 研发工单 v1.3.0 - 03-02 音频录制与转写
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01API 契约)
---
## 一、目标
实现前端音频录制(MediaRecorder API)和后端音频转写(STT)能力,打通"录音 → 上传 → 转写文字 → 保存为会议纪要"的完整链路。
## 二、范围
**包含**
- 前端录音控件(`AudioRecorder.vue`
- 前端录音组合式函数(`useAudioRecorder.ts`
- 录音状态管理(录制中/暂停/停止/时长显示)
- 音频上传 API 交互(POST multipart/form-data
- 后端音频文件临时存储
- 后端 STT 转写服务(`SttService.cs`
- 转写结果回写 `MeetingNotes`
**不包含**
- 实时转写(边录边转)
- 音频持久存储(转写完成后删除音频文件)
- 音频云同步
- 语音指令输入(属于工单 02
## 三、前置条件
| 条件 | 说明 |
|---|---|
| 03-01 完成 | `MeetingController``MeetingService` 基础骨架就绪 |
## 四、详细规格
### 4.1 前端录音控件
**AudioRecorder.vue**
```
┌────────────────────────────────┐
│ 🎤 会议录音 │
│ │
│ ● 录制中... 00:15:23 │
│ ┌──────────────────────────┐ │
│ │ ▁▃▂▄▅▂▁▃▄▅▃▂▁▂▄▅▃▁ │ │ ← 简易波形
│ └──────────────────────────┘ │
│ │
│ [⏹ 停止录音] │
│ │
│ 录音时长限制:最长 2 小时 │
└────────────────────────────────┘
```
状态:
- **就绪**:显示录音按钮
- **录制中**:显示停止按钮 + 时长计时 + 简易波形条
- **已停止**:显示"提交转写" / "重新录制"
### 4.2 useAudioRecorder 组合式函数
```typescript
/// <summary>音频录制器组合式函数,封装 MediaRecorder API</summary>
export function useAudioRecorder() {
// 状态
const isRecording = ref(false)
const isPaused = ref(false)
const duration = ref(0) // 秒
const audioBlob = ref<Blob | null>(null)
const audioUrl = ref<string | null>(null) // 用于预览播放
// 方法
async function startRecording(): Promise<void> // 请求麦克风权限,开始录制
function stopRecording(): void // 停止录制,生成 Blob
function resetRecording(): void // 重置状态
function getAudioBlob(): Blob | null // 获取录制结果
// 内部
let mediaRecorder: MediaRecorder | null = null
let timerInterval: number | null = null
// 音频格式:webmChrome/Firefox)、mp4Safari
// 时长限制:最长 2 小时(7200 秒)
return { isRecording, isPaused, duration, audioBlob, audioUrl,
startRecording, stopRecording, resetRecording, getAudioBlob }
}
```
### 4.3 前端 API 模块
```typescript
// src/Hua.Todo.Web/src/api/meeting.ts
/// <summary>上传音频文件并请求转写</summary>
async function transcribeAudio(taskId: number, audioBlob: Blob): Promise<TranscribeResponse>
{
const formData = new FormData();
formData.append('audio', audioBlob, 'meeting.webm');
const apiBaseUrl = window.__API_BASE_URL__ || 'http://localhost:5173/api';
const resp = await fetch(`${apiBaseUrl}/meeting/${taskId}/transcribe`, {
method: 'POST',
body: formData
});
if (!resp.ok) throw new Error(`转写请求失败: ${resp.status}`);
return resp.json();
}
```
### 4.4 后端 SttService
```csharp
/// <summary>语音转写服务接口</summary>
public interface ISttService
{
/// <summary>将音频文件转写为文字</summary>
/// <returns>转写文字</returns>
Task<string> TranscribeAsync(Stream audioStream, string format, CancellationToken ct = default);
}
```
实现策略(按优先级):
| 平台/环境 | 实现方式 | 说明 |
|---|---|---|
| Windows MAUI | `Windows.Media.SpeechRecognition` 文件识别 | 系统自带,离线可用 |
| macOS/iOS | `SFSpeechRecognizer` 文件识别 | 需在线 |
| Android | `SpeechRecognizer` | 需在线 |
| Linux | WebKit 在线;本地 `whisper.cpp` 降级 | 多策略 |
| 通用服务端 | 扩展 `LlmClientService` 调用 Whisper API | Host 端部署 |
> 初期(v1.3.0)优先实现服务端 Whisper API 调用方式(通过 `LlmClientService` 扩展),后续版本各平台原生逐补。
### 4.5 音频上传处理流程
```
前端 AudioRecorder → stopRecording → Blob (webm/mp4)
POST /api/meeting/{taskId}/transcribe (multipart/form-data)
MeetingController.Transcribe()
↓ 保存音频临时文件到 meetings/ 目录
↓ 调用 ISttService.TranscribeAsync()
↓ 得到文字结果
↓ 删除临时音频文件
↓ 调用 MeetingService.SaveTranscript() 保存到数据库
返回 TranscribeResponse { taskId, transcript, audioDuration }
```
### 4.6 错误处理
| 场景 | 处理 |
|---|---|
| 音频格式不支持 | 返回 400 "不支持的音频格式,支持 webm/wav/mp3" |
| 音频文件太大 | 返回 400 "音频文件过大,请控制录音在 2 小时以内" |
| STT 服务不可用 | 返回 503 "转写服务暂不可用,请稍后重试" |
| taskId 不是会议类型 | 返回 400 "该待办项不是会议类型" |
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 录音按钮可用 | 点击录音按钮 | 浏览器弹出麦克风权限请求 |
| 录制过程 | 授权后开始录制 | 显示录音时长,波形条有变化 |
| 停止录音 | 点击停止按钮 | 时长停止,显示"提交转写"按钮 |
| 重新录制 | 点击"重新录制" | 状态重置,可再次录制 |
| 上传转写 | 提交录音文件 | 返回转写文字 |
| 纪要保存 | 转写完成后查看任务 | `meetingNotes` 字段有转写文字 |
| 权限拒绝 | 浏览器拒绝麦克风 | 提示"无法访问麦克风,请使用文字输入" |
| 浏览器不支持 | IE/Safari 旧版等 | 提示"当前浏览器不支持录音,请使用文字输入" |
## 六、风险
| 风险 | 影响 | 应对 |
|---|---|---|
| Safari 不支持 webm | 无法录制 | 使用 mp4 格式(Safari 支持);MIME type 自动适配 |
| STT 准确率不足 | 转写错误多 | 支持用户编辑修正转写结果 |
| 长音频转写耗时长 | 用户等待 | 前端显示转写进度或"转写中"加载状态 |
## 七、Touch List
| 文件路径 | 修改类型 |
|---|---|
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 |
| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 |
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 |
| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 |
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
---
**工单编号**03-02
**标题**:音频录制与转写
**版本**v1.3.0
**创建日期**2026-06-16
@@ -0,0 +1,281 @@
# 研发工单 v1.3.0 - 03-03 AI 任务拆分服务
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01API 契约)、工单 02`LlmClientService`
---
## 一、目标
实现基于 LLM 的会议内容分析服务:接收会议纪要文字,通过 LLM prompt 工程提取行动项,生成结构化的待办项建议列表(含标题、优先级、理由)。
## 二、范围
**包含**
- `MeetingAiBreakdownService`:会议文本 → LLM → 结构化建议列表
- 会议拆分专用 prompt 设计与迭代
- AI 拆分 API 端点:`POST /api/meeting/{taskId}/breakdown`
- 批量创建确认端点:`POST /api/meeting/{taskId}/breakdown/confirm`
- 在线可用性检测(离线时拒绝 AI 拆分请求)
- 复用工单 02 的 `LlmClientService`
**不包含**
- 单任务拆分(属于工单 02 `AI_BREAKDOWN` 意图)
- LLM 基础设施搭建(由工单 02 提供)
- 前端审阅 UI(由 03-04 完成)
## 三、前置条件
| 条件 | 说明 |
|---|---|
| 03-01 完成 | `MeetingController` 骨架就绪 |
| 工单 02 `LlmClientService` | LLM 调用封装可用 |
## 四、详细规格
### 4.1 MeetingAiBreakdownService
```csharp
/// <summary>会议 AI 拆分服务,将会议文字内容分析为待办项建议</summary>
public class MeetingAiBreakdownService
{
private readonly ILlmClientService _llmClient;
private readonly MeetingService _meetingService;
private readonly ITaskService _taskService;
public MeetingAiBreakdownService(
ILlmClientService llmClient,
MeetingService meetingService,
ITaskService taskService) { ... }
/// <summary>分析会议内容,返回待办项建议列表</summary>
public async Task<List<MeetingTaskSuggestion>> AnalyzeAsync(
int taskId, string? notes = null, CancellationToken ct = default)
{
// 1. 验证 taskId 为 Meeting 类型
var meeting = await _meetingService.GetMeetingTaskOrThrow(taskId);
// 2. 获取会议文字(参数优先,否则取数据库中的 meetingNotes
var meetingText = notes ?? meeting.MeetingNotes;
if (string.IsNullOrWhiteSpace(meetingText))
throw new BusinessException("会议内容为空,请先输入纪要或上传录音");
// 3. 构建 prompt,调用 LLM
var prompt = BuildBreakdownPrompt(meetingText, meeting.Title);
var response = await _llmClient.SendAsync(prompt, ct);
// 4. 解析 LLM 返回的 JSON 为建议列表
return ParseSuggestions(response);
}
/// <summary>确认建议并批量创建子任务</summary>
public async Task<BatchCreateResult> ConfirmAndCreateAsync(
int taskId, List<SubTaskCreateItem> subTasks, CancellationToken ct = default)
{
// 1. 验证 taskId 为 Meeting 类型
await _meetingService.GetMeetingTaskOrThrow(taskId);
// 2. 批量创建子任务
var created = new List<TaskDto>();
foreach (var item in subTasks)
{
var dto = new CreateTaskDto
{
Title = item.Title,
Priority = item.Priority,
ParentTaskId = taskId
};
created.Add(await _taskService.CreateTaskAsync(dto));
}
return new BatchCreateResult { CreatedCount = created.Count, SubTasks = created };
}
}
```
### 4.2 LLM Prompt 设计
```
System Prompt:
你是专业的项目管理和会议纪要分析助手。根据用户提供的会议内容,提取所有需要
后续行动的事项,生成待办项建议列表。
要求:
1. 每条建议包含标题(title)、优先级(priority)、原因(reason
2. 标题应简洁明确(15 字以内),如"整理需求文档"、"安排评审会议"
3. 优先级:High(2)=紧急重要,Medium(1)=一般,Low(0)=可延迟
4. 原因应解释为什么需要这个待办项,引用会议中的具体决策或讨论
5. 只提取明确需要执行的事项,不要生成模糊或无来源的建议
6. 建议数量根据会议内容合理判断,通常 3-10 条
7. 如果会议内容无法提取出明确的行动项,返回空列表
输出格式(只输出 JSON,不要解释):
{
"suggestions": [
{
"title": "待办项标题",
"priority": 1,
"reason": "原因说明"
}
]
}
```
### 4.3 DTO 定义
```csharp
/// <summary>会议任务拆分建议</summary>
public class MeetingTaskSuggestion
{
/// <summary>建议的待办项标题</summary>
public string Title { get; set; } = string.Empty;
/// <summary>建议优先级</summary>
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
/// <summary>拆分原因(LLM 输出,用于 UI 展示)</summary>
public string Reason { get; set; } = string.Empty;
}
/// <summary>拆分请求</summary>
public class BreakdownRequest
{
/// <summary>会议纪要文字(可选,不传则使用已保存的 meetingNotes</summary>
public string? Notes { get; set; }
}
/// <summary>拆分响应</summary>
public class BreakdownResponse
{
public int TaskId { get; set; }
public List<MeetingTaskSuggestion> Suggestions { get; set; } = new();
}
/// <summary>批量创建请求中的单个子任务</summary>
public class SubTaskCreateItem
{
public string Title { get; set; } = string.Empty;
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
}
/// <summary>批量确认请求</summary>
public class ConfirmBreakdownRequest
{
public List<SubTaskCreateItem> SubTasks { get; set; } = new();
}
/// <summary>批量创建结果</summary>
public class BatchCreateResult
{
public int CreatedCount { get; set; }
public List<TaskDto> SubTasks { get; set; } = new();
}
```
### 4.4 API 端点
#### POST /api/meeting/{taskId}/breakdown
- 接收:`BreakdownRequest`
- 返回:`ApiResponse<BreakdownResponse>`
- 处理:
1. 获取会议文字(参数优先于数据库)
2. 调用 `MeetingAiBreakdownService.AnalyzeAsync()`
3. 返回建议列表
- 错误:
- 内容为空 → 400 "会议内容为空"
- 离线 → 503 "AI 拆分仅在线可用"
- taskId 非 Meeting 类型 → 400
#### POST /api/meeting/{taskId}/breakdown/confirm
- 接收:`ConfirmBreakdownRequest`
- 返回:`ApiResponse<BatchCreateResult>`
- 处理:
1. 验证 taskId 为 Meeting 类型
2. 逐条创建子任务(通过 `TaskService.CreateTaskAsync`
3. 返回创建结果
### 4.5 LLM 响应解析
```csharp
/// <summary>解析 LLM 返回的 JSON 为建议列表</summary>
private List<MeetingTaskSuggestion> ParseSuggestions(string llmResponse)
{
// 1. 清理 LLM 响应(移除可能的 markdown 代码块包裹、前后空白)
var json = CleanJsonResponse(llmResponse);
// 2. 反序列化
var result = JsonSerializer.Deserialize<LlBreakdownResponse>(json);
// 3. 验证
if (result?.Suggestions == null || result.Suggestions.Count == 0)
return new List<MeetingTaskSuggestion>();
// 4. 过滤无效条目(标题为空)
return result.Suggestions
.Where(s => !string.IsNullOrWhiteSpace(s.Title))
.Select(s => new MeetingTaskSuggestion
{
Title = s.Title.Trim(),
Priority = s.Priority,
Reason = s.Reason?.Trim() ?? string.Empty
})
.ToList();
}
/// <summary>LLM 原始响应结构</summary>
private class LlBreakdownResponse
{
public List<MeetingTaskSuggestion> Suggestions { get; set; } = new();
}
```
### 4.6 错误处理
| 场景 | HTTP 状态码 | message |
|---|---|---|
| 会议文字内容为空 | 400 | 会议内容为空,请先输入纪要或上传录音 |
| taskId 不存在 | 404 | 待办项不存在 |
| taskId 不是 Meeting 类型 | 400 | 该待办项不是会议类型 |
| LLM API 调用失败 | 502 | AI 拆分服务暂时不可用,请稍后重试 |
| LLM 返回格式异常 | 500 | AI 拆分结果解析失败 |
| 离线 | 503 | AI 拆分仅在线可用 |
| 确认时子任务列表为空 | 400 | 请至少选择一个待办项 |
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 会议内容分析 | 提交一段会议纪要文字 | 返回 3-10 条结构化建议 |
| 建议质量 | 检查建议的标题、优先级、原因 | 标题明确、优先级合理、原因引用会议内容 |
| 空内容拒绝 | 不传 notes 且 meetingNotes 为空 | 返回 400 |
| 非 Meeting 类型拒绝 | 传普通待办项 ID | 返回 400 |
| 在线依赖 | 离线时请求拆分 | 返回 503 |
| 批量创建 | 确认勾选的建议 | 子任务批量创建成功 |
| LLM 异常降级 | 模拟 LLM 调用失败 | 返回 502,不影响其他功能 |
| 空结果处理 | 会议内容无可提取的待办项 | 返回空建议列表 + 提示信息 |
## 六、风险
| 风险 | 影响 | 应对 |
|---|---|---|
| LLM 返回格式不符 | 建议解析失败 | 增加 JSON 清理逻辑(移除 markdown 包裹、前后文本) |
| LLM Token 限制 | 长会议纪要超出上下文窗口 | 限制文本长度(如 4000 字);超长时截断并告知用户 |
| Prompt 需要迭代 | 建议质量不达预期 | prompt 作为配置项,支持热更新 |
## 七、Touch List
| 文件路径 | 修改类型 |
|---|---|
| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 |
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 修改 |
---
**工单编号**03-03
**标题**AI 任务拆分服务
**版本**v1.3.0
**创建日期**2026-06-16
@@ -0,0 +1,295 @@
# 研发工单 v1.3.0 - 03-04 任务建议与确认 UI
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01、03-03(后端 API 就绪)
---
## 一、目标
实现会议任务拆分的完整前端 UI:在 TaskItem 中为 Meeting 类型待办项提供录音/纪要/拆分入口,实现建议审阅与确认对话框,打通从会议内容输入到批量创建子任务的端到端用户体验。
## 二、范围
**包含**
- Meeting 类型待办项的特殊 UI(图标、录音/纪要/拆分入口)
- 录音控件的嵌入(调用 03-02 的 `AudioRecorder.vue` + `useAudioRecorder`
- 纪要输入区域(textarea 保存会议文字内容)
- AI 拆分请求与加载状态
- 建议审阅对话框(`MeetingBreakdownDialog.vue`
- 建议勾选、编辑、调整优先级、手动补充
- 确认后批量创建子任务
- 前端类型扩展(`taskType``meetingNotes``audioDuration`
**不包含**
- 录音控件本身(由 03-02 提供)
- 后端逻辑(由 03-01、03-03 提供)
## 三、前置条件
| 条件 | 说明 |
|---|---|
| 03-01 完成 | 后端 API 就绪(meeting 端点 + TaskType |
| 03-02 完成 | AudioRecorder 组件和 useAudioRecorder 可用 |
| 03-03 完成 | breakdown/breakdown/confirm 端点可用 |
## 四、详细规格
### 4.1 前端类型扩展
```typescript
// src/Hua.Todo.Web/src/types/task.ts
/// <summary>待办项类型</summary>
type TaskType = 0 | 1; // 0=Normal, 1=Meeting
interface Task {
// ... 已有字段 ...
/** 待办项类型 */
taskType?: TaskType;
/** 会议纪要/转写文字 */
meetingNotes?: string;
/** 录音时长(秒) */
audioDuration?: number;
}
interface CreateTaskDto {
title: string;
priority: TaskPriority;
parentTaskId?: number;
/** 待办项类型,默认 0 */
taskType?: TaskType;
}
```
### 4.2 会议相关类型
```typescript
// 新增或扩展 meeting.ts 中的类型
/// <summary>会议任务拆分建议</summary>
interface MeetingTaskSuggestion {
title: string;
priority: TaskPriority;
reason: string;
}
/// <summary>拆分响应</summary>
interface BreakdownResponse {
taskId: number;
suggestions: MeetingTaskSuggestion[];
}
/// <summary>待确认的子任务项</summary>
interface PendingSubTask {
title: string;
priority: TaskPriority;
/** 是否被用户勾选 */
checked: boolean;
/** LLM 拆分的原始建议(含原因) */
suggestion?: MeetingTaskSuggestion;
/** 是否为用户手动添加的条目 */
isManual?: boolean;
}
```
### 4.3 TaskItem.vue 扩展
为 Meeting 类型的待办项显示额外操作入口:
```
┌──────────────────────────────────────────────┐
│ 📋 ☐ 周一产品评审会 🔴 H ··· │
│ 🎙 会议 · 📝 有纪要 · 0个子任务 │
│ [🎤] [📝] [🤖] │
│ ┌─ ✅ 整理需求文档 🔴 │ ← 已有子任务(若有)
│ └─ ☐ 安排评审会议 🟡 │
└──────────────────────────────────────────────┘
[🎤] = 录音按钮
[📝] = 打开纪要编辑区
[🤖] = AI 拆分(需有会议内容)
```
条件渲染逻辑:
- `task.taskType === 1`Meeting)时,显示会议图标 `🎙` 标签
- 显示三个操作按钮在任务行右侧
- 拆分子任务入口:点击 `[🤖]` → 触发 `handleAiBreakdown()`
### 4.4 会议纪要编辑区(内联)
点击 `[📝]` 后展开内联编辑区:
```
┌──────────────────────────────────────────────┐
│ 📋 ☐ 周一产品评审会 │
│ │
│ 会议纪要: │
│ ┌──────────────────────────────────────┐ │
│ │ 今天的产品评审会主要讨论了三个议题: │ │
│ │ 第一,确定 Q3 产品路线图... │ │
│ │ 第二,... │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ 录音时长:30分15秒 │
│ │
│ [🔊 开始录音] [🤖 AI拆分] [💾 保存纪要] │
└──────────────────────────────────────────────┘
```
### 4.5 AI 拆分流程(前端)
```typescript
// 在 TaskItem.vue 或拆分逻辑中
async function handleAiBreakdown(task: Task) {
// 1. 检查是否有会议内容
if (!task.meetingNotes) {
showToast('请先输入纪要或上传录音', 'warning');
return;
}
// 2. 调用 AI 拆分
isLoading.value = true;
try {
const resp = await meetingApi.requestBreakdown(task.id);
if (resp.data.suggestions.length === 0) {
showToast('未从会议内容中提取到待办项,请确认纪要内容', 'info');
return;
}
// 3. 打开审阅对话框
openBreakdownDialog(task, resp.data.suggestions);
} catch (err) {
showToast('AI拆分失败,请稍后重试', 'error');
} finally {
isLoading.value = false;
}
}
```
### 4.6 MeetingBreakdownDialog.vue
审阅确认对话框:
```
┌──────────────────────────────────────────────┐
│ 会议任务拆分 — 周一产品评审会 [✕] │
│ │
│ AI 根据会议内容生成了以下待办项建议: │
│ 请勾选需要创建的条目,可编辑标题和优先级。 │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ☑ 整理产品需求文档 [🔴 高 ▼] │ │
│ │ 📝 会议中提到需要在本周五前完成 │ │
│ │ │ │
│ │ ☑ 安排技术方案评审 [🟡 中 ▼] │ │
│ │ 📝 张三负责在下周三前输出技术方案 │ │
│ │ │ │
│ │ ☑ 跟进客户反馈 [🟢 低 ▼] │ │
│ │ 📝 李四反馈了三个客户问题 │ │
│ │ │ │
│ │ ☐ 项目总结报告 [🟡 中 ▼] │ │
│ │ 📝 月末需要汇总各模块进度 │ │
│ └──────────────────────────────────────┘ │
│ │
│ [+ 手动添加待办项] │
│ │
│ 已选 3 项 │
│ [取消] [✅ 确认创建 (3)] │
└──────────────────────────────────────────────┘
```
交互:
- 每条建议默认勾选
- 点击标题可编辑(内联 input
- 优先级下拉可调整
- 取消勾选的条目不会被创建
- `[+ 手动添加]` 新增空白条目
- 底部显示已选数量
### 4.7 确认创建
```typescript
async function confirmBreakdown() {
const selected = pendingSubTasks.value
.filter(s => s.checked)
.map(s => ({ title: s.title, priority: s.priority }));
if (selected.length === 0) {
showToast('请至少选择一个待办项', 'warning');
return;
}
isCreating.value = true;
try {
const resp = await meetingApi.confirmBreakdown(task.id, selected);
showToast(`已成功创建 ${resp.data.createdCount} 个待办项`, 'success');
closeDialog();
emit('updated'); // 刷新任务列表
} catch (err) {
showToast('创建失败,请重试', 'error');
} finally {
isCreating.value = false;
}
}
```
### 4.8 创建任务时的类型选择
`TaskList.vue` 的快速创建表单中增加类型选择:
```
┌──────────────────────────────────────────────┐
│ [+ 新待办项] [_____输入标题_____] │
│ 优先级: [🟡 中 ▼] 类型: [📋 普通 ▼] │
│ [🎙 会议] │
│ [+ 添加] │
└──────────────────────────────────────────────┘
```
默认类型为 `Normal`(普通),选择 `Meeting` 后创建的待办项为会议类型。
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 创建 Meeting 类型 | 新建时选择"会议"类型 | 列表中显示 🎙 图标 |
| 会议操作按钮 | 展开 Meeting 类型任务 | 显示录音/纪要/拆分三个按钮 |
| 普通类型不显示 | 查看普通待办项 | 无会议相关按钮 |
| 录音入口 | 点击录音按钮 | 弹出/展开 AudioRecorder 组件 |
| 纪要编辑 | 输入会议纪要后保存 | `meetingNotes` 保存成功 |
| AI 拆分请求 | 点击 AI 拆分按钮 | 加载状态 → 审阅对话框弹出 |
| 无内容拒绝拆分 | 无纪要时点击拆分 | 提示"请先输入纪要" |
| 建议审阅 | 审阅对话框中勾选/取消/编辑 | 操作流畅,状态正确 |
| 手动添加 | 点击"+ 手动添加" | 新增空白条目,可编辑 |
| 批量创建 | 确认勾选的条目 | 子任务创建到父任务下 |
| 离线降级提示 | 离线时点击拆分 | 提示"AI 拆分仅在线可用" |
| 响应式 | 缩小浏览器窗口 | 对话框和控件正常显示 |
## 六、风险
| 风险 | 影响 | 应对 |
|---|---|---|
| 组件嵌套复杂 | TaskItem 已有子任务递归,再加会议 UI 后层级深 | 会议功能用可选插槽方式注入,不影响普通任务渲染 |
| 录音与拆分同时操作 | 用户可能在转写期间又点拆分 | 操作按钮在加载中时禁用 |
| 大量建议渲染 | LLM 返回 10+ 条建议时列表长 | 审阅列表设最大高度 + 滚动;建议上限 15 条 |
## 七、Touch List
| 文件路径 | 修改类型 |
|---|---|
| `src/Hua.Todo.Web/src/types/task.ts` | 修改(新增 TaskType、meetingNotes、audioDuration |
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 |
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增(由 03-02 产出,本工单集成) |
| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 |
| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改(Meeting 类型条件渲染) |
| `src/Hua.Todo.Web/src/components/TaskList.vue` | 修改(新建任务时类型选择) |
---
**工单编号**03-04
**标题**:任务建议与确认 UI
**版本**v1.3.0
**创建日期**2026-06-16
@@ -0,0 +1,341 @@
# 研发工单 v1.3.0 - 03 会议任务拆分
> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。
---
## 一、目标与范围
### 1.1 目标
实现"会议内容 → AI 任务拆分"功能:用户创建一个标记为"会议"类型的 Todo 待办项作为父目录,通过录音或文字输入会议内容,由 AI 分析后生成待办项建议列表,用户确认后批量创建为子任务。
### 1.2 核心流程
```
用户创建"会议"类型父任务(指定标题,如"周一产品评审会")
选择输入方式:
├─ 录音:录制会议音频 → 系统转写为文字
└─ 文字:直接输入/粘贴会议纪要
AI 分析会议内容 → 生成待办项建议列表
(每条建议含:标题、优先级、可选截止日期)
用户审阅建议列表:
├─ 勾选/取消勾选
├─ 编辑建议内容(修改标题、调整优先级)
└─ 手动补充新条目
确认 → 批量创建为父任务的子待办项
```
### 1.3 范围
**包含**
- "会议"类型标记:创建 Todo 时可指定为会议类型,UI 上以图标/标签区分
- 音频录制:前端录音控件,录制会议音频
- 音频转写:将录音发送至后端,调用 STT 服务转为文字
- 文字输入:直接输入/粘贴会议纪要文本
- AI 任务拆分:将会议文字内容发送给 LLM,生成结构化待办项建议
- 建议审阅与确认 UI:用户勾选、编辑、补充建议后批量创建子任务
- 与工单 02 的 LLM 基础设施复用(`LlmClientService`
**不包含**
- 实时语音转写(边录边转,后续版本考虑)
- 多人协作/会议纪要共享
- 音频文件持久存储(录音转写完成后不保留音频,节省空间;如需保留为后续版本需求)
- 会议录音的云同步(后续版本)
---
## 二、前置条件
| 条件 | 说明 | 状态 |
|---|---|---|
| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 |
| 工单 02 LLM 基础设施 | `LlmClientService`、AI 拆分服务 | 待实现 |
| 浏览器 MediaRecorder API | 前端音频录制 | 已就绪(主流浏览器支持) |
| 后端 STT 服务 | 音频转文字(可复用工单 02 的 STT 能力或调用第三方 API) | 待实现 |
---
## 三、子工单拆分
| 子工单 | 标题 | 依赖 | 可并行 |
|---|---|---|---|
| 03-01 | 会议数据模型与 API | 无 | 是 |
| 03-02 | 音频录制与转写 | 03-01(API 契约) | 部分(前端录音 UI 可并行) |
| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 部分(prompt 设计可并行) |
| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 否(依赖后端 API 就绪) |
### 执行顺序建议
```
03-01(模型与API) ──────┐
├──→ 03-04(确认UI)
03-02(录制与转写) ──────┤
03-03(AI拆分服务) ──────┘
```
03-01、03-02、03-03 可部分并行推进;03-04 需等前三者 API 就绪后开始。
---
## 四、架构设计
### 4.1 整体架构
```
┌──────────────────────────────────────────────────┐
│ Vue 前端 │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ 录音控件 │ │ 文字输入区 │ │ 建议审阅 │ │
│ │ MediaRecorder│ │ Textarea │ │ 确认对话框 │ │
│ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌──────────────────────────────┐ │ │
│ │ meetingApi (前端 API 模块) │ │ │
│ └──────────────┬───────────────┘ │ │
└─────────────────┼──────────────────────┼─────────┘
│ HTTP │
▼ │
┌─────────────────────────────────────────┤
│ 后端 (Application 层) │
│ ┌────────────┐ ┌─────────────────┐ │
│ │ Meeting │ │ MeetingAi │ │
│ │ Controller │ │ BreakdownService│ │
│ └─────┬──────┘ └───────┬─────────┘ │
│ │ │ │
│ ┌─────▼──────┐ ┌──────▼─────────┐ │
│ │ Meeting │ │ LlmClient │ │
│ │ Service │ │ Service (复用02)│ │
│ └─────┬──────┘ └────────────────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ STT 服务 │ │
│ │ (转写音频) │ │
│ └────────────┘ │
└─────────────────────────────────────────┘
```
### 4.2 数据模型扩展
在现有 `TaskEntity` 上新增 `TaskType` 字段,区分普通待办项与会议:
```csharp
/// <summary>待办项类型枚举</summary>
public enum TaskType
{
/// <summary>普通待办项(默认)</summary>
Normal = 0,
/// <summary>会议(可包含录音、纪要,支持 AI 任务拆分)</summary>
Meeting = 1
}
```
`TaskEntity` 新增字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `TaskType` | `TaskType` | `Normal` | 待办项类型 |
| `MeetingNotes` | `string?` | null | 会议纪要/转写文字(仅 Meeting 类型有值) |
| `AudioDuration` | `double?` | null | 录音时长(秒),仅 Meeting 类型有值 |
> 注意:音频文件本身不持久存储,转写完成后仅保留文字结果。`AudioDuration` 用于 UI 展示录音时长。
### 4.3 API 设计
#### 4.3.1 创建会议类型待办项
```
POST /api/task
请求体:
{
"title": "周一产品评审会",
"priority": 1,
"taskType": 1 // TaskType.Meeting = 1
}
响应体:
{
"success": true,
"data": {
"id": 42,
"title": "周一产品评审会",
"taskType": 1,
"meetingNotes": null,
"audioDuration": null,
...
}
}
```
#### 4.3.2 上传录音并转写
```
POST /api/meeting/{taskId}/transcribe
Content-Type: multipart/form-data
字段:
- audio: 音频文件(webm/wav/mp3
- format: 音频格式(可选,默认从文件扩展名推断)
响应体:
{
"success": true,
"data": {
"taskId": 42,
"transcript": "今天的产品评审会主要讨论了三个议题:第一...",
"audioDuration": 1830.5
}
}
```
#### 4.3.3 保存会议纪要(文字输入)
```
POST /api/meeting/{taskId}/notes
请求体:
{
"notes": "今天的产品评审会主要讨论了三个议题:第一..."
}
响应体:
{
"success": true,
"data": {
"taskId": 42,
"meetingNotes": "今天的产品评审会主要讨论了三个议题:第一..."
}
}
```
#### 4.3.4 AI 任务拆分建议
```
POST /api/meeting/{taskId}/breakdown
请求体:
{
"notes": "..." // 可选,若不传则使用已保存的 meetingNotes
}
响应体:
{
"success": true,
"data": {
"taskId": 42,
"suggestions": [
{
"title": "整理产品需求文档",
"priority": 2,
"reason": "会议中提到需要在本周五前完成需求文档的整理"
},
{
"title": "安排技术方案评审",
"priority": 1,
"reason": "张三负责在下周三前输出技术方案"
},
{
"title": "跟进客户反馈",
"priority": 0,
"reason": "李四反馈了三个客户问题,需后续跟进"
}
]
}
}
```
#### 4.3.5 确认并批量创建子任务
```
POST /api/meeting/{taskId}/breakdown/confirm
请求体:
{
"subTasks": [
{ "title": "整理产品需求文档", "priority": 2 },
{ "title": "安排技术方案评审", "priority": 1 }
]
}
响应体:
{
"success": true,
"data": {
"createdCount": 2,
"subTasks": [
{ "id": 43, "title": "整理产品需求文档", "priority": 2, "parentTaskId": 42 },
{ "id": 44, "title": "安排技术方案评审", "priority": 1, "parentTaskId": 42 }
]
}
}
```
---
## 五、与工单 02 的边界
| 能力 | 工单 02 | 工单 03 |
|---|---|---|
| LLM 调用 | `LlmClientService` 基础设施 | 复用 `LlmClientService`,新增会议拆分专用 prompt |
| 语音输入 | STT 平台原生能力(语音指令) | 前端 MediaRecorder 录音 → 后端 STT 转写(场景不同) |
| AI 拆分 | 对已有单个任务做拆分(`AI_BREAKDOWN` 意图) | 对会议内容做拆分(输入为长文本,输出为多条建议) |
| 确认流程 | 语音确认/点选候选 | 专门的审阅 UI(勾选、编辑、补充) |
**复用关系**
- `LlmClientService`:03 直接复用 02 的 LLM 调用封装
- `AiBreakdownService`:02 的单任务拆分服务,03 不直接复用(输入形式和输出结构不同),但设计上保持一致的调用模式
---
## 六、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 创建会议类型待办项 | 创建 Todo 时选择"会议"类型 | 创建成功,列表中显示会议图标/标签 |
| 录音功能 | 点击录音按钮,录制一段音频 | 录音控件正常工作,显示录音时长 |
| 录音转写 | 录音完成后提交 | 后端返回转写文字,文字内容基本准确 |
| 文字输入纪要 | 在会议待办项中粘贴会议纪要 | 保存成功,再次打开可见纪要内容 |
| AI 拆分建议 | 提交会议内容请求 AI 拆分 | 返回 3-10 条建议,每条含标题+优先级+理由 |
| 建议审阅 | 勾选/取消/编辑建议 | UI 支持勾选、编辑标题和优先级 |
| 批量创建 | 确认勾选的建议 | 子任务批量创建到父任务下 |
| 离线降级 | 离线时请求 AI 拆分 | 提示"离线模式不支持 AI 拆分" |
| 非会议类型 | 普通待办项不显示录音/拆分入口 | 功能入口仅对 Meeting 类型显示 |
---
## 七、风险与回滚
| 风险 | 影响 | 应对策略 |
|---|---|---|
| STT 转写准确率不足 | 会议纪要质量差,影响 AI 拆分效果 | 支持文字输入作为备选;转写后允许用户编辑修正 |
| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,其他会议功能(录音、纪要)不受影响 |
| 录音文件过大 | 上传超时/存储压力大 | 前端限制录音时长(建议最长 2 小时);压缩音频格式 |
| 浏览器 MediaRecorder 兼容性 | 部分平台录音功能不可用 | 降级提示"当前浏览器不支持录音,请使用文字输入" |
---
## 八、Touch List
| 文件路径 | 修改类型 | 是否共享 | 说明 |
|---|---|---|---|
| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 | TaskType 枚举 |
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 | 新增 TaskType/MeetingNotes/AudioDuration 字段 |
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 | 新增字段映射 + 迁移 |
| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 | DTO 扩展(CreateTaskDto/TaskDto 新增字段) |
| `src/Hua.Todo.Application/Meeting/` | 新增目录 | 否 | 会议相关服务目录 |
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 | 会议 API 端点 |
| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 | 会议业务逻辑 |
| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 | 否 | 会议 AI 拆分服务 |
| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 | 否 | 音频转写服务 |
| `src/Hua.Todo.Application/Meeting/Models/` | 新增 | 否 | 会议相关 DTO |
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 | 否 | 前端会议 API 模块 |
| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 | 否 | 前端录音组合式函数 |
| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 | 否 | 会议任务拆分审阅对话框 |
| `src/Hua.Todo.Web/src/components/MeetingTaskItem.vue` | 新增 | 否 | 会议类型待办项(含录音/纪要入口) |
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 | 否 | 录音控件 |
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 | 类型扩展(taskType/meetingNotes |
---
**工单编号**03
**标题**:会议任务拆分
**版本**v1.3.0
**创建日期**2026-06-16
@@ -0,0 +1,554 @@
# 研发工单 v1.3.0 - 04 富文本描述、附件与外部链接
> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。
---
## 一、目标与范围
### 1.1 目标
当前 Todo 待办项仅包含单行标题(`Title`),无法满足"一个待办项关联更多上下文信息"的需求。本工单为 Todo 待办项新增三项能力:
| 能力 | 描述 |
|---|---|
| **多行描述(Description** | 每个待办项可附带一段多行文字描述,用于记录详细说明、步骤、备注等 |
| **文件附件(Attachments** | 可为一个待办项关联多个本地文件,支持上传、查看、下载、删除 |
| **外部链接与程序启动** | 附件可以是外部 URL(点击在系统默认浏览器打开)或本地文件路径/可执行文件(点击通过系统关联程序打开) |
### 1.2 平台范围
| 平台 | 是否支持 | 说明 |
|---|---|---|
| WindowsMAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 |
| LinuxAvalonia | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 |
| macOSMAUI | 待定 | 视为 Windows 同级,但本期不单独投入 |
| iOS / Android | ❌ 不支持 | 移动端不提供附件/描述编辑入口,但**移动端编辑待办项时不得覆盖/清空已有描述和附件数据**(详见 1.4 节) |
### 1.3 范围
**包含**
- `TaskEntity` 新增 `Description` 字段(多行文本)
- 新增 `AttachmentEntity` 数据模型(附件元数据)
- 附件 CRUD API(上传、列表、下载、删除、打开)
- 前端编辑对话框扩展(描述 textarea + 附件管理区域)
- 前端待办项列表/详情展示(描述预览、附件数量角标)
- 桌面端通过系统关联程序打开附件(URL → 浏览器,文件路径 → 关联应用)
**不包含**
- 附件云同步(本期不涉及,后续版本规划)
- 附件预览(如图片缩略图、PDF 内嵌预览,本期不做)
- 移动端附件管理(本期仅桌面端)
- 富文本编辑器(如 Markdown 渲染,本期仅纯文本多行)
### 1.4 跨平台数据安全:移动端编辑不得破坏已有数据(强制)
**背景**:移动端(iOS/Android)不提供描述/附件的编辑 UI,但用户仍可在移动端修改待办项标题、优先级、完成状态等基础字段。如果没有防护,移动端发起更新请求时会用"空值"覆盖掉桌面端已设置的 `Description``Attachments`,导致数据丢失。
**约束**
| 约束项 | 要求 |
|---|---|
| **后端 API 必须支持真正的部分更新** | `PUT /api/task/{id}` 的请求体中,**未传递的字段保持原值不变**,不得将缺失字段视为"置空"。即:`description` 不在 JSON 中 → 不修改数据库中的 `Description``description``null` → 清空 |
| **UpdateTaskDto 所有扩展字段均为 optional** | `description``attachments` 相关字段在 DTO 中均标记为可选(`string?` / `null`),与必填字段(`id`)区分 |
| **移动端前端不传递未知字段** | 移动端构建 `UpdateTaskDto` 时只传 `id``title``priority``isCompleted`,不传 `description``attachments`。后端对这些字段视为"不修改" |
| **Attachment 实体独立于 Task 更新** | 附件 CRUD 走独立端点(`/api/task/{id}/attachments`),不通过 `PUT /api/task/{id}` 携带。移动端不调附件端点,自然不会破坏附件数据 |
**验证方式**
1. 桌面端创建待办项,添加描述 + 上传附件
2. 移动端编辑同一待办项(只改标题),保存
3. 回到桌面端查看 → 描述和附件完整保留,未被覆盖
---
## 二、前置条件
| 条件 | 说明 | 状态 |
|---|---|---|
| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 |
| 工单 03 TaskType 枚举 | `TaskEntity` 已有字段扩展模式可参考 | 待实现(03 先于 04 或并行) |
| 桌面 WebView 文件选择 | 前端 `<input type="file">` 能力 | 已就绪 |
| 桌面程序启动 | 通过后端 `Process.Start` / `xdg-open` 实现 | 需新增 |
---
## 三、功能入口覆盖检查
> 依据 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md),新功能必须检查两个入口覆盖情况。
| 入口 | 已规划 | 方案 |
|---|---|---|
| UI | ✅ | 编辑对话框新增描述 textarea + 附件列表区域;TaskItem 卡片显示描述预览与附件数量角标 |
| 语音 | ⚠️ 部分 | 语音可追加/编辑描述文本(通过"给「标题」添加描述"意图);附件上传与外部程序启动不适合语音入口 |
> 语音入口覆盖说明:
> - **支持**:通过语音添加/修改待办项描述("给「XXX」添加备注:..."
> - **不支持**:语音上传附件、语音打开外部程序 —— 两者本质上是可视化交互,语音不是合适的入口。本期不覆盖。
> - TODO:工单 02 的 LLM intent parser prompt 需预留 `SET_DESCRIPTION` 意图,参数为 `targetTask` + `description`
---
## 四、数据模型设计
### 4.1 TaskEntity 扩展
在现有 `TaskEntity` 上新增一个字段:
```csharp
/// <summary>
/// 多行描述文本(可为空)。
/// 用于记录待办项相关的详细说明、步骤或备注。
/// </summary>
public string? Description { get; set; }
```
| 字段 | 类型 | 默认值 | 约束 | 说明 |
|---|---|---|---|---|
| `Description` | `string?` | null | 最大 5000 字符 | 多行描述文本 |
### 4.2 AttachmentEntity(新增)
`Hua.Todo.Core/Entities/` 下新增 `AttachmentEntity.cs`
```csharp
/// <summary>附件类型枚举</summary>
public enum AttachmentType
{
/// <summary>本地文件(已复制到应用数据目录)</summary>
LocalFile = 0,
/// <summary>外部链接(URL</summary>
ExternalLink = 1
}
/// <summary>附件实体,表示待办项关联的文件或外部链接</summary>
public class AttachmentEntity
{
/// <summary>附件唯一标识符</summary>
public int Id { get; set; }
/// <summary>所属待办项ID</summary>
public int TaskId { get; set; }
/// <summary>显示名称(用户可见的文件名或链接标题)</summary>
public string FileName { get; set; } = string.Empty;
/// <summary>存储路径(本地文件的相对路径)或外部URL</summary>
public string FilePath { get; set; } = string.Empty;
/// <summary>文件大小(字节),外部链接为 0</summary>
public long FileSize { get; set; }
/// <summary>MIME 类型(如 text/plain、application/pdf),外部链接为空字符串</summary>
public string ContentType { get; set; } = string.Empty;
/// <summary>附件类型</summary>
public AttachmentType AttachmentType { get; set; } = AttachmentType.LocalFile;
/// <summary>创建时间(UTC</summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 导航属性
public TaskEntity Task { get; set; } = null!;
}
```
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `Id` | `int` | 自增 | 主键 |
| `TaskId` | `int` | - | FK → Tasks.Id |
| `FileName` | `string` | - | 显示名称,最大 256 字符 |
| `FilePath` | `string` | - | 存储路径或外部 URL,最大 1024 字符 |
| `FileSize` | `long` | 0 | 字节数 |
| `ContentType` | `string` | "" | MIME 类型 |
| `AttachmentType` | `AttachmentType` | `LocalFile` | 本地文件 vs 外部链接 |
| `CreatedAt` | `DateTime` | `UtcNow` | 创建时间 |
`TaskEntity` 新增导航属性:
```csharp
public List<AttachmentEntity> Attachments { get; set; } = new();
```
### 4.3 数据库表
| 表名 | 说明 |
|---|---|
| `T_Attachments` | 附件表 |
> 注:表名沿用 ABP 模板规范 `T_{实体名}s`(见 [.trae/rules/项目/03-数据模型与迁移约束.md](../../../.trae/rules/项目/03-数据模型与迁移约束.md))。
### 4.4 附件存储策略
| 策略项 | 决定 |
|---|---|
| 存储位置 | 应用数据目录下 `Attachments/` 子目录(与数据库 `.db` 同级) |
| 文件命名 | `{attachmentId}_{originalFileName}`,避免重名冲突 |
| 文件大小上限 | 单文件 50MB(`appsettings.json` 可配置) |
| 总附件数上限 | 每个待办项最多 20 个附件 |
| 外部链接 | 不复制文件,仅存储 URL 字符串;打开时调用系统默认浏览器 |
---
## 五、API 设计
### 5.1 已修改:创建/更新待办项(扩展 Description
```
POST /api/task
请求体新增字段:
{
"title": "...",
"priority": 1,
"description": "多行描述文本(可选)" // ← 新增
}
PUT /api/task/{id}
请求体新增字段(所有扩展字段均为可选):
{
"id": 42,
"title": "...",
"priority": 1,
"description": "..." // ← 可选;不传=保持原值,传 null=清空
}
```
> **部分更新语义(强制)**:`PUT` 端点必须实现"仅更新已传递字段"的逻辑。
> - `description` 字段**不在 JSON 中** → 数据库 `Description` 保持原值
> - `description` 字段**为 `null`** → 数据库 `Description` 清空为 `null`
> - 此设计确保移动端不传递 `description` 时不会意外清空已有描述
### 5.2 附件上传
```
POST /api/task/{taskId}/attachments
Content-Type: multipart/form-data
字段:
- file: 文件内容(必填,最大 50MB)
响应体:
{
"success": true,
"data": {
"id": 1,
"taskId": 42,
"fileName": "需求文档.pdf",
"fileSize": 204800,
"contentType": "application/pdf",
"attachmentType": 0,
"createdAt": "2026-06-16T10:00:00Z"
}
}
```
### 5.3 添加外部链接
```
POST /api/task/{taskId}/attachments/link
请求体:
{
"url": "https://example.com/doc",
"fileName": "参考文档" // 可选,不传则使用 URL 作为显示名
}
响应体:
{
"success": true,
"data": {
"id": 2,
"taskId": 42,
"fileName": "参考文档",
"filePath": "https://example.com/doc",
"fileSize": 0,
"contentType": "",
"attachmentType": 1,
"createdAt": "2026-06-16T10:00:00Z"
}
}
```
### 5.4 获取附件列表
```
GET /api/task/{taskId}/attachments
响应体:
{
"success": true,
"data": [
{
"id": 1,
"fileName": "需求文档.pdf",
"fileSize": 204800,
"contentType": "application/pdf",
"attachmentType": 0,
"createdAt": "2026-06-16T10:00:00Z"
},
{
"id": 2,
"fileName": "参考文档",
"filePath": "https://example.com/doc",
"attachmentType": 1,
"createdAt": "2026-06-16T10:05:00Z"
}
]
}
```
### 5.5 下载附件
```
GET /api/attachments/{attachmentId}/download
响应:文件流(Content-Disposition: attachment; filename="需求文档.pdf"
```
### 5.6 打开附件(桌面端)
```
POST /api/attachments/{attachmentId}/open
响应体:
{
"success": true,
"data": {
"opened": true
}
}
// 后端行为:
// - AttachmentType.LocalFile → Process.Start(filePath)Windows)或 xdg-openLinux
// - AttachmentType.ExternalLink → Process.Start(url)(在默认浏览器打开)
```
### 5.7 删除附件
```
DELETE /api/task/{taskId}/attachments/{attachmentId}
响应体:
{
"success": true,
"message": "附件已删除"
}
```
---
## 六、前端设计
### 6.1 类型扩展
`src/Hua.Todo.Web/src/types/task.ts` 新增 / 修改:
```typescript
// Task 接口新增字段
export interface Task {
// ... 既有字段
description?: string; // 多行描述
attachments?: AttachmentItem[]; // 附件列表
attachmentCount?: number; // 附件数量(列表视图用,不传完整列表)
}
export interface AttachmentItem {
id: number;
taskId: number;
fileName: string;
filePath: string;
fileSize: number;
contentType: string;
attachmentType: AttachmentType; // 0=本地文件, 1=外部链接
createdAt: string;
}
export type AttachmentType = 0 | 1;
```
### 6.2 组件变更
| 组件 | 变更类型 | 说明 |
|---|---|---|
| `TaskEditDialog.vue` | 修改 | 新增描述 textarea + 附件管理区域(上传按钮、附件列表) |
| `TaskItem.vue` | 修改 | 卡片底部显示描述预览(单行截断)+ 附件数量角标 |
| `AttachmentList.vue` | 新增 | 附件列表组件:文件名、大小、类型图标、删除按钮、打开按钮 |
| `LinkInputDialog.vue` | 新增 | 外部链接输入弹出框(URL + 显示名称) |
### 6.3 新增 API 模块
`src/Hua.Todo.Web/src/api/attachments.ts`
```typescript
// uploadAttachment(taskId, file) — POST multipart/form-data
// addLink(taskId, url, fileName?) — POST /api/task/{id}/attachments/link
// getAttachments(taskId) — GET
// deleteAttachment(taskId, attId) — DELETE
// openAttachment(attId) — POST /api/attachments/{id}/open
// downloadAttachment(attId) — GET /api/attachments/{id}/download
```
### 6.4 交互流程
```
编辑待办项对话框
├─ 标题输入框(现有)
├─ 优先级选择(现有)
├─ 描述 textarea(新增:多行文本,placeholder "添加备注、步骤说明..."
├─ 附件区域(新增)
│ ├─ [+ 上传文件] 按钮 → 触发文件选择器 → 上传至后端
│ ├─ [+ 添加链接] 按钮 → 弹出 URL 输入框 → 保存为外部链接附件
│ └─ 附件列表(每项显示:图标 + 文件名 + 大小 + [打开] [删除])
│ ├─ 本地文件:[打开] → 调用后端 open API → 系统关联程序打开
│ ├─ 外部链接:[打开] → 调用后端 open API → 浏览器打开
│ └─ [删除] → 确认 → 删除附件
└─ [取消] [保存]
```
### 6.5 列表卡片展示
```
┌─────────────────────────────────────┐
│ ☐ 整理产品需求文档 │
│ 高优先级 │
│ 需要在本周五前完成需求文档的整理... │ ← 描述预览(单行,超长截断)
│ 📎 2 个附件 │ ← 附件数量角标
└─────────────────────────────────────┘
```
---
## 七、后端架构
### 7.1 新增服务
| 服务/类 | 位置 | 说明 |
|---|---|---|
| `AttachmentEntity` | `Hua.Todo.Core/Entities/` | 附件实体 |
| `AttachmentType` | `Hua.Todo.Core/Entities/` | 附件类型枚举 |
| `IAttachmentRepository` | `Hua.Todo.Core/Repositories/` | 仓储接口 |
| `AttachmentRepository` | `Hua.Todo.Application/Data/` | EF Core 仓储实现 |
| `AttachmentService` | `Hua.Todo.Application/Services/` | 附件业务逻辑(上传、下载、打开、清理) |
| `IAttachmentService` | `Hua.Todo.Application/Services/` | 服务接口 |
| `AttachmentController` | `Hua.Todo.Application/DynamicApi/` 或独立 Controller | 附件动态 API 端点(或手动 Controller |
| `PlatformAttachmentOpener` | `Hua.Todo.{Maui,Avalonia}/Services/` | 平台特定文件/链接打开实现(`Process.Start` / `xdg-open` |
### 7.2 平台差异:外部程序启动
| 平台 | 实现方式 |
|---|---|
| WindowsMAUI | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` |
| LinuxAvalonia | `System.Diagnostics.Process.Start("xdg-open", path)` |
通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。
### 7.3 附件文件管理
- 上传的本地文件复制到 `<AppData>/Hua.Todo/Attachments/{attachmentId}_{originalFileName}`
- 删除附件时同步删除磁盘文件
- 删除待办项时级联删除其所有附件(实体 + 文件)
- 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示)
### 7.4 安全约束
| 约束 | 说明 |
|---|---|
| 文件类型白名单 | 不限制(本地工具场景,用户自行管理) |
| 文件大小上限 | 50MB(可配置) |
| 路径遍历防护 | 文件名去除 `../``..\` 等危险路径片段 |
| 外部链接验证 | 仅允许 `http://``https://` 协议 |
---
## 八、子工单拆分
本工单体积适中,不拆分子工单,单文件完整实现。
如需与工单 03 并行推进,注意以下共享文件:
| 共享文件 | Writer | 说明 |
|---|---|---|
| `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration04 后续追加 Description/Attachments 导航属性 |
| `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 |
| `task.ts` | **工单 03** 先写入 | 03 新增 taskType04 后追加 description、attachments |
| `TaskEditDialog.vue` | 协调 | 03 新增 MeetingType 条件渲染;04 追加描述 + 附件区域 |
> 执行顺序:优先完成 03-01(数据模型),再开始 04 的后端模型部分。前端组件部分可独立并行。
---
## 九、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 描述编辑 | 编辑待办项,在描述 textarea 输入多行文字,保存 | 再次打开编辑框,描述内容完整保留 |
| 描述显示 | 查看待办项列表/详情 | 列表卡片显示描述预览(单行截断),详情显示完整描述 |
| 附件上传 | 编辑待办项,点击上传文件,选择一个本地文件 | 文件上传成功,附件列表显示文件名和大小 |
| 附件下载 | 在附件列表点击下载按钮 | 文件以原始文件名下载到本地 |
| 附件删除 | 点击附件删除按钮 | 附件从列表移除,磁盘文件同步清理 |
| 外部链接添加 | 点击添加链接,输入 URL | 链接保存为附件,类型标记为"外部链接" |
| 打开本地文件 | 点击本地附件"打开" | 系统关联程序打开该文件(如 PDF → PDF 阅读器) |
| 打开外部链接 | 点击外部链接附件"打开" | 系统默认浏览器打开该 URL |
| 打开可执行文件 | 附件为 `.exe`Windows)或可执行脚本(Linux) | 系统正常运行该程序 |
| 附件数量限制 | 超过 20 个附件后继续上传 | 提示"每个待办项最多 20 个附件" |
| 文件大小限制 | 上传超过 50MB 的文件 | 提示"文件大小不能超过 50MB" |
| 待办项删除级联 | 删除有附件的待办项 | 附件记录和磁盘文件同步清理 |
| 空描述 | 创建待办项时不填描述 | 正常创建,列表中不显示描述行 |
| 跨平台安全 | 桌面端设置描述+附件 → 移动端只改标题并保存 → 桌面端查看 | 描述和附件完整保留,未被覆盖(详见 1.4 节) |
---
## 十、风险与回滚
| 风险 | 影响 | 应对策略 |
|---|---|---|
| `Process.Start` 在不同 Linux 发行版行为差异 | 部分 Linux 文件打开失败 | 用 `xdg-open` 兜底;提供错误提示"无法打开文件:{原因}" |
| 附件文件被用户手动删除 | 数据库有记录但文件不存在 | 启动时校验,缺失文件标记为"失效",UI 用灰色 + 感叹号提示 |
| 大文件上传耗尽磁盘空间 | 应用数据目录磁盘满 | 单文件 50MB + 每待办项 20 个上限 = 最多 1GB;检查和提示剩余空间 |
| 并发编辑附件冲突 | 多窗口同时删除/添加附件 | 附件操作为即时保存(非批量提交),利用数据库事务隔离 |
---
## 十一、Touch List
| 文件路径 | 修改类型 | 是否共享 | 说明 |
|---|---|---|---|
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是(与 03 共享) | 新增 Description 字段 + Attachments 导航属性 |
| `src/Hua.Todo.Core/Entities/AttachmentEntity.cs` | 新增 | 否 | 附件实体 |
| `src/Hua.Todo.Core/Entities/AttachmentType.cs` | 新增 | 否 | 附件类型枚举 |
| `src/Hua.Todo.Core/Repositories/IAttachmentRepository.cs` | 新增 | 否 | 附件仓储接口 |
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是(与 03 共享) | 新增 Attachments DbSet + 关系映射 |
| `src/Hua.Todo.Application/Services/IAttachmentService.cs` | 新增 | 否 | 附件服务接口 |
| `src/Hua.Todo.Application/Services/AttachmentService.cs` | 新增 | 否 | 附件业务逻辑 |
| `src/Hua.Todo.Application/Controllers/AttachmentController.cs` | 新增 | 否 | 附件 API 端点 |
| `src/Hua.Todo.Application/Models/AttachmentModels.cs` | 新增 | 否 | 附件 DTO |
| `src/Hua.Todo.Core/Services/IPlatformAttachmentOpener.cs` | 新增 | 否 | 平台文件打开接口 |
| `src/Hua.Todo.Maui/Services/Platforms/MauiAttachmentOpener.cs` | 新增 | 否 | Windows MAUI 文件打开实现 |
| `src/Hua.Todo.Avalonia/Services/Platforms/AvaloniaAttachmentOpener.cs` | 新增 | 否 | Linux Avalonia 文件打开实现 |
| `src/Hua.Todo.Web/src/api/attachments.ts` | 新增 | 否 | 前端附件 API 模块 |
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是(与 03 共享) | 新增 description、AttachmentItem 等类型 |
| `src/Hua.Todo.Web/src/components/TaskEditDialog.vue` | 修改 | 是(与 03 共享) | 新增描述 textarea + 附件管理区域 |
| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改 | 否 | 描述预览 + 附件角标 |
| `src/Hua.Todo.Web/src/components/AttachmentList.vue` | 新增 | 否 | 附件列表组件 |
| `src/Hua.Todo.Web/src/composables/useAttachments.ts` | 新增 | 否 | 附件管理组合式函数 |
---
## 十二、与工单 03 的交互
| 维度 | 工单 03 | 工单 04 |
|---|---|---|
| 描述文本 | `MeetingNotes`(会议纪要,仅会议类型) | `Description`(通用多行描述,所有类型) |
| 附件 | 不涉及 | 通用附件管理 |
| 外部链接 | 不涉及 | 通用外部链接 |
> `MeetingNotes` 与 `Description` 是两个独立字段:
> - `MeetingNotes`:会议转写/文字纪要,可能很长(数千字),仅在会议类型下有值
> - `Description`:通用简短备注(上限 5000 字符),所有类型的待办项都可以有
>
> 两者可共存:会议类型的待办项可以同时有 `MeetingNotes`AI 拆分的输入)和 `Description`(用户的简短备注)。
---
**工单编号**04
**标题**:富文本描述、附件与外部链接
**版本**v1.3.0
**创建日期**2026-06-16