Skip to Content
管理员后台诊断问题

诊断问题 (Dramagon Admin · 诊断问题)

2026-09-10 UIUX 对齐(WT#1):主页 KPI 扩展为 5 卡(今日新增 / 未解决 / Agent 自动修复率 / 错误率 / 最常见路由),未解决卡琥珀边框 + 错误率红边框;表格列从 8 扩展为 10(新增全选 checkbox + 路由列);新增 selected / critical / slow 三态视觉;行 ⋯ 按钮 router.push 跳 /admin/diagnostic/[id] 全屏页(Drawer 入口在主页保留,WT#4 收口);副标题未解决数字用黄色高亮。Glass 视觉由 admin.css §17 段 38 个 .admin-diagnostic-* class 兜底,严格复用 --dramagon-* / --glass-* 令牌,不引入新 CSS 变量。详细规范见 .learnings/PLANS/diag-uiux-align-2026-09-10.md

目的

诊断模块不是被动的 admin 日志查看器,而是 Dramagon Studio 的全局可观测性中枢,整合:

  1. 日常开发维护 — 每个错误可查、可重放、可导出 ZIP 给支持 / GitHub issue。
  2. 面向切面(AOP)withDiagnosticContext 装饰器 + apiHandler 切点 + ErrorBoundary 边界统一收口,不污染业务代码。
  3. 深度整合日志框架 — 与 createScopedLogger + AsyncLocalStorage log context 双向绑定(共享 traceId);同步 DiagnosticIssue 行写入 + 异步 publish 到 diagnostic-events BullMQ Queue。
  4. API 监控深整合 — 通过 fingerprintapi-timing-samples 互查;admin 在 api-timing-stats 看到 P95 异常 → 跳到对应 diagnostic event。
  5. 全局异常捕获 + 错误码元数据升级UnifiedErrorCode 从 1 维枚举升级为 4 维 metadata(severity × category × retryable × agent_actionable)。
  6. 消息队列预警 + Agent 自愈diagnostic-events Queue 三类消费者:① 通知 worker(email / Slack / webhook);② admin UI 实时面板;③ Agent Diagnostic Worker(自动修复 agent_actionable=true 错误,否则产出修复 plan)。
  7. 观测自指 + RBAC + 审计 + 保留期 + 隐私脱敏 — 由 INV-DIAG-01 至 INV-DIAG-15 覆盖。

开始之前

  • 仅管理员账号可见;登录会话与 Supabase token 由 requireDiagnosticAdminAuth 守卫。
  • ErrorBoundary 自动 emit 通过 POST /api/internal/diagnostic/emit(由 INTERNAL_TASK_TOKEN 保护;绝不在浏览器使用 admin JWT)。
  • Agent service-account 使用 x-internal-task-token + x-internal-agent-id 请求头(详见 requireDiagnosticAgentAuth);admin 不能直接调用 agent-resolve
  • 所有 admin 操作(resolve / delete / export / bulk-delete / retention-cleanup)写一条 audit_log 行,action: diagnostic.*
  • DiagnosticIssue 表通过 resolved 字段软删除(true / false);物理删除仅 C2/C3(单条 + 批量)。

步骤

1. 进入诊断仪表板

访问 /admin/diagnostic。页面在 sidebar groups.ops 下(API 耗时)。admin shell 使用与其他 admin 页面相同的玻璃态布局。

2. 查看 KPI strip

仪表板在表格上方展示 5 张卡(玻璃化,admin.css §17 段),对齐 .design/dramagon-redesign/pages/admin-diagnostic.html 原型:

#KPI数据源视觉
1今日新增count(DiagnosticIssue where createdAt >= today 0:00)默认玻璃描边
2未解决 incidentscount(DiagnosticIssue where resolved=false)琥珀边框 is-warn(admin.css:5282-5285);副标题”其中 N 为 error 级”
3Agent 自动修复率succeeded / (succeeded + partial),最近 200 条 agent runs默认玻璃描边
4错误率count(status>=400) / count(*) over ApiTimingSample 24h 窗口红边框 is-error(admin.css:5287-5290);本轮为占位 ’—‘,依赖 WT#2 后端 /api/admin/diagnostic/stats endpoint
5最常见路由GROUP BY apiTimingIds.route top-1,失败次数默认玻璃描边;本轮为占位 ’—‘,依赖 WT#2 后端 stats endpoint

点击 KPI 卡可跳转到对应过滤后的列表。

副标题格式:最近 7 天 · 共 247 条 · 18 未解决(琥珀高亮) · 实时刷新中,未解决数字用 <strong class="admin-diagnostic-unresolved-highlight"> 包裹。实时刷新中 文案保留但当前无 setInterval/SSE(本轮不实现,WT#5 SSE 收口)。

3. 过滤列表

过滤栏暴露 9 个查询参数(AND 组合),本轮 UIUX 收敛为 5 类 chip(玻璃化,.admin-diagnostic-chip):

控件参数类型描述
搜索框search自由文本匹配 descriptioncode(大小写不敏感)
级别 chip(全部/error/warning/info)levelerror | warning | info严重度,active 状态反色
来源 selectsourceapi | worker | queue | auth | react | manual | webhook | unhandledrejection | diagnostic来源子系统
时间窗(2 个 datetime-local)from / toISO 8601createdAt 范围
状态 chip(未解决/已解决/全部)resolvedtrue | false默认:未解决(琥珀色)
fingerprint inputfingerprint精确字符串同 fingerprint → 折叠为同一事件
code inputcode精确字符串UnifiedErrorCode 过滤(如 INTERNAL_ERRORGENERATION_TIMEOUT)
pageSize(分页)page / pageSize整数1-based 分页;pageSize 上限 200

GET /api/admin/diagnostic/issues?...{ items, total, unresolved, page, pageSize }

本轮新增:全选 checkbox + 行选 checkbox(state 在 DiagnosticTable 内部 Set<string>,未接后端)。selected 行视觉:左 3px primary 边框 + 8% 红渐变背景;critical 行(error + counter>1):左 3px 红 + 10% 红渐变;slow 行(warning + code=‘SLOW’):左 3px 黄 + 8% 黄渐变。批量标记已解决按钮本轮不接入,依赖 WT#3 后端 POST /api/admin/diagnostic/issues/bulk-resolve endpoint。

4. 打开详情全屏页

本轮 UIUX 变更:主页行 ⋯ 按钮(24×24 glass 圆角,触发 router.push('/admin/diagnostic/${id}'))直接跳全屏详情页;主页行 click 暂仍触发 Drawer(WT#4 收口:Drawer 入口弱化,与 Detail 合并)。onSelect prop 在 DiagnosticTable 仍保留,前端用 _onSelect 抑制 noUnusedParameters,待 WT#4 删除。

/admin/diagnostic/[id] 全屏详情页 5 Tab(玻璃化,.admin-diagnostic-detail-* 8 个新 class,admin.css §18 段):

  • Tab 1 · 上下文contextJson(PII 已脱敏)、logContext(AsyncLocalStorage 快照)、requestIdtaskIdprojectIdtraceId
  • Tab 2 · 调用栈 — 抛出点的 error.stack
  • Tab 3 · 同指纹历史 — 同 fingerprint 已 resolved 的最近 20 条,显示历史修复方案。(本轮 Tab 3 名称与原型对齐,生产实现原 Tab 3 为 “Agent Run”,WT#4 收口时切换)
  • Tab 4 · Agent Run — 与该 issue 关联的每个 DiagnosticAgentRun,含 status / durationMs / errorMessage(本轮保持原 Tab 4 位置,内容不变)。
  • Tab 5 · 处理记录(Activity Feed) — 5 节点 timeline:created / agentPlan / agentRun.{started,succeeded,failed,partial,skipped} / resolvedWT#4 实现,本轮 Activity Feed 仅有 created / agentPlan / resolved 3 节点。

详情页 5 KPI 摘要(路由 / 错误码 / 请求 ID / 用户 / 指纹 / 首次出现 / 最近一次 / 出现次数)与 Drawer 5 Tab 独立,不复用主页统计 API。URL 持久化:?tab=1&section=activity(WT#4 实现,刷新页面保持 tab 状态)。

5. 标记 issue 已解决(管理员)

POST /api/admin/diagnostic/issues/[id]/resolve,body { note, commitSha?, prUrl? }。路由:

  • 加载 issue;缺失抛 DIAGNOSTIC_ISSUE_NOT_FOUND(404)。
  • resolved=trueDIAGNOSTIC_ISSUE_ALREADY_RESOLVED(409)。
  • 更新 resolved / resolvedAt / resolvedById / resolutionNote / resolutionCommitSha / resolutionPrUrl
  • audit_log(action: 'diagnostic.resolve')。

6. 标记 agent 已解决(仅 agent service-account)

POST /api/admin/diagnostic/issues/[id]/agent-resolve(admin 调用返回 403 FORBIDDEN)。agent 必须带 x-internal-task-token + x-internal-agent-id 请求头。Body:

{ "resolution": "fixed" | "plan", "summary": "...", "planMarkdown": "...", "commitSha": "..." }
  • resolution=fixed → 设置 resolved=true,写 DiagnosticAgentRun.status=succeeded
  • resolution=plan → 写 agentPlanMarkdown + agentPlanGeneratedAt,run.status=partial
  • 审计动作:diagnostic.agent-resolve

7. 批量操作(WT#3 计划)

7.1 批量清空已解决 incidents(已实现)

DELETE /api/admin/diagnostic/issues?resolved=true(resolved=true 是必需参数;否则 400 DIAGNOSTIC_RESOLVED_REQUIRED)。删除所有 DiagnosticIssue where resolved=true,写 audit_log(action: 'diagnostic.bulk-delete')。

7.2 批量标记已解决(WT#3 待实现)

主页 UI 已就绪(checkbox 全选 + “批量标记已解决”按钮),但后端 endpoint POST /api/admin/diagnostic/issues/bulk-resolve 待 WT#3 落地。计划签名:

POST /api/admin/diagnostic/issues/bulk-resolve Body: { ids: string[], note?: string, commitSha?: string, prUrl?: string } Response: { resolved: number }

需走与单条 resolve 相同的 audit hook(action: 'diagnostic.bulk-resolve'),权限校验 requireAdminAuth

8. 单条删除

DELETE /api/admin/diagnostic/issues/[id](幂等:缺失 id 返回 { deleted: 0 })。审计动作:diagnostic.delete

9. 导出 ZIP 诊断包

GET /api/admin/diagnostic/export?from=&to=&resolved= 返回 Content-Type: application/zip,含三个硬编码文件名(INV-DIAG-04):

  • diagnostic-issues.json — 过滤窗口内完整 DiagnosticIssue 行(上限 5000 行)。
  • diagnostic-summary.txt — total / resolved / unresolved + top-10 code 计数。
  • diagnostic-export-meta.jsonexportedAt + exportedBy + filters

HTTP Content-Dispositionattachment; filename="diagnostic-export-{timestamp}.zip"。审计动作:diagnostic.export

9.1 实时刷新(SSE,WT#5 计划)

本轮不实现。“实时刷新中”文案在副标题保留但当前无 setInterval / SSE / WebSocket,刷新靠用户点击”刷新”按钮或重新进入页面。

WT#5 计划新增 GET /api/admin/diagnostic/stream SSE 端点(Next.js 15 ReadableStream),事件类型:

  • issue.created / issue.updated / issue.resolved
  • agentRun.started / agentRun.succeeded / agentRun.failed

后端 src/lib/diagnostic/publisher.ts 通过 Redis pub/sub(diag-events:stream 通道)派发;emit.tsagent-worker.ts 在 7 + 4 处状态变更后 publish。客户端 use-diagnostic-stream.ts EventSource hook 订阅(SSE 不支持自定义 header,走 ?access_token= 兜底鉴权)。

10. 配置预警规则

访问 /admin/diagnostic/alerts。CRUD:

  • GET /api/admin/diagnostic/alerts — 列出 DiagnosticAlert 行。
  • POST /api/admin/diagnostic/alerts — 创建 { ruleName, condition, channels, cooldownSec? }ruleName 唯一。
  • PATCH /api/admin/diagnostic/alerts/[id] — 切换 enabled / channels / cooldownSec

Burst 规则由 emit 自动创建:同 code 在 5 分钟内超过 burstThreshold 时,触发 burst.${code} upsert(例如 INTERNAL_ERRORburst.INTERNAL_ERROR)。

11. 配置通知订阅

访问 /admin/diagnostic/subscriptions。CRUD:

  • GET /api/admin/diagnostic/subscriptions — 列出本人的订阅。
  • POST /api/admin/diagnostic/subscriptions — 创建 { filter: {level?, source?, code?, fingerprintPattern?}, channel: 'email'|'slack'|'webhook', target }
  • DELETE /api/admin/diagnostic/subscriptions/[id]

通知 worker 按 filter 匹配 payload 并路由到配置的 channel。

11.1 Slack 通知配置(2026-09-11 新增)

DiagnosticSubscription.channel='slack' 时,target 字段填 Slack Incoming Webhook URL(形如 https://hooks.slack.com/services/T.../B.../...)。Notification Worker 把每个匹配事件渲染为 Slack Block Kit 消息,字段包括:

  • 标题Dramagon · Diagnostic · {level}(error/warning/info 三色)
  • 来源 / 错误码source + UnifiedErrorCode
  • 描述description(截断 500 字符)
  • Issue 链接${siteUrl}/admin/diagnostic/[id](全屏详情页)
  • Trace IDrequestId / taskId / projectId(若有)
  • FooterenqueuedAt + 折叠计数 counter

请求细节:

  • HTTP POST,Content-Type: application/json,Slack 自有 body 格式(非 Block Kit 高级卡片)
  • 超时 8s(AbortSignal.timeout(8000));非 2xx 抛 DIAGNOSTIC_SLACK_SEND_FAILED(被 worker 吞,不阻断主流程)
  • 失败 3 次后 BullMQ 重试(attempts: 3, backoff: exponential 1s)

测试方法(dev 环境):

curl -X POST http://localhost:3000/api/admin/diagnostic/subscriptions \ -H "Cookie: $ADMIN_SESSION" \ -H "Content-Type: application/json" \ -d '{ "filter": { "level": ["error"], "source": ["api"] }, "channel": "slack", "target": "https://hooks.slack.com/services/REPLACE/ME" }' # 触发一次 emit,30 秒内 Slack 应收到一条 curl -X POST http://localhost:3000/api/internal/diagnostic/emit \ -H "x-internal-task-token: $INTERNAL_TASK_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "level": "error", "source": "api", "code": "INTERNAL_ERROR", "description": "slack smoke test" }'

安全约束(INV-DIAG-14 同样适用于 webhook channel):

  • Webhook URL 必须 当作 secret 处理(Prisma AdminAuditLog.target 写入时需打码,生产部署应通过环境变量注入,数据库不留明文)
  • Notification Worker 永不 在日志中打印完整 target(截断为 https://hooks.slack.com/... 8 字符后缀)
  • target 多事件合并发送(burst 场景)按 cooldownSec 控制,默认 60s

API 端点

方法 + 路径AuthBody / QueryResponse
GET /api/admin/diagnostic/issuesadminlevel, source, resolved, from, to, search, fingerprint, code, page, pageSize(≤200){ items, total, unresolved, page, pageSize }
GET /api/admin/diagnostic/issues/[id]admin{ item: DiagnosticIssue & { user, resolvedBy, agentRuns } }
POST /api/admin/diagnostic/issues/[id]/resolveadmin{ note?, commitSha?, prUrl? }{ item }
POST /api/admin/diagnostic/issues/[id]/agent-resolveagent{ resolution, summary?, planMarkdown?, commitSha? }{ item, agentRunId }
DELETE /api/admin/diagnostic/issues/[id]admin{ deleted }(幂等)
DELETE /api/admin/diagnostic/issues?resolved=trueadminresolved=true 必需{ deleted }
GET /api/admin/diagnostic/exportadminfrom, to, resolved?application/zip blob
GET /api/admin/diagnostic/alertsadmin{ items: DiagnosticAlert[] }
POST /api/admin/diagnostic/alertsadmin{ ruleName, condition, channels, cooldownSec? }{ item }
PATCH /api/admin/diagnostic/alerts/[id]admin{ enabled?, channels?, cooldownSec? }{ item }
GET /api/admin/diagnostic/subscriptionsadmin{ items: DiagnosticSubscription[] }
POST /api/admin/diagnostic/subscriptionsadmin{ filter, channel, target }{ item }
DELETE /api/admin/diagnostic/subscriptions/[id]admin{ deleted }
GET /api/admin/diagnostic/agent-runsadmin?issueId, ?agentId, ?status{ items: DiagnosticAgentRun[] }
POST /api/internal/diagnostic/emitINTERNAL_TASK_TOKEN{ level, source, code?, description, context? }{ accepted: true }

数据模型

DiagnosticIssue 关键字段:

字段类型说明
iduuidPK
levelenumerror | warning | info
sourceenumapi | worker | queue | auth | react | manual | webhook | unhandledrejection | diagnostic
descriptiontext人类可读
codestring?对齐 UnifiedErrorCode(文本存储)
agentActionenumnone | auto | plan
contextJsontext (JSON)已脱敏;失败上下文
fingerprintstring?${source}::${code}::${slug(description,40)}::${YYYYMMDDHH}
counterint60s 窗口折叠计数
firstSeenAt / lastSeenAtdatetime折叠 + 审计
traceId / spanIdstring?OTel 128/64-bit hex
logContextjson?AsyncLocalStorage 快照
userId / taskId / projectIdstring?跨模块链接
apiTimingIdsstring[]折叠的 api-timing-sample.id 列表
resolved / resolvedAt / resolvedById / resolvedByAgentIdbool / datetime / fk / string解决状态
resolutionNote / resolutionCommitSha / resolutionPrUrltext? / string? / string?审计追踪
agentPlanMarkdown / agentPlanGeneratedAttext? / datetime?runDiagnosticAgent kind=plan-only 输出
createdAt / updatedAtdatetime审计

DiagnosticSubscription: { userId, filter: {level?, source?, code?, fingerprintPattern?}, channel, target, enabled }

DiagnosticAlert: { ruleName (unique), condition: {metric, threshold, window}, channels, cooldownSec, enabled, lastFiredAt }

DiagnosticAgentRun: { issueId, agentId, agentName, status: 'started'|'succeeded'|'failed'|'partial'|'skipped', stepsJson, durationMs, errorMessage? }

错误码

HTTPCode场景
400DIAGNOSTIC_INVALID_QUERYlevel / source 值非法(枚举不匹配)
400DIAGNOSTIC_RESOLVED_REQUIREDDELETE ?resolved != true
400DIAGNOSTIC_AGENT_INVALID_PAYLOAD缺失 / 非法 resolution 字段
401UNAUTHORIZED非 admin / 非 agent / 无效 INTERNAL_TASK_TOKEN
403FORBIDDENadmin 调用 agent-resolve;agent profile 缺失
404DIAGNOSTIC_ISSUE_NOT_FOUNDissue id 不存在
409DIAGNOSTIC_ISSUE_ALREADY_RESOLVED重复解决
500DIAGNOSTIC_DB_ERROR数据库故障
500DIAGNOSTIC_ZIP_ERRORZIP 生成失败

INV-DIAG-01 ~ INV-DIAG-15 不变式

ID描述实施位置
INV-DIAG-01软删除通过 resolved 字段;物理删除仅 C2/C3C2/C3 route handler
INV-DIAG-02emit 永不抛出(吞错 logWarn)emit.ts try/catch
INV-DIAG-03emit 是 O(1) fire-and-forgetemit() 返回 Promise.resolve()
INV-DIAG-04ZIP 文件名硬编码常量export/route.ts ZIP_FILE_NAME_*
INV-DIAG-05所有 C1/C1b/C1c/C1d/C2/C3 路由 requireAdminAuthroute 顶部守卫
INV-DIAG-0660s fingerprint 折叠,counter++emit.ts fingerprint step
INV-DIAG-07context email/token/api_key 自动脱敏emit.ts redact step
INV-DIAG-08burst:同 code 5min 内 ≥ threshold → DiagnosticAlertemit.ts + notification-worker.ts
INV-DIAG-09Agent worker concurrency=2, attempts=3 指数退避agent-worker.ts
INV-DIAG-10Watchdog 每 5 分钟;buffer > 5000 / queue > 10000 自指 emitwatchdog.ts
INV-DIAG-11retention:resolved > 90d 删;unresolved > 30d 删watchdog.ts
INV-DIAG-12所有 admin 操作写 audit_logaudit.ts + 5 个 route 调用
INV-DIAG-13DLQ 失败 Job 保留 7d + 自指 emit DIAGNOSTIC_QUEUE_DLQqueue.ts + agent-worker.ts
INV-DIAG-14客户端 emit 走 internal 端点 + INTERNAL_TASK_TOKEN/api/internal/diagnostic/emit
INV-DIAG-15缺失 code 兜底为 INTERNAL_ERROR(不允许 null)DIAGNOSTIC_CATALOG + getDiagnosticMeta

故障排查

  • 仪表板看不到 incidents:

    • 确认 instrumentation.ts Phase 5 已启动(startDiagnosticNotificationWorker + startDiagnosticAgentWorker + startDiagnosticWatchdog)。
    • 检查服务端 process.env.INTERNAL_TASK_TOKEN 已配置(生产要求)。
    • 确认 prisma generate 已重新跑过,客户端能识别 DiagnosticIssue / DiagnosticAlert / DiagnosticSubscription / DiagnosticAgentRun
  • emit 不触发:

    • 抛出点必须在 try/catch 内重新抛出或显式 surface 错误码。
    • 检查 src/lib/api-errors.ts 的 catch 分支调用 emitDiagnosticIssue
  • ZIP 导出返回 500 DIAGNOSTIC_ZIP_ERROR:

    • 检查 archiver 已安装(@types/archiver 应在 devDependencies)。
    • 确认 items 有界(take: 5000)— 极大时间窗口可能 OOM。
  • Agent worker 不消费:

    • 确认 agentAction 不为 none(仅 auto / plan 进入 agent worker)。
    • 检查 Redis 连通性(queueRedis 与 task queue 共享)。
    • 检查 DiagnosticAgentRun 行 — 失败应有 status=failed + errorMessage 填值。
  • Burst 预警不触发:

    • catalog 中 burstThreshold 对低严重度 code(如 UNAUTHORIZED)为 null
    • INTERNAL_ERROR burstThreshold=5;5 分钟内 emit 5 次可触发 burst.INTERNAL_ERROR upsert。
  • Retention 删除太激进:

    • Watchdog 每 5 分钟删 unresolved > 30dresolved > 90d。如需调整,改 src/lib/diagnostic/watchdog.ts 中的 RESOLVED_RETENTION_DAYS / UNRESOLVED_RETENTION_DAYS
Last updated on