# 用户诊断包设计 本文定义影策“导出诊断包”功能的产品流程、前端采集、后端聚合、链路关联、安全脱敏和实施边界。第一阶段 MVP 已实现安全下载脱敏诊断包;提示词、截图、自动上传和异步大包等增强能力仍按后续阶段规划。 ## 目标 当普通用户遇到页面报错、任务失败、生成卡住或插件不可用时,不要求用户打开开发者工具或理解技术日志,只需一键导出诊断包并提交给支持人员。开发人员应能从诊断包还原以下链路: ```text 用户操作 -> 前端事件或异常 -> HTTP 请求 -> 后端业务任务 -> 模型上游请求 -> 响应或失败结果 ``` 第一阶段需要满足: - 用户在任务失败后可以用不超过三步导出诊断包。 - 诊断包同时包含前端上下文和当前用户有权访问的后端记录。 - 开发人员可以通过关联 ID 查询一次操作涉及的请求、任务和上游调用。 - 所有内容默认脱敏;当前 MVP 不提供加入提示词、截图或其他敏感数据的选项。 - 单个诊断包有明确的时间、条数和体积上限,不能影响正常任务执行。 ## 非目标 第一阶段不实现: - 自动把诊断包上传到客服、工单或第三方错误平台。 - 全量采集浏览器控制台输出、键盘输入或用户操作录像。 - 导出服务器数据库、环境变量、进程日志或宿主机文件。 - 在管理后台直接渲染用户上传的任意 HTML 或其他不可信文件。 - 用诊断包代替服务器监控、告警和生产可观测性系统。 ## 用户流程 ### 通用入口 在“设置 -> 问题诊断”中提供“导出诊断包”。默认流程如下: 1. 自动选择最近 30 分钟。 2. 用户填写一句问题描述,例如“生成视频一直停在处理中”。 3. 用户点击“导出”,浏览器下载 ZIP 文件。 4. 页面展示诊断编号,用户将 ZIP 和编号一并提交给支持人员。 文件名示例: ```text 影策诊断包-20260825-143012-DIAG_8F3K2P.zip ``` ### 失败任务快捷入口 当前实现先在任务中心失败详情提供“导出诊断包”快捷入口,并自动填入 `taskId` 和 `projectId`。任务创建前后自动扩展时间范围、通知入口和其他失败提示入口属于后续增强。 ### 当前选项 当前 MVP 只提供必要控制: - 时间范围:最近 15 分钟、30 分钟、1 小时或 24 小时;当前不提供自定义时间范围。 - 问题描述:最多 1000 个字符。 - 数据预览:预计包含的前端事件、任务记录和上游调用数量。 提示词、截图和可选项目元数据的开关属于后续阶段,当前不会写入诊断包。 界面应使用“问题描述”“诊断包”“问题编号”等普通用户能理解的词,不把 `traceId`、JSONL 或调用链作为主界面概念。 ## 总体架构 第一阶段由浏览器提交短期前端事件,后端按当前登录用户聚合数据库记录,统一脱敏后同步生成 ZIP: ```text 前端诊断缓冲区 + 当前页面、项目、任务上下文 + 用户问题描述 | v POST /api/diagnostics/export | v 后端权限校验 -> 数据查询 -> 统一脱敏 -> 限制与截断 -> ZIP 二进制 | v 浏览器下载诊断包 ``` 后端是诊断包内容和脱敏策略的最终边界。即使前端已经脱敏,后端仍需再次执行白名单过滤和敏感值清理。 ## 关联 ID 诊断能力依赖统一的链路标识。建议区分以下四类 ID: | 字段 | 生命周期 | 用途 | | --- | --- | --- | | `traceId` | 一次用户业务操作 | 串联一次生成、重试或导入涉及的多个 HTTP 请求。 | | `requestId` | 单个 HTTP 请求 | 定位前端请求与 Gin 请求处理。 | | `taskId` | 一个异步业务任务 | 关联任务状态、任务日志、计费和结果。 | | `providerRequestId` | 一次上游模型调用 | 查询渠道供应商侧任务或请求。 | 典型链路为: ```text clientEvent.traceId -> request.requestId -> task.taskId -> apiCallLog.providerRequestId ``` 后端增加请求中间件,为每个请求生成不可预测的 `requestId`,写入 Gin Context,并通过 `X-Request-ID` 响应头返回。客户端提供的 ID 只能在通过长度、字符集和格式校验后作为 `traceId` 使用,不能直接覆盖后端生成的 `requestId`。 一次生成操作由前端生成 `traceId`,通过 `X-Canvas-Trace-ID` 请求头传递。轮询、取消、重试等同一业务操作继续使用原 `traceId`;用户主动发起的新操作生成新值。 ## 前端设计 ### 模块边界 当前 MVP 实现: ```text web/src/services/diagnostics/ client-diagnostics.ts diagnostics-api.ts ``` - `client-diagnostics.ts`:环形缓冲区、全局错误/请求事件采集、上下文关联和浏览器侧脱敏。 - `diagnostics-api.ts`:预览、ZIP 导出请求和浏览器下载处理。 事件和导出参数类型暂时与对应模块 colocate;后续协议稳定、类型复用扩大后再拆分独立文件。 页面入口可以放在设置页面的私有组件目录;任务失败快捷入口复用同一个导出弹窗,不维护第二套导出逻辑。 ### 客户端事件结构 ```ts type ClientDiagnosticEvent = { id: string; timestamp: string; level: "info" | "warning" | "error"; category: "navigation" | "action" | "request" | "runtime" | "task" | "plugin"; code?: string; message: string; route?: string; durationMs?: number; httpStatus?: number; requestId?: string; traceId?: string; taskId?: string; projectId?: string; canvasId?: string; stack?: string; }; ``` 事件只记录诊断所需的结果和关联字段,不保存组件完整状态、表单值或请求正文。 ### 当前采集范围 当前 MVP 已采集: - `window.error`。 - `unhandledrejection`。 - 公共 `apiClient` 的 API 成功、业务失败和 HTTP 失败摘要,包括方法、规范化路径、状态、业务错误码、耗时和 `X-Request-ID`。 - 创建任务事件。 - 应用版本、构建提交、浏览器、操作系统和时区。 第一阶段不采集: - 用户键盘、鼠标坐标和连续操作录像。 - 全量 `console.log`、`console.info` 或第三方库调试输出。 - Cookie、请求头、请求正文和响应正文。 - Blob、data URL、图片、视频和完整画布快照。 ### 缓冲与持久化 使用固定容量环形缓冲区,当前最多保存 500 条事件,默认只保存在内存中,不写入 `localforage` 或其他持久化存储。用户切换或退出登录时清空当前缓冲区,避免账号之间串数据。 内存缓冲区需要: - 按登录用户隔离,用户 scope 变化时清理。 - 单条堆栈和消息分别限制长度。 - 无法序列化的对象转换为固定错误描述,不递归保存原始对象。 ### API 请求采集 在公共 `apiClient` 的请求和响应拦截器中完成统一采集: - 请求开始时保存方法、规范化路径、开始时间和 `traceId`,并发送 `X-Canvas-Trace-ID`。 - 响应结束时记录状态、耗时、业务错误码和 `X-Request-ID`。 - URL 删除查询字符串和片段,动态资源 ID可以保留,令牌和签名参数不得保留。 - 二进制、SSE 和本地 Agent 请求不复制完整载荷。 ### 生产堆栈 如果生产构建不提供可解析的源码映射,前端堆栈只能定位到压缩后的文件。源码映射应作为受控的构建产物保存给开发团队,不公开部署到无需鉴权即可下载的位置。诊断包记录应用版本、构建 Commit 和资源文件名,开发人员据此使用对应源码映射还原堆栈。 ## 后端设计 ### 分层 当前 MVP 实现: ```text backend/internal/handler/diagnostics.go backend/internal/service/diagnostics.go backend/internal/repository/diagnostics.go ``` - Handler:绑定请求、获取当前用户、设置 ZIP 响应头并输出二进制内容。 - Service:校验时间和上下文归属、聚合数据、脱敏、截断并生成清单。 - Repository:只提供带 `userID` 的诊断查询,不向 Handler 暴露通用全表查询。 第一阶段同步生成 ZIP,不新增诊断包数据库表,也不在服务器长期保存 ZIP。当前实现先在内存中生成并限制压缩包不超过 10 MB;数据量增大后再升级为异步任务或流式生成。 ### 数据来源 优先复用现有业务数据: - `Task`:任务状态、时间、错误码和所属项目。 - `TaskLog`:任务执行步骤和结构化失败信息。 - `ApiCallLog`:渠道、模型、耗时、状态、错误码和 `providerRequestId`。 - 前端运行时信息(版本、构建提交、浏览器、操作系统和时区)。 - 前端请求体提交的短期客户端事件。 Gin 当前标准访问日志输出到进程标准输出,无法可靠地按用户归属查询,因此不作为用户诊断包的数据源。服务器进程日志应属于独立的管理员诊断能力。 ### 模型调整 为形成完整链路,当前已增加: ```go type Task struct { RequestID string TraceID string // existing fields... } type ApiCallLog struct { RequestID string TraceID string // existing fields... } type TaskLog struct { RequestID string TraceID string // existing fields... } ``` `Task`、`TaskLog` 和 `ApiCallLog` 都保存关联字段,并为 `request_id`、`trace_id` 增加查询索引;诊断查询继续带当前用户和时间范围约束。 字段和索引说明同步维护在后端数据库专题文档中。 ## API 设计 ### 预览 ```http POST /api/diagnostics/preview Content-Type: application/json ``` 请求示例: ```json { "from": "2026-08-25T06:00:00Z", "to": "2026-08-25T06:30:00Z", "taskId": "task_xxx", "projectId": "project_xxx" } ``` 响应使用统一业务信封: ```json { "code": 0, "data": { "clientEventLimit": 500, "taskCount": 1, "taskLogCount": 18, "apiCallCount": 4, "estimatedBytes": 86000, "willTruncate": false }, "msg": "ok" } ``` 预览数量只是当前估算,导出期间产生的新事件可以不加入本次诊断包。 ### 导出 ```http POST /api/diagnostics/export Content-Type: application/json Accept: application/zip ``` 请求示例: ```json { "from": "2026-08-25T06:00:00Z", "to": "2026-08-25T06:30:00Z", "taskId": "task_xxx", "projectId": "project_xxx", "description": "生成视频一直停在处理中", "runtime": { "appVersion": "0.1.0", "buildCommit": "local" }, "clientEvents": [] } ``` 成功响应: ```http HTTP/1.1 200 OK Content-Type: application/zip Content-Disposition: attachment; filename="yingce-diagnostics-20260825-143012-DIAG_8F3K2P.zip" X-Diagnostic-Bundle-ID: DIAG_8F3K2P X-Diagnostic-Schema-Version: 1 ``` 由于成功响应是二进制,不经过 JSON 信封解包;前端用 `http.raw`(同一 `apiClient` 的认证和 Base URL),设置 `responseType: "blob"`。错误响应继续使用统一 `{ code, data, msg }` 语义,前端按响应类型解析。 ### 权限规则 - 必须登录才能预览或导出。 - `taskId`、`projectId` 都由 Service 校验归属。 - Repository 查询始终同时带当前 `userID`,不能先按 ID 查询后再由前端隐藏。 - 不能因为用户知道其他任务 ID 而导出其他用户的日志。 - 管理员也不通过用户接口绕过归属;管理员诊断使用独立接口和审计规则。 ## 诊断包格式 第一版 ZIP 结构: ```text manifest.json README.txt client/ events.jsonl backend/ tasks.jsonl task-logs.jsonl upstream-calls.jsonl context/ runtime.json ``` 插件上下文、功能开关、截图和其他可选媒体尚未进入当前 MVP,后续增加时必须单独设计隐私开关和脱敏规则。 JSONL 文件一行一个对象,便于流式生成、截断和命令行处理。所有文件使用 UTF-8。 ### Manifest ```json { "schemaVersion": 1, "bundleId": "DIAG_8F3K2P", "generatedAt": "2026-08-25T06:30:12Z", "appVersion": "0.0.0", "buildCommit": "abcdef1", "timeRange": { "from": "2026-08-25T06:00:00Z", "to": "2026-08-25T06:30:00Z" }, "redactionVersion": 1, "truncated": false, "counts": { "clientEvents": 42, "tasks": 1, "taskLogs": 18, "upstreamCalls": 4 } } ``` Manifest 不保存用户邮箱、用户名和真实 API Key。需要在包内区分同一用户时,使用只在当前诊断包内稳定的随机别名或哈希,不创建跨诊断包追踪标识。 ## 脱敏规则 ### 默认禁止内容 无论用户如何选择,以下内容都不能进入诊断包: - `Authorization`、Cookie、Session、API Key、Access Key、Secret、Password、Token。 - `x-goog-api-key`、`X-Canvas-Upstream-Headers` 等上游认证头。 - 数据库、环境变量、`.settings-key` 和服务器配置原文。 - 本机绝对文件路径、内部数据库路径和对象存储签名参数。 - 完整浏览器存储、完整请求头和完整响应头。 - 原始媒体文件和 data URL。 ### 默认关闭内容 以下内容只有用户明确选择后才能加入,并在界面说明隐私影响: - 提示词原文和消息正文。 - 项目、画布、素材的用户自定义名称。 - 页面截图。 - 上游请求或响应中的业务正文。 即使用户选择包含,仍需执行密钥清理、字段白名单、字符串截断和 URL 去签名。 ### 脱敏实现 后端维护单一脱敏服务,按以下顺序处理: 1. 只构造允许导出的字段,不直接序列化完整数据库模型。 2. 递归删除敏感键名。 3. 对字符串执行 Token、Bearer、密钥和签名 URL 模式清理。 4. 对路径、错误堆栈和上游错误信息执行长度限制。 5. 将被清理的内容替换为 `[REDACTED]`,不留原值摘要。 不要只依赖正则表达式处理完整对象;字段白名单是主要边界,模式清理只作为第二层防护。 ## 限制与降级 建议第一阶段采用以下上限: | 项目 | 上限 | | --- | --- | | 时间范围 | 24 小时 | | 前端事件 | 500 条 | | 任务 | 100 条 | | 任务日志 | 2,000 条 | | 上游调用 | 1,000 条 | | 问题描述 | 1,000 字符 | | 单条消息或堆栈 | 4,000 字符 | | ZIP 总大小 | 10 MB | | 用户截图 | 当前不支持 | 达到条数限制时优先保留错误事件、选中任务和距离错误最近的记录,并在 `manifest.json` 中写入 `truncated: true` 和各数据源的截断数量。 达到 ZIP 体积限制时,依次移除:普通信息事件、成功请求、冗长堆栈和可选截图。不能为了完成导出而放宽敏感字段规则。 导出失败不能影响原任务和用户数据。前端展示可操作错误,例如“时间范围过大,请缩短到 1 小时”,而不是笼统显示“系统错误”。 ## 开发人员排障流程 拿到诊断包后建议按以下顺序检查: 1. 阅读 `README.txt` 和 `manifest.json`,确认版本、时间范围、是否截断。 2. 从用户描述或失败任务找到 `traceId`、`requestId` 和 `taskId`。 3. 在 `client/events.jsonl` 中还原用户操作和前端异常。 4. 在 `backend/tasks.jsonl` 和 `task-logs.jsonl` 中确认业务状态变化。 5. 在 `backend/upstream-calls.jsonl` 中检查渠道、模型、状态码、耗时和 `providerRequestId`。 6. 使用构建版本对应的源码映射还原前端堆栈。 第二阶段可以提供本地只读诊断查看器,将 JSONL 转换为时间线。查看器不得执行诊断包中的脚本或渲染不可信 HTML。 ## 测试要求 ### 前端 - 环形缓冲区按时间和容量正确淘汰。 - 用户切换后不会读取其他账号的缓存。 - Axios 成功、业务失败、HTTP 失败和取消请求均生成正确摘要。 - URL 查询参数、Token 和请求正文不会进入事件。 - 任务失败快捷入口正确带入 `taskId` 和时间范围。 - 二进制下载失败时可以解析后端 JSON 错误。 ### 后端 - 普通用户无法通过猜测 ID 导出其他用户的数据。 - 时间范围、条数、消息长度和 ZIP 体积限制生效。 - `RequestBody`、`ResponseBody`、`UpstreamURL` 中的密钥和签名参数不会泄漏。 - ZIP 文件名、响应头、Manifest 和 JSONL 格式正确。 - 达到上限时保留失败任务并正确标记截断。 - 客户端提交恶意字段、超深 JSON、超长字符串和伪造关联 ID 时安全拒绝或截断。 - 生成失败不会留下长期临时 ZIP 文件。 ### 验收场景 至少验证: 1. 前端渲染异常。 2. 后端返回业务错误码。 3. 模型上游返回 429 或 5xx。 4. 异步任务长时间处理中。 5. 插件连接失败。 6. 用户主动取消请求。 7. 诊断数据超过限制。 8. 包含疑似 API Key、Cookie 和签名 URL 的错误文本。 ## 实施阶段 ### 第一阶段:可用的安全诊断包 - 增加 `traceId`、`requestId` 和响应头。 - 建立前端错误与请求环形缓冲区。 - 增加设置入口和任务失败快捷入口。 - 后端关联 `Task`、`TaskLog` 和 `ApiCallLog`。 - 同步生成脱敏 ZIP,不落盘。 - 补齐脱敏、权限、限制和下载测试。 ### 第二阶段:提升提交与分析效率 - 异步生成诊断包并设置 24 小时自动过期。 - 支持用户主动附加截图。 - 提供离线诊断时间线查看器。 - 管理员日志页面支持按 `traceId`、`requestId` 和诊断编号筛选。 ### 第三阶段:生产可观测性联动 - 将结构化关联字段接入 OpenTelemetry 或内部监控。 - 对相同错误码和堆栈进行聚类。 - 在用户明确同意和部署方配置完成后接入工单提交。 生产可观测性与用户诊断包共享关联 ID,但权限、保留周期和数据范围保持独立。