# 研发工单 v1.3.0 - 04 富文本描述、附件与外部链接 > 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。 --- ## 一、目标与范围 ### 1.1 目标 当前 Todo 待办项仅包含单行标题(`Title`),无法满足"一个待办项关联更多上下文信息"的需求。本工单为 Todo 待办项新增三项能力: | 能力 | 描述 | |---|---| | **多行描述(Description)** | 每个待办项可附带一段多行文字描述,用于记录详细说明、步骤、备注等 | | **文件附件(Attachments)** | 可为一个待办项关联多个本地文件,支持上传、查看、下载、删除 | | **外部链接与程序启动** | 附件可以是外部 URL(点击在系统默认浏览器打开)或本地文件路径/可执行文件(点击通过系统关联程序打开) | ### 1.2 平台范围 | 平台 | 是否支持 | 说明 | |---|---|---| | Windows(MAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 | | Linux(Avalonia) | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 | | macOS(MAUI) | 待定 | 视为 Windows 同级,但本期不单独投入 | | iOS / Android | ❌ 不支持 | 移动端不提供附件/描述编辑入口,但**移动端编辑待办项时不得覆盖/清空已有描述和附件数据**(详见 1.4 节) | ### 1.3 范围 **包含**: - `TaskEntity` 新增 `Description` 字段(多行文本) - 新增 `AttachmentEntity` 数据模型(附件元数据) - 附件 CRUD API(上传、列表、下载、删除、打开) - 前端编辑对话框扩展(描述 textarea + 附件管理区域) - 前端待办项列表/详情展示(描述预览、附件数量角标) - 桌面端通过系统关联程序打开附件(URL → 浏览器,文件路径 → 关联应用) **不包含**: - 附件云同步(本期不涉及,后续版本规划) - 附件预览(如图片缩略图、PDF 内嵌预览,本期不做) - 移动端附件管理(本期仅桌面端) - 富文本编辑器(如 Markdown 渲染,本期仅纯文本多行) ### 1.4 跨平台数据安全:移动端编辑不得破坏已有数据(强制) **背景**:移动端(iOS/Android)不提供描述/附件的编辑 UI,但用户仍可在移动端修改待办项标题、优先级、完成状态等基础字段。如果没有防护,移动端发起更新请求时会用"空值"覆盖掉桌面端已设置的 `Description` 和 `Attachments`,导致数据丢失。 **约束**: | 约束项 | 要求 | |---|---| | **后端 API 必须支持真正的部分更新** | `PUT /api/task/{id}` 的请求体中,**未传递的字段保持原值不变**,不得将缺失字段视为"置空"。即:`description` 不在 JSON 中 → 不修改数据库中的 `Description`;`description` 为 `null` → 清空 | | **UpdateTaskDto 所有扩展字段均为 optional** | `description`、`attachments` 相关字段在 DTO 中均标记为可选(`string?` / `null`),与必填字段(`id`)区分 | | **移动端前端不传递未知字段** | 移动端构建 `UpdateTaskDto` 时只传 `id`、`title`、`priority`、`isCompleted`,不传 `description`、`attachments`。后端对这些字段视为"不修改" | | **Attachment 实体独立于 Task 更新** | 附件 CRUD 走独立端点(`/api/task/{id}/attachments`),不通过 `PUT /api/task/{id}` 携带。移动端不调附件端点,自然不会破坏附件数据 | **验证方式**: 1. 桌面端创建待办项,添加描述 + 上传附件 2. 移动端编辑同一待办项(只改标题),保存 3. 回到桌面端查看 → 描述和附件完整保留,未被覆盖 --- ## 二、前置条件 | 条件 | 说明 | 状态 | |---|---|---| | Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 | | 工单 03 TaskType 枚举 | `TaskEntity` 已有字段扩展模式可参考 | 待实现(03 先于 04 或并行) | | 桌面 WebView 文件选择 | 前端 `` 能力 | 已就绪 | | 桌面程序启动 | 通过后端 `Process.Start` / `xdg-open` 实现 | 需新增 | --- ## 三、功能入口覆盖检查 > 依据 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md),新功能必须检查两个入口覆盖情况。 | 入口 | 已规划 | 方案 | |---|---|---| | UI | ✅ | 编辑对话框新增描述 textarea + 附件列表区域;TaskItem 卡片显示描述预览与附件数量角标 | | 语音 | ⚠️ 部分 | 语音可追加/编辑描述文本(通过"给「标题」添加描述"意图);附件上传与外部程序启动不适合语音入口 | > 语音入口覆盖说明: > - **支持**:通过语音添加/修改待办项描述("给「XXX」添加备注:...") > - **不支持**:语音上传附件、语音打开外部程序 —— 两者本质上是可视化交互,语音不是合适的入口。本期不覆盖。 > - TODO:工单 02 的 LLM intent parser prompt 需预留 `SET_DESCRIPTION` 意图,参数为 `targetTask` + `description` --- ## 四、数据模型设计 ### 4.1 TaskEntity 扩展 在现有 `TaskEntity` 上新增一个字段: ```csharp /// /// 多行描述文本(可为空)。 /// 用于记录待办项相关的详细说明、步骤或备注。 /// public string? Description { get; set; } ``` | 字段 | 类型 | 默认值 | 约束 | 说明 | |---|---|---|---|---| | `Description` | `string?` | null | 最大 5000 字符 | 多行描述文本 | ### 4.2 AttachmentEntity(新增) 在 `Hua.Todo.Core/Entities/` 下新增 `AttachmentEntity.cs`: ```csharp /// 附件类型枚举 public enum AttachmentType { /// 本地文件(已复制到应用数据目录) LocalFile = 0, /// 外部链接(URL) ExternalLink = 1 } /// 附件实体,表示待办项关联的文件或外部链接 public class AttachmentEntity { /// 附件唯一标识符 public int Id { get; set; } /// 所属待办项ID public int TaskId { get; set; } /// 显示名称(用户可见的文件名或链接标题) public string FileName { get; set; } = string.Empty; /// 存储路径(本地文件的相对路径)或外部URL public string FilePath { get; set; } = string.Empty; /// 文件大小(字节),外部链接为 0 public long FileSize { get; set; } /// MIME 类型(如 text/plain、application/pdf),外部链接为空字符串 public string ContentType { get; set; } = string.Empty; /// 附件类型 public AttachmentType AttachmentType { get; set; } = AttachmentType.LocalFile; /// 创建时间(UTC) public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // 导航属性 public TaskEntity Task { get; set; } = null!; } ``` | 字段 | 类型 | 默认值 | 说明 | |---|---|---|---| | `Id` | `int` | 自增 | 主键 | | `TaskId` | `int` | - | FK → Tasks.Id | | `FileName` | `string` | - | 显示名称,最大 256 字符 | | `FilePath` | `string` | - | 存储路径或外部 URL,最大 1024 字符 | | `FileSize` | `long` | 0 | 字节数 | | `ContentType` | `string` | "" | MIME 类型 | | `AttachmentType` | `AttachmentType` | `LocalFile` | 本地文件 vs 外部链接 | | `CreatedAt` | `DateTime` | `UtcNow` | 创建时间 | `TaskEntity` 新增导航属性: ```csharp public List Attachments { get; set; } = new(); ``` ### 4.3 数据库表 | 表名 | 说明 | |---|---| | `T_Attachments` | 附件表 | > 注:表名沿用 ABP 模板规范 `T_{实体名}s`(见 [.trae/rules/项目/03-数据模型与迁移约束.md](../../../.trae/rules/项目/03-数据模型与迁移约束.md))。 ### 4.4 附件存储策略 | 策略项 | 决定 | |---|---| | 存储位置 | 应用数据目录下 `Attachments/` 子目录(与数据库 `.db` 同级) | | 文件命名 | `{attachmentId}_{originalFileName}`,避免重名冲突 | | 文件大小上限 | 单文件 50MB(`appsettings.json` 可配置) | | 总附件数上限 | 每个待办项最多 20 个附件 | | 外部链接 | 不复制文件,仅存储 URL 字符串;打开时调用系统默认浏览器 | --- ## 五、API 设计 ### 5.1 已修改:创建/更新待办项(扩展 Description) ``` POST /api/task 请求体新增字段: { "title": "...", "priority": 1, "description": "多行描述文本(可选)" // ← 新增 } PUT /api/task/{id} 请求体新增字段(所有扩展字段均为可选): { "id": 42, "title": "...", "priority": 1, "description": "..." // ← 可选;不传=保持原值,传 null=清空 } ``` > **部分更新语义(强制)**:`PUT` 端点必须实现"仅更新已传递字段"的逻辑。 > - `description` 字段**不在 JSON 中** → 数据库 `Description` 保持原值 > - `description` 字段**为 `null`** → 数据库 `Description` 清空为 `null` > - 此设计确保移动端不传递 `description` 时不会意外清空已有描述 ### 5.2 附件上传 ``` POST /api/task/{taskId}/attachments Content-Type: multipart/form-data 字段: - file: 文件内容(必填,最大 50MB) 响应体: { "success": true, "data": { "id": 1, "taskId": 42, "fileName": "需求文档.pdf", "fileSize": 204800, "contentType": "application/pdf", "attachmentType": 0, "createdAt": "2026-06-16T10:00:00Z" } } ``` ### 5.3 添加外部链接 ``` POST /api/task/{taskId}/attachments/link 请求体: { "url": "https://example.com/doc", "fileName": "参考文档" // 可选,不传则使用 URL 作为显示名 } 响应体: { "success": true, "data": { "id": 2, "taskId": 42, "fileName": "参考文档", "filePath": "https://example.com/doc", "fileSize": 0, "contentType": "", "attachmentType": 1, "createdAt": "2026-06-16T10:00:00Z" } } ``` ### 5.4 获取附件列表 ``` GET /api/task/{taskId}/attachments 响应体: { "success": true, "data": [ { "id": 1, "fileName": "需求文档.pdf", "fileSize": 204800, "contentType": "application/pdf", "attachmentType": 0, "createdAt": "2026-06-16T10:00:00Z" }, { "id": 2, "fileName": "参考文档", "filePath": "https://example.com/doc", "attachmentType": 1, "createdAt": "2026-06-16T10:05:00Z" } ] } ``` ### 5.5 下载附件 ``` GET /api/attachments/{attachmentId}/download 响应:文件流(Content-Disposition: attachment; filename="需求文档.pdf") ``` ### 5.6 打开附件(桌面端) ``` POST /api/attachments/{attachmentId}/open 响应体: { "success": true, "data": { "opened": true } } // 后端行为: // - AttachmentType.LocalFile → Process.Start(filePath)(Windows)或 xdg-open(Linux) // - AttachmentType.ExternalLink → Process.Start(url)(在默认浏览器打开) ``` ### 5.7 删除附件 ``` DELETE /api/task/{taskId}/attachments/{attachmentId} 响应体: { "success": true, "message": "附件已删除" } ``` --- ## 六、前端设计 ### 6.1 类型扩展 `src/Hua.Todo.Web/src/types/task.ts` 新增 / 修改: ```typescript // Task 接口新增字段 export interface Task { // ... 既有字段 description?: string; // 多行描述 attachments?: AttachmentItem[]; // 附件列表 attachmentCount?: number; // 附件数量(列表视图用,不传完整列表) } export interface AttachmentItem { id: number; taskId: number; fileName: string; filePath: string; fileSize: number; contentType: string; attachmentType: AttachmentType; // 0=本地文件, 1=外部链接 createdAt: string; } export type AttachmentType = 0 | 1; ``` ### 6.2 组件变更 | 组件 | 变更类型 | 说明 | |---|---|---| | `TaskEditDialog.vue` | 修改 | 新增描述 textarea + 附件管理区域(上传按钮、附件列表) | | `TaskItem.vue` | 修改 | 卡片底部显示描述预览(单行截断)+ 附件数量角标 | | `AttachmentList.vue` | 新增 | 附件列表组件:文件名、大小、类型图标、删除按钮、打开按钮 | | `LinkInputDialog.vue` | 新增 | 外部链接输入弹出框(URL + 显示名称) | ### 6.3 新增 API 模块 `src/Hua.Todo.Web/src/api/attachments.ts`: ```typescript // uploadAttachment(taskId, file) — POST multipart/form-data // addLink(taskId, url, fileName?) — POST /api/task/{id}/attachments/link // getAttachments(taskId) — GET // deleteAttachment(taskId, attId) — DELETE // openAttachment(attId) — POST /api/attachments/{id}/open // downloadAttachment(attId) — GET /api/attachments/{id}/download ``` ### 6.4 交互流程 ``` 编辑待办项对话框 ├─ 标题输入框(现有) ├─ 优先级选择(现有) ├─ 描述 textarea(新增:多行文本,placeholder "添加备注、步骤说明...") ├─ 附件区域(新增) │ ├─ [+ 上传文件] 按钮 → 触发文件选择器 → 上传至后端 │ ├─ [+ 添加链接] 按钮 → 弹出 URL 输入框 → 保存为外部链接附件 │ └─ 附件列表(每项显示:图标 + 文件名 + 大小 + [打开] [删除]) │ ├─ 本地文件:[打开] → 调用后端 open API → 系统关联程序打开 │ ├─ 外部链接:[打开] → 调用后端 open API → 浏览器打开 │ └─ [删除] → 确认 → 删除附件 └─ [取消] [保存] ``` ### 6.5 列表卡片展示 ``` ┌─────────────────────────────────────┐ │ ☐ 整理产品需求文档 │ │ 高优先级 │ │ 需要在本周五前完成需求文档的整理... │ ← 描述预览(单行,超长截断) │ 📎 2 个附件 │ ← 附件数量角标 └─────────────────────────────────────┘ ``` --- ## 七、后端架构 ### 7.1 新增服务 | 服务/类 | 位置 | 说明 | |---|---|---| | `AttachmentEntity` | `Hua.Todo.Core/Entities/` | 附件实体 | | `AttachmentType` | `Hua.Todo.Core/Entities/` | 附件类型枚举 | | `IAttachmentRepository` | `Hua.Todo.Core/Repositories/` | 仓储接口 | | `AttachmentRepository` | `Hua.Todo.Application/Data/` | EF Core 仓储实现 | | `AttachmentService` | `Hua.Todo.Application/Services/` | 附件业务逻辑(上传、下载、打开、清理) | | `IAttachmentService` | `Hua.Todo.Application/Services/` | 服务接口 | | `AttachmentController` | `Hua.Todo.Application/DynamicApi/` 或独立 Controller | 附件动态 API 端点(或手动 Controller) | | `PlatformAttachmentOpener` | `Hua.Todo.{Maui,Avalonia}/Services/` | 平台特定文件/链接打开实现(`Process.Start` / `xdg-open`) | ### 7.2 平台差异:外部程序启动 | 平台 | 实现方式 | |---|---| | Windows(MAUI) | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` | | Linux(Avalonia) | `System.Diagnostics.Process.Start("xdg-open", path)` | 通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。 ### 7.3 附件文件管理 - 上传的本地文件复制到 `/Hua.Todo/Attachments/{attachmentId}_{originalFileName}` - 删除附件时同步删除磁盘文件 - 删除待办项时级联删除其所有附件(实体 + 文件) - 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示) ### 7.4 安全约束 | 约束 | 说明 | |---|---| | 文件类型白名单 | 不限制(本地工具场景,用户自行管理) | | 文件大小上限 | 50MB(可配置) | | 路径遍历防护 | 文件名去除 `../`、`..\` 等危险路径片段 | | 外部链接验证 | 仅允许 `http://`、`https://` 协议 | --- ## 八、子工单拆分 本工单体积适中,不拆分子工单,单文件完整实现。 如需与工单 03 并行推进,注意以下共享文件: | 共享文件 | Writer | 说明 | |---|---|---| | `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration;04 后续追加 Description/Attachments 导航属性 | | `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 | | `task.ts` | **工单 03** 先写入 | 03 新增 taskType;04 后追加 description、attachments | | `TaskEditDialog.vue` | 协调 | 03 新增 MeetingType 条件渲染;04 追加描述 + 附件区域 | > 执行顺序:优先完成 03-01(数据模型),再开始 04 的后端模型部分。前端组件部分可独立并行。 --- ## 九、验收标准 | 验收项 | 验证方法 | 预期结果 | |---|---|---| | 描述编辑 | 编辑待办项,在描述 textarea 输入多行文字,保存 | 再次打开编辑框,描述内容完整保留 | | 描述显示 | 查看待办项列表/详情 | 列表卡片显示描述预览(单行截断),详情显示完整描述 | | 附件上传 | 编辑待办项,点击上传文件,选择一个本地文件 | 文件上传成功,附件列表显示文件名和大小 | | 附件下载 | 在附件列表点击下载按钮 | 文件以原始文件名下载到本地 | | 附件删除 | 点击附件删除按钮 | 附件从列表移除,磁盘文件同步清理 | | 外部链接添加 | 点击添加链接,输入 URL | 链接保存为附件,类型标记为"外部链接" | | 打开本地文件 | 点击本地附件"打开" | 系统关联程序打开该文件(如 PDF → PDF 阅读器) | | 打开外部链接 | 点击外部链接附件"打开" | 系统默认浏览器打开该 URL | | 打开可执行文件 | 附件为 `.exe`(Windows)或可执行脚本(Linux) | 系统正常运行该程序 | | 附件数量限制 | 超过 20 个附件后继续上传 | 提示"每个待办项最多 20 个附件" | | 文件大小限制 | 上传超过 50MB 的文件 | 提示"文件大小不能超过 50MB" | | 待办项删除级联 | 删除有附件的待办项 | 附件记录和磁盘文件同步清理 | | 空描述 | 创建待办项时不填描述 | 正常创建,列表中不显示描述行 | | 跨平台安全 | 桌面端设置描述+附件 → 移动端只改标题并保存 → 桌面端查看 | 描述和附件完整保留,未被覆盖(详见 1.4 节) | --- ## 十、风险与回滚 | 风险 | 影响 | 应对策略 | |---|---|---| | `Process.Start` 在不同 Linux 发行版行为差异 | 部分 Linux 文件打开失败 | 用 `xdg-open` 兜底;提供错误提示"无法打开文件:{原因}" | | 附件文件被用户手动删除 | 数据库有记录但文件不存在 | 启动时校验,缺失文件标记为"失效",UI 用灰色 + 感叹号提示 | | 大文件上传耗尽磁盘空间 | 应用数据目录磁盘满 | 单文件 50MB + 每待办项 20 个上限 = 最多 1GB;检查和提示剩余空间 | | 并发编辑附件冲突 | 多窗口同时删除/添加附件 | 附件操作为即时保存(非批量提交),利用数据库事务隔离 | --- ## 十一、Touch List | 文件路径 | 修改类型 | 是否共享 | 说明 | |---|---|---|---| | `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是(与 03 共享) | 新增 Description 字段 + Attachments 导航属性 | | `src/Hua.Todo.Core/Entities/AttachmentEntity.cs` | 新增 | 否 | 附件实体 | | `src/Hua.Todo.Core/Entities/AttachmentType.cs` | 新增 | 否 | 附件类型枚举 | | `src/Hua.Todo.Core/Repositories/IAttachmentRepository.cs` | 新增 | 否 | 附件仓储接口 | | `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是(与 03 共享) | 新增 Attachments DbSet + 关系映射 | | `src/Hua.Todo.Application/Services/IAttachmentService.cs` | 新增 | 否 | 附件服务接口 | | `src/Hua.Todo.Application/Services/AttachmentService.cs` | 新增 | 否 | 附件业务逻辑 | | `src/Hua.Todo.Application/Controllers/AttachmentController.cs` | 新增 | 否 | 附件 API 端点 | | `src/Hua.Todo.Application/Models/AttachmentModels.cs` | 新增 | 否 | 附件 DTO | | `src/Hua.Todo.Core/Services/IPlatformAttachmentOpener.cs` | 新增 | 否 | 平台文件打开接口 | | `src/Hua.Todo.Maui/Services/Platforms/MauiAttachmentOpener.cs` | 新增 | 否 | Windows MAUI 文件打开实现 | | `src/Hua.Todo.Avalonia/Services/Platforms/AvaloniaAttachmentOpener.cs` | 新增 | 否 | Linux Avalonia 文件打开实现 | | `src/Hua.Todo.Web/src/api/attachments.ts` | 新增 | 否 | 前端附件 API 模块 | | `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是(与 03 共享) | 新增 description、AttachmentItem 等类型 | | `src/Hua.Todo.Web/src/components/TaskEditDialog.vue` | 修改 | 是(与 03 共享) | 新增描述 textarea + 附件管理区域 | | `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改 | 否 | 描述预览 + 附件角标 | | `src/Hua.Todo.Web/src/components/AttachmentList.vue` | 新增 | 否 | 附件列表组件 | | `src/Hua.Todo.Web/src/composables/useAttachments.ts` | 新增 | 否 | 附件管理组合式函数 | --- ## 十二、与工单 03 的交互 | 维度 | 工单 03 | 工单 04 | |---|---|---| | 描述文本 | `MeetingNotes`(会议纪要,仅会议类型) | `Description`(通用多行描述,所有类型) | | 附件 | 不涉及 | 通用附件管理 | | 外部链接 | 不涉及 | 通用外部链接 | > `MeetingNotes` 与 `Description` 是两个独立字段: > - `MeetingNotes`:会议转写/文字纪要,可能很长(数千字),仅在会议类型下有值 > - `Description`:通用简短备注(上限 5000 字符),所有类型的待办项都可以有 > > 两者可共存:会议类型的待办项可以同时有 `MeetingNotes`(AI 拆分的输入)和 `Description`(用户的简短备注)。 --- **工单编号**:04 **标题**:富文本描述、附件与外部链接 **版本**:v1.3.0 **创建日期**:2026-06-16