dsh-agent-board 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/index.mjs +1 -0
- package/lib/client/board-list.js +81 -14
- package/lib/client/dashboard.js +16 -21
- package/lib/client/kernel.js +47 -8
- package/lib/client/task-detail.js +47 -2
- package/lib/client.js +191 -45
- package/lib/core.mjs +92 -0
- package/lib/dispatch.mjs +19 -143
- package/lib/health.mjs +8 -7
- package/lib/rpc.mjs +23 -5
- package/package.json +1 -1
- package/test/core.test.mjs +299 -0
- package/test/health.test.mjs +19 -8
package/README.md
CHANGED
|
@@ -91,7 +91,10 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
91
91
|
|
|
92
92
|
- 会话标题栏「智能看板」按钮 → 顶部抽屉面板(看板 / 团队 / 仪表盘三视图)
|
|
93
93
|
- 面板右上角「+ 新建任务」:表单弹层填标题/描述/优先级/管线/touches/依赖多选/验收脚本,Team 托管默认存为草稿(提交走 create-task RPC,成功即刷新、失败表单内联报错)
|
|
94
|
-
-
|
|
94
|
+
- 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 + 按钮区吸底常驻,不再被看板抽屉裁切);卡片「当前动作」行摘要化为「工具名 + 关键参数」(40 字符内有意义断点截断,悬停 title 看 host 原文),不再展示裸 JSON
|
|
95
|
+
- 六列状态流:草稿 → 待办 → 进行中 → 验证中 → 已完成 → 阻塞(空板也统一渲染六列骨架 + 引导 CTA:右上角「+ 新建任务」或对话一句 task_create 建第一张卡)
|
|
96
|
+
- 读路径 error 防线:get-tasks 返回 {error} 或网络抖动时保留现有卡片与设置(不再静默清空全板),面板头部下方出非阻断红色错误条,下次刷新成功自动消失
|
|
97
|
+
- 耗时口径三分离:待办/草稿卡显「⏳ 等待 N」(创建→被领取),执行后显「⏱ 执行 N」(被领取→完成,tooltip 写清口径)——排队时间不再混进"耗时"
|
|
95
98
|
- 拖拽流转、多选批量操作(带一步撤销)、文本/优先级/标签筛选
|
|
96
99
|
- 归档区:时间倒序 + 排序选择器 + 纵向滚动
|
|
97
100
|
- Esc 逐级关闭(详情 → 看板 → 面板)
|
|
@@ -101,7 +104,8 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
101
104
|
|
|
102
105
|
- 仪表盘视图新增「Token 消耗」区:本看板累计总量 + 输入 / 输出 / 缓存读(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`)
|
|
103
106
|
- 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计与 `runs` 计数,多轮重跑/驳回重做自动累加),`get-tasks` 再现算 board 级 `usageSummary`(总量 / 按模型 / Top8,不落盘额外表)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」
|
|
104
|
-
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 /
|
|
107
|
+
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
|
|
108
|
+
- 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
|
|
105
109
|
|
|
106
110
|
### 学习反馈(候选教训信号 → 主窗口沉淀)
|
|
107
111
|
|
|
@@ -128,6 +132,8 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
128
132
|
- **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
|
|
129
133
|
- **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
|
|
130
134
|
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档
|
|
135
|
+
- 卡片识别层:父卡显示「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条(有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
|
|
136
|
+
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,点击直达子卡详情,标题行汇总 resolved/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
131
137
|
- **删除通道(真删,无 undo)**:`delete-task` RPC(卡片 hover 垃圾桶按钮 / 详情页「删除」按钮,均先 `confirm('删除不可恢复,确认删除「标题」?')`)+ `batch-op op='delete'`(多选模式底部「批量删除」,同样 confirm)。状态门禁:**草稿/待办/阻塞可删**;进行中/验证中拒绝并提示先用 `terminate-agent` 终止(避免在跑的 run 变孤儿);已完成/取消引导改用归档(`archive-task`,留档可检索);有**未归档子任务**时拒删(防 `parentId` 悬空破坏父任务自动流转);已归档任务幂等返回 ok。是真删(从 `tasks` 数组移除),因此**不产生 `batch-undo` 撤销快照**(批量条对 delete 不显示「↩️ 撤销」),删除操作在 host 端 `console.error` 留一行日志便于溯源
|
|
132
138
|
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约
|
|
133
139
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
@@ -135,9 +141,10 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
135
141
|
### 一次性派发(v74 去池化)
|
|
136
142
|
|
|
137
143
|
- 每个任务 spawn 一个**一次性子代理**(Worker/Verifier),上下文全量注入 prompt,做完即销毁——无常驻池、无池化状态残留
|
|
138
|
-
- **预研上下文注入(contextFiles/contextNotes)**:主窗口调研时读过的文件与笔记,由 host 在派发时读盘取最新内容,经「上下文注入」区块提供给 Worker/Verifier(不混进 user prompt);预算口径单文件 8KB、总包 40KB
|
|
144
|
+
- **预研上下文注入(contextFiles/contextNotes)**:主窗口调研时读过的文件与笔记,由 host 在派发时读盘取最新内容,经「上下文注入」区块提供给 Worker/Verifier(不混进 user prompt);预算口径单文件 8KB、总包 40KB;UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
|
|
139
145
|
- **锚点行段**:`contextFiles` 支持 `path:L2350-L2420` / `path:L2350` 行段语法(只认尾部 `:L<行号>`,兼容 Windows 盘符),只注入该段(段长上限 400 行,超出截断并标注);锚点无效(越界/写法错)自动回退头部注入并标注「锚点无效,已回退头部」
|
|
140
146
|
- **截断结构索引**:头部注入被预算截断时,标注升级为「截断:共 N 行,已注入 1–M 行」,并附结构索引块(JS/TS 顶层函数/类/箭头赋值、Markdown 标题及行号,上限 40 条)——Worker 照索引用锚点语法补读目标段即可,不用全文盘点
|
|
147
|
+
- **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时读包失败落任务「最近失败」行,不再静默
|
|
141
148
|
- **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
|
|
142
149
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
143
150
|
- 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
|
package/index.mjs
CHANGED
|
@@ -63,6 +63,7 @@ export function apply(ctx) {
|
|
|
63
63
|
rootForSession: session.rootForSession, deriveWorkMode: session.deriveWorkMode, runsFor: session.runsFor,
|
|
64
64
|
rt: store.rt, mutateLocked: store.mutateLocked,
|
|
65
65
|
maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
66
|
+
pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
|
|
66
67
|
spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
|
|
67
68
|
})
|
|
68
69
|
|
package/lib/client/board-list.js
CHANGED
|
@@ -67,14 +67,62 @@
|
|
|
67
67
|
function isCardHover(id, kind) { return String(state.cardHover || '') === (id + '|' + kind) }
|
|
68
68
|
function setHover(id, kind) { var v = id ? (id + '|' + kind) : ''; if (state.cardHover !== v) { state.cardHover = v; notify() } }
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// ===== 卡片「当前动作」行摘要化(client 侧提炼;host 仍下发原文,不改 host)=====
|
|
71
|
+
// host 的 activity 形如 `🔧 pwsh {"command": "Get-ChildItem …"}`(90 字符硬截,常断在 JSON 中段,
|
|
72
|
+
// 原样展示零信息量,反馈 n-mut914xana0t)或 `💬 自由文本`。这里提炼为「工具名 + 关键参数摘要」:
|
|
73
|
+
// pwsh → 命令体;read/write/edit → 文件 basename;browser_* → 动作名 + 目标(target/url/ref);
|
|
74
|
+
// 参数摘要 40 字符内、优先在空格/路径分隔符处断点;解析失败/自由文本回退为原文前 40 字符(头部截断)。
|
|
75
|
+
function actCut(s, n) { s = String(s || ''); if (s.length <= n) return s; var c = s.slice(0, n); var last = c.charCodeAt(c.length - 1); if (last >= 0xD800 && last <= 0xDBFF) c = c.slice(0, -1); var sp = Math.max(c.lastIndexOf(' '), c.lastIndexOf('/'), c.lastIndexOf('\\')); if (sp >= Math.floor(n / 2)) c = c.slice(0, sp); return c.replace(/[\s/\\]+$/, '') + '…' } // 不斩断代理对(emoji)
|
|
76
|
+
// 从不完整 JSON 文本里抢指定 key 的第一个字符串值(host 文本常被硬截,JSON.parse 多半失败,正则直达)
|
|
77
|
+
function actArg(rest, key) { var m = String(rest || '').match(new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)')); return m ? m[1] : '' }
|
|
78
|
+
function actBase(p) { p = String(p || ''); var i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')); return i >= 0 ? p.slice(i + 1) : p }
|
|
79
|
+
function activitySummary(text) {
|
|
80
|
+
var s = String(text || '').trim()
|
|
81
|
+
if (!s) return ''
|
|
82
|
+
if (s.indexOf('💬') === 0) return actCut(s, 40) // 自由文本:保留 💬 前缀从头部截
|
|
83
|
+
var body = s.replace(/^🔧\s*/, '') // 去工具调用前缀(host 格式 🔧 <tool> <argsJSON>)
|
|
84
|
+
var m = body.match(/^([A-Za-z_][\w-]*)\s*([\s\S]*)$/)
|
|
85
|
+
if (!m) return actCut(s, 40)
|
|
86
|
+
var tool = m[1], rest = (m[2] || '').trim()
|
|
87
|
+
if (!rest) return actCut(s, 40)
|
|
88
|
+
var v = ''
|
|
89
|
+
if (tool === 'pwsh') v = actArg(rest, 'command')
|
|
90
|
+
else if (tool === 'read' || tool === 'write' || tool === 'edit') v = actBase(actArg(rest, 'file_path'))
|
|
91
|
+
if (v) return tool + ' ' + actCut(v, 40)
|
|
92
|
+
if (tool.indexOf('browser_') === 0) {
|
|
93
|
+
var act = actArg(rest, 'action'); var tgt = actArg(rest, 'target') || actArg(rest, 'url') || actArg(rest, 'ref')
|
|
94
|
+
var parts = []; if (act) parts.push(act); if (tgt) parts.push(tgt)
|
|
95
|
+
if (parts.length) return tool + ' ' + actCut(parts.join(' '), 40)
|
|
96
|
+
}
|
|
97
|
+
return actCut(s, 40) // 无法解析:原文前 40 字符(从头部截,不再断在 JSON 中段)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
|
|
101
|
+
// 父卡 = 元信息行首位「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
|
|
102
|
+
// 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
|
|
103
|
+
// 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
|
|
104
|
+
// ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
|
|
105
|
+
// 口径:pipeline ∈ {full, work}(direct 主窗口自处理,无需调研注入)且 touches 非空(声明了要改文件)
|
|
106
|
+
// 且 context.files / context.notes 皆空。context 缺省兼容:老任务无 context 对象按空处理——
|
|
107
|
+
// 即老任务也会亮徽章,这是预期行为(立单质量可见即问责;draft/pending 等状态照常显示,与状态无关)。
|
|
108
|
+
function noResearch(t) {
|
|
109
|
+
var pl = t.pipeline || 'full'
|
|
110
|
+
if (pl !== 'full' && pl !== 'work') return false
|
|
111
|
+
if (!Array.isArray(t.touches) || t.touches.length === 0) return false
|
|
112
|
+
var cx = t.context || {}
|
|
113
|
+
var files = Array.isArray(cx.files) ? cx.files : []
|
|
114
|
+
var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
|
|
115
|
+
return files.length === 0 && notes.length === 0
|
|
116
|
+
}
|
|
117
|
+
function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onClick: function () { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }, style: { border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
118
|
+
epic ? React.createElement('span', { title: '史诗父卡:' + cs.resolved + '/' + cs.total + ' 个子任务已完成', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + cs.resolved + '/' + cs.total) : null,
|
|
71
119
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
72
120
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
73
121
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
74
122
|
(t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
|
|
75
123
|
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
76
124
|
t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
|
|
77
|
-
|
|
125
|
+
durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
|
|
78
126
|
// 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
|
|
79
127
|
delOk ? React.createElement('span', {
|
|
80
128
|
onClick: function (e) { e.stopPropagation(); deleteTask(t.id, t.title) }, // 阻止冒泡:不打开详情页
|
|
@@ -82,10 +130,14 @@
|
|
|
82
130
|
title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
|
|
83
131
|
style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
|
|
84
132
|
}, ic('trash-2', 11)) : null),
|
|
85
|
-
|
|
133
|
+
// 史诗迷你进度条(3px 高,resolved/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
|
|
134
|
+
epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + cs.resolved + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((cs.resolved / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
|
|
135
|
+
epic && cs.activeTitle ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '在跑子任务:' + cs.activeTitle }, '▸ 在跑:' + cs.activeTitle) : null,
|
|
136
|
+
// 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
|
|
137
|
+
(t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + activitySummary(state.activity[t.id])) : null,
|
|
86
138
|
// 里程碑进展(Worker 主动上报的轻量进展,kind=progress):进行中的卡片显示最新一条 + 相对时间,无则不显示
|
|
87
139
|
t.status === 'in-progress' && t.lastProgress && t.lastProgress.text ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '最近进展(' + (t.lastProgress.by || '') + ' · ' + fmtTime(t.lastProgress.at) + '):' + t.lastProgress.text }, '📈 ' + t.lastProgress.text + ' · ' + ago(t.lastProgress.at)) : null,
|
|
88
|
-
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
|
|
140
|
+
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '依赖全部完成后才会派发' }, '⛓ 等待「' + depWaitTitle + '」' + (depWaitN > 1 ? ' 等 ' + depWaitN + ' 个' : '')) : null) }
|
|
89
141
|
|
|
90
142
|
function ArchiveView() {
|
|
91
143
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
@@ -146,6 +198,8 @@
|
|
|
146
198
|
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
147
199
|
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
148
200
|
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
201
|
+
var _ok = useState(''), okMsg = _ok[0], setOkMsg = _ok[1] // 成功行(仅「创建成功但响应带 warning」的不关窗路径展示)
|
|
202
|
+
var _wn = useState(''), warn = _wn[0], setWarn = _wn[1] // host create-task 成功路径附带的 warning 原文(可能多条合并,原样黄色展示)
|
|
149
203
|
// 依赖候选:当前看板 status 为 draft/pending 的任务
|
|
150
204
|
var cands = state.tasks.filter(function (t) { return t.status === 'draft' || t.status === 'pending' })
|
|
151
205
|
function close() { state.createOpen = false; notify() }
|
|
@@ -156,13 +210,17 @@
|
|
|
156
210
|
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
157
211
|
var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
158
212
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
159
|
-
setBusy(true); setErr('')
|
|
213
|
+
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
160
214
|
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
161
215
|
setBusy(false)
|
|
162
216
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
217
|
+
fetchTasks()
|
|
218
|
+
// 调研门禁·软警告:host 成功路径可附 warning 字符串(如空描述提醒,可能多条合并)——不拦截创建、
|
|
219
|
+
// 也不打断成功提示:表单保持打开,成功行 + ⚠️ 黄色 warning 原文行同屏展示,读完自行关闭;
|
|
220
|
+
// 清掉标题防「再点一次创建」连击出重复卡(空标题会被校验拦住)。无 warning 维持原关窗 + 头部 flash 行为。
|
|
221
|
+
if (r && r.warning) { setOkMsg(asDraft ? '✅ 已存草稿' : '✅ 已创建任务'); setWarn(String(r.warning)); setTitle(''); return }
|
|
163
222
|
state.createOpen = false
|
|
164
223
|
flashCreated(asDraft ? '✅ 已存草稿' : '✅ 已创建任务')
|
|
165
|
-
fetchTasks()
|
|
166
224
|
notify()
|
|
167
225
|
}).catch(function (e) { setBusy(false); setErr('⚠️ ' + String(e)) })
|
|
168
226
|
}
|
|
@@ -171,11 +229,16 @@
|
|
|
171
229
|
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
172
230
|
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
173
231
|
var btnPrimary = { fontSize: 11, padding: '4px 14px', border: 'none', borderRadius: 5, background: C.brand, color: C_INV, fontWeight: 600, cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
232
|
+
// 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
|
|
233
|
+
// 的裁剪上下文(抽屉无 transform/filter,fixed 相对视口定位不被裁);弹窗限高 80vh、字段区内部滚动、
|
|
234
|
+
// 底部按钮区吸底常驻——标题与「创建并派发」始终同屏,无需盲滚。
|
|
235
|
+
return React.createElement('div', { style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: function (e) { if (e.target === e.currentTarget) close() } },
|
|
236
|
+
React.createElement('div', { style: { width: 440, maxWidth: '100%', maxHeight: '80vh', display: 'flex', flexDirection: 'column', background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.28)', overflow: 'hidden' } },
|
|
237
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0 } },
|
|
177
238
|
React.createElement('span', { style: { fontSize: 13, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('plus', 14), '新建任务'),
|
|
178
239
|
React.createElement('button', { onClick: close, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', color: C.text2, display: 'inline-flex' } }, ic('x', 14))),
|
|
240
|
+
// 字段区:内容超出 80vh 时在此内部滚动
|
|
241
|
+
React.createElement('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '10px 12px 4px' } },
|
|
179
242
|
field('标题 *', React.createElement('input', { value: title, onChange: function (e) { setTitle(e.target.value) }, placeholder: '一句话说清要做什么', style: inp })),
|
|
180
243
|
field('描述', React.createElement('textarea', { value: desc, onChange: function (e) { setDesc(e.target.value) }, rows: 3, placeholder: '背景 / 目标 / 约束(可空)', style: Object.assign({}, inp, { resize: 'vertical', minHeight: 46 }) })),
|
|
181
244
|
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
@@ -196,9 +259,13 @@
|
|
|
196
259
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
197
260
|
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
198
261
|
'存为草稿',
|
|
199
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)')),
|
|
200
|
-
|
|
201
|
-
React.createElement('div', { style: {
|
|
202
|
-
React.createElement('
|
|
203
|
-
React.createElement('
|
|
262
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)'))),
|
|
263
|
+
// 底部:错误行 + 按钮区吸底常驻(flexShrink:0,不随字段区滚动)
|
|
264
|
+
React.createElement('div', { style: { flexShrink: 0, padding: '8px 12px 12px', borderTop: '1px solid ' + C.border, background: C.bg } },
|
|
265
|
+
okMsg ? React.createElement('div', { style: { fontSize: 11, color: C.ok, marginBottom: 6 } }, okMsg) : null,
|
|
266
|
+
warn ? React.createElement('div', { style: { fontSize: 11, color: C.warn, marginBottom: 6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, '⚠️ ' + warn) : null,
|
|
267
|
+
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
268
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
269
|
+
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
270
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
|
|
204
271
|
}
|
package/lib/client/dashboard.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
|
|
9
9
|
function computeStats(tasks) {
|
|
10
|
-
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {},
|
|
10
|
+
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {}, queueTimes = [], execTimes = [], verifyTimes = [], todayDone = 0, todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); var recentActivity = []
|
|
11
11
|
var dailyDone = [] // #15 近 7 天每日完成趋势
|
|
12
12
|
for (var di = 6; di >= 0; di--) { var dayStart = new Date(todayStart); dayStart.setDate(dayStart.getDate() - di); var dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); dailyDone.push({ label: (dayStart.getMonth() + 1) + '/' + dayStart.getDate(), count: 0, start: dayStart, end: dayEnd }) }
|
|
13
|
-
tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.
|
|
13
|
+
tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.createdAt && t.claimedAt) queueTimes.push(new Date(t.claimedAt) - new Date(t.createdAt)); if (t.claimedAt && t.resolvedAt) execTimes.push(new Date(t.resolvedAt) - new Date(t.claimedAt)); if (t.verifiedAt && t.resolvedAt) verifyTimes.push(new Date(t.verifiedAt) - new Date(t.resolvedAt)); if (t.resolvedAt) { var rd = new Date(t.resolvedAt); if (rd >= todayStart) todayDone++; for (var k = 0; k < dailyDone.length; k++) { if (rd >= dailyDone[k].start && rd < dailyDone[k].end) { dailyDone[k].count++; break } } } if (Array.isArray(t.history)) t.history.forEach(function (h) { recentActivity.push({ taskId: t.id, title: t.title, from: h.from, to: h.to, actor: h.actor, timestamp: h.timestamp, note: h.note }) }) })
|
|
14
14
|
recentActivity.sort(function (a, b) { return (b.timestamp || '').localeCompare(a.timestamp || '') }); recentActivity = recentActivity.slice(0, 10)
|
|
15
15
|
function avgMs(arr) { if (arr.length === 0) return null; var s = arr.reduce(function (a, b) { return a + b }, 0); return Math.round(s / arr.length) }
|
|
16
16
|
function fmtMs(ms) { if (!ms) return '-'; var m = Math.floor(ms / 60000); if (m < 60) return m + ' 分钟'; var h = Math.floor(m / 60); if (h < 24) return h + ' 小时 ' + (m % 60) + ' 分'; return Math.floor(h / 24) + ' 天 ' + (h % 24) + ' 时' }
|
|
17
|
-
|
|
17
|
+
// 耗时口径三分离(task-mutdnitw):avgQueue=平均排队(创建→被领取)、avgExec=平均执行(被领取→完成),均只统计领取过的卡;avgVerify 口径不动(完成→验收)
|
|
18
|
+
return { total: total, byStatus: byStatus, byPriority: byPriority, byAgent: byAgent, avgQueue: fmtMs(avgMs(queueTimes)), avgExec: fmtMs(avgMs(execTimes)), avgVerify: fmtMs(avgMs(verifyTimes)), todayDone: todayDone, dailyDone: dailyDone, recentActivity: recentActivity }
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function TrendChart(props) {
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
|
|
35
36
|
function StatCard(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' } }, React.createElement('div', { style: { fontSize: 20, fontWeight: 700, color: props.color || C.text } }, String(props.value)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2 } }, props.label)) }
|
|
36
37
|
|
|
38
|
+
// 双行指标卡(耗时口径三分离:原「平均完成时间」一个展示位拆为 排队/执行 两行,不挤占其他指标位)
|
|
39
|
+
function StatCardDual(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' }, title: props.title }, React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text } }, String(props.value1)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label1), React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text, marginTop: 3 } }, String(props.value2)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label2)) }
|
|
40
|
+
|
|
37
41
|
function BarRow(props) { var pct = props.total > 0 ? (props.count / props.total * 100) : 0; return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } }, React.createElement('span', { style: { width: 40, fontSize: 10, color: C.text2, textAlign: 'right', flexShrink: 0 } }, props.label), React.createElement('div', { style: { flex: 1, height: 8, background: C.nested, borderRadius: 4, overflow: 'hidden' } }, React.createElement('div', { style: { height: '100%', width: pct + '%', background: props.color || C.brand, borderRadius: 4, transition: 'width .3s' } })), React.createElement('span', { style: { width: 24, fontSize: 10, color: C.text2, flexShrink: 0 } }, String(props.count))) }
|
|
38
42
|
|
|
39
43
|
// ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
|
|
@@ -205,23 +209,13 @@
|
|
|
205
209
|
}))))
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
// ===== 架构健康提示区(架构自省 L1 · 数据源:
|
|
212
|
+
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
209
213
|
// healthHints 由 host lib/health.mjs 的 computeHealthHints(tasks) 每次请求现算(纯函数零存储零 IO):
|
|
210
|
-
// [{ level: 'warn'|'info', text }]
|
|
211
|
-
//
|
|
212
|
-
//
|
|
214
|
+
// [{ level: 'warn'|'info', text }]。kernel fetchTasks 已将其与 tasks 同源透传进 state.healthHints,
|
|
215
|
+
// 本组件不再自持 rpc('get-tasks')(消灭仪表盘打开期间的 3s 双轮询),随面板 notify 重渲染同步刷新。
|
|
216
|
+
// 缺省兼容:老 host 无此字段 / 非数组 / 空数组 → 返回 null,整块不渲染(零残留)。无按钮无状态。
|
|
213
217
|
function HealthHints() {
|
|
214
|
-
var
|
|
215
|
-
var _a = useState([]), hints = _a[0], setHints = _a[1]
|
|
216
|
-
useEffect(function () {
|
|
217
|
-
var dead = false
|
|
218
|
-
rpc('get-tasks').then(function (d) {
|
|
219
|
-
if (dead) return
|
|
220
|
-
var arr = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
|
|
221
|
-
setHints(arr.filter(function (h) { return h && h.text }))
|
|
222
|
-
}).catch(function () {})
|
|
223
|
-
return function () { dead = true }
|
|
224
|
-
}, [state.tasks])
|
|
218
|
+
var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
|
|
225
219
|
if (!hints.length) return null
|
|
226
220
|
// 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
|
|
227
221
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
@@ -247,7 +241,7 @@
|
|
|
247
241
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
248
242
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按状态分布'), statusOrder.map(function (s) { return React.createElement(BarRow, { key: s, label: statusLabels[s] || s, count: stats.byStatus[s] || 0, total: stats.total, color: statusColors[s] || C.brand }) })),
|
|
249
243
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按优先级分布'), prioOrder.map(function (p) { return React.createElement(BarRow, { key: p, label: prioLabel[p] || p, count: stats.byPriority[p] || 0, total: stats.total, color: prioColor[p] || C.brand }) }))),
|
|
250
|
-
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(
|
|
244
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCardDual, { label1: '平均排队', value1: stats.avgQueue, label2: '平均执行', value2: stats.avgExec, title: '耗时口径三分离:⏳ 排队 = 创建 → 被领取(等了多久);⏱ 执行 = 被领取 → 完成(干了多久);均不含验收时长' }), React.createElement(StatCard, { label: '平均验证时间', value: stats.avgVerify }), React.createElement(StatCard, { label: '今日完成', value: stats.todayDone, color: C.ok })),
|
|
251
245
|
React.createElement(TrendChart, { data: stats.dailyDone }),
|
|
252
246
|
Object.keys(stats.byAgent).length > 0 ? React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, 'Agent 负载(进行中+验证中)'), Object.keys(stats.byAgent).map(function (aid) { return React.createElement('div', { key: aid, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 11 } }, React.createElement(ActorLink, { id: aid }), React.createElement('span', { style: { color: C.text2 } }, stats.byAgent[aid] + ' 个任务')) })) : null,
|
|
253
247
|
React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '最近活跃'), stats.recentActivity.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无记录') : stats.recentActivity.map(function (a, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 3, display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('span', { style: { color: statusColors[a.to] || C.brand, fontWeight: 600 } }, statusLabels[a.to] || a.to), React.createElement('span', { style: { color: C.text, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, a.title), React.createElement(ActorLink, { id: a.actor }), React.createElement('span', null, ago(a.timestamp))) })))
|
|
@@ -308,8 +302,9 @@
|
|
|
308
302
|
var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
|
|
309
303
|
var vTotal = (ps.verifiers || []).length
|
|
310
304
|
return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
|
|
311
|
-
|
|
312
|
-
React.createElement('span', { title: '
|
|
305
|
+
// 池水位 title 标注(反馈 n-mut9rzoq3flu):裸数字新用户看不懂,⚡=Worker 在跑/上限,✓=Verifier 在跑/上限
|
|
306
|
+
React.createElement('span', { title: 'Worker 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
|
|
307
|
+
React.createElement('span', { title: 'Verifier 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
|
|
313
308
|
}
|
|
314
309
|
|
|
315
310
|
// #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
|
package/lib/client/kernel.js
CHANGED
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
|
|
220
220
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
221
221
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
222
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, lessonPushed: {}, isRoot: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', usageSummary: null }
|
|
222
|
+
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, lessonPushed: {}, isRoot: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', usageSummary: null, childStats: {}, tasksErr: '' }
|
|
223
223
|
|
|
224
224
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
225
225
|
try {
|
|
@@ -245,11 +245,26 @@
|
|
|
245
245
|
function notify() { listeners.forEach(function (fn) { try { fn(state) } catch (_) {} }) }
|
|
246
246
|
function rpc(method, args) { var a = args || {}; a.sessionId = state.sessionId; return fetch('/dsh-agent-board', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: method, args: a }) }).then(function (r) { return r.json() }) }
|
|
247
247
|
|
|
248
|
+
// ===== 读路径错误防线(反馈 n-mut9q600tnpx)=====
|
|
249
|
+
// {error} 响应 / 网络 reject 一律保留旧数据(旧卡片继续显示)+ 面板头部下方非阻断错误条,
|
|
250
|
+
// 下次成功自动消失;绝不把 {error} 当空板渲染。console.warn 30s 去抖——3s 轮询失败每次都打会刷屏。
|
|
251
|
+
var readErrWarnAt = 0
|
|
252
|
+
function reportReadErr(msg) {
|
|
253
|
+
state.tasksErr = msg
|
|
254
|
+
var now = Date.now()
|
|
255
|
+
if (now - readErrWarnAt > 30000) { readErrWarnAt = now; try { console.warn('[task-board] ' + msg) } catch (_) {} }
|
|
256
|
+
notify()
|
|
257
|
+
}
|
|
258
|
+
function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
|
|
259
|
+
function readErrText(e) { return String((e && e.message) || e || '网络异常') }
|
|
260
|
+
|
|
248
261
|
function fetchTasks() {
|
|
249
262
|
if (!state.sessionId) return
|
|
250
263
|
var epoch = reqEpoch
|
|
251
264
|
rpc('get-tasks').then(function (d) {
|
|
252
265
|
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
266
|
+
if (d && d.error) { reportReadErr('看板数据刷新失败:' + d.error); return } // error 分支:保留旧 tasks/设置,绝不当空板渲染
|
|
267
|
+
clearReadErr()
|
|
253
268
|
state.tasks = (d && d.tasks) || []
|
|
254
269
|
state.boardMode = (d && d.boardMode) || 'auto'
|
|
255
270
|
state.teamMode = !!(d && d.teamMode)
|
|
@@ -266,6 +281,12 @@
|
|
|
266
281
|
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
267
282
|
state.poolStatus = (d && d.poolStatus) || null
|
|
268
283
|
state.usageSummary = (d && d.usageSummary) || null // token 消耗聚合(board 级,host 端现算)
|
|
284
|
+
// 架构健康提示(架构自省 L1):与 tasks 同源透传,HealthHints 组件直接读 state.healthHints,
|
|
285
|
+
// 不再自持 rpc('get-tasks')——消灭仪表盘打开期间的双轮询。老 host 无此字段 → 空数组零渲染
|
|
286
|
+
state.healthHints = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
|
|
287
|
+
// 史诗父卡语义层:childStats 由 host 按 tasks 现算({parentId:{total,resolved,active,activeTitle}})。
|
|
288
|
+
// 缺省兼容——老 host 不返回该字段时置空对象,卡片/详情层遇空一律不渲染相关元素
|
|
289
|
+
state.childStats = (d && d.childStats) || {}
|
|
269
290
|
// 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
|
|
270
291
|
state.feedbackEnabled = !(d && d.feedbackEnabled === false)
|
|
271
292
|
state.isRoot = !d || d.isRoot !== false
|
|
@@ -276,10 +297,11 @@
|
|
|
276
297
|
state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
|
|
277
298
|
if (newEsc.length > 0) { state.open = true; state.view = 'board'; state.detailId = newEsc[0].id }
|
|
278
299
|
notify()
|
|
279
|
-
}).catch(function () {})
|
|
300
|
+
}).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('看板数据刷新失败:' + readErrText(e)) }) // 网络 reject 同口径
|
|
280
301
|
}
|
|
281
302
|
|
|
282
|
-
|
|
303
|
+
// fetchChildren 同病同药:{error} 保留旧 children + 错误条;成功路径不清条(交给 fetchTasks 成功兜底清除,避免并发响应互相抢)
|
|
304
|
+
function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; if (d && d.error) { reportReadErr('子任务列表刷新失败:' + d.error); return } state.children = (d && d.children) || []; notify() }).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('子任务列表刷新失败:' + readErrText(e)) }) }
|
|
283
305
|
|
|
284
306
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
285
307
|
function fetchActivity() {
|
|
@@ -401,7 +423,21 @@
|
|
|
401
423
|
function depsBlocked(t) { if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false; return t.dependsOn.some(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }) }
|
|
402
424
|
// #19 管线档位元数据
|
|
403
425
|
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
404
|
-
|
|
426
|
+
// ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
|
|
427
|
+
// waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
|
|
428
|
+
// execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
|
|
429
|
+
// 无 claimedAt(手工/direct 卡没有领取动作)回退原口径 createdAt → resolvedAt/archivedAt/now
|
|
430
|
+
// (验收时长 = resolvedAt → verifiedAt,仪表盘 verifyTimes 已有此口径,卡片不展示)
|
|
431
|
+
function fmtDur(s, e) { var m = Math.max(0, Math.round((e - s) / 60000)); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') }
|
|
432
|
+
function waitOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var e = t.claimedAt ? new Date(t.claimedAt).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
|
|
433
|
+
function execOf(t) { try { var sRaw = t.claimedAt || t.createdAt; var s = sRaw ? new Date(sRaw).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
|
|
434
|
+
// 卡片耗时徽章:draft/pending(还在排队)显「⏳ 等待」;执行后(含 blocked/归档)显「⏱ 执行」;tooltip 写清口径
|
|
435
|
+
function cardDur(t) {
|
|
436
|
+
var st = t.status
|
|
437
|
+
if (st === 'draft' || st === 'pending') { var w = waitOf(t); return w ? { txt: '⏳ 等待 ' + w, tip: '⏳ 排队时长:创建 → 被领取(还没被领取则统计创建至今)——"等了多久",不含执行' } : null }
|
|
438
|
+
var x = execOf(t)
|
|
439
|
+
return x ? { txt: '⏱ 执行 ' + x, tip: '⏱ 执行时长:被领取 → 完成/归档(进行中则统计领取至今)——"干了多久",不含排队与验收' + (t.claimedAt ? '' : ';本卡无领取记录(手工/直办),按创建起算') } : null
|
|
440
|
+
}
|
|
405
441
|
function elapsedMin(iso) { var s = iso ? new Date(iso).getTime() : 0; if (!s) return 0; return Math.max(0, Math.round((Date.now() - s) / 60000)) }
|
|
406
442
|
// 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
|
|
407
443
|
function historyRuns(t) {
|
|
@@ -420,7 +456,7 @@
|
|
|
420
456
|
function BoardButton(props) {
|
|
421
457
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
422
458
|
var _a = useState(0), pendingCount = _a[0], setPendingCount = _a[1]; var _b = useState(false), isOpen = _b[0], setIsOpen = _b[1]; var _c = useState(0), escCount = _c[0], setEscCount = _c[1]; var _d2 = useState(true), isRoot = _d2[0], setIsRoot = _d2[1]
|
|
423
|
-
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
459
|
+
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
424
460
|
useEffect(function () { function update() { var n = 0, e = 0; for (var i = 0; i < state.tasks.length; i++) { if (state.tasks[i].status === 'pending') n++; if (state.tasks[i].escalation) e++ }; setPendingCount(n); setEscCount(e); setIsOpen(state.open); setIsRoot(state.isRoot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
425
461
|
if (!isRoot) return null // 子代理会话不显示看板入口
|
|
426
462
|
return React.createElement('button', { onClick: function () { state.open = !state.open; notify() }, title: '智能看板' + (pendingCount > 0 ? '(' + pendingCount + ' 待办)' : '') + (escCount > 0 ? '(' + escCount + ' 待裁决)' : ''), style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', border: '1px solid ' + (escCount > 0 ? C.err : C.border), borderRadius: 6, background: isOpen ? C.nested : 'transparent', color: C.text, cursor: 'pointer', fontSize: 12 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, ic('clipboard-list', 14)), React.createElement('span', null, '智能看板'), escCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.err, color: C_INV, fontSize: 10, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 1, animation: 'tskb-pulse 1s ease-in-out infinite' }, title: escCount + ' 个任务待裁决' }, ic('alert-triangle', 10), escCount) : null, pendingCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.brand, color: C_INV, fontSize: 10, fontWeight: 600, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' } }, String(pendingCount)) : null)
|
|
@@ -435,9 +471,10 @@
|
|
|
435
471
|
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
436
472
|
var _wmo = useState(state.workMode), workMode = _wmo[0], setWorkModeState = _wmo[1]
|
|
437
473
|
var _cro = useState(state.createOpen), createOpen = _cro[0], setCreateOpen = _cro[1]; var _crf = useState(state.createFlash), createFlash = _crf[0], setCreateFlash = _crf[1]
|
|
474
|
+
var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
|
|
438
475
|
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
439
476
|
var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
|
|
440
|
-
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
477
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
441
478
|
if (!open) return null
|
|
442
479
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
443
480
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -457,7 +494,7 @@
|
|
|
457
494
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
458
495
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
459
496
|
})
|
|
460
|
-
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
497
|
+
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或在对话里说一句 ', React.createElement('code', { style: { fontSize: 11, background: C.nested, padding: '0 4px', borderRadius: 3 } }, 'task_create')) : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
461
498
|
}
|
|
462
499
|
return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, background: C.bg, border: '1px solid ' + C.border, borderRadius: '0 0 10px 10px', boxShadow: '0 8px 24px rgba(0,0,0,0.12)', display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
463
500
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0, flexWrap: 'wrap', gap: 4 } },
|
|
@@ -471,7 +508,9 @@
|
|
|
471
508
|
React.createElement(WorkModeSwitch, { mode: workMode }),
|
|
472
509
|
React.createElement('button', { onClick: function () { fetchTasks(); fetchChildren() }, title: '刷新', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 12, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('refresh-cw', 13)),
|
|
473
510
|
React.createElement('button', { onClick: function () { state.open = false; state.detailId = null; notify() }, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 14, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('x', 14)))),
|
|
474
|
-
React.createElement('div', { style: {
|
|
511
|
+
tasksErr ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '4px 12px', fontSize: 11, color: C.err, background: 'rgba(239,68,68,.08)', borderBottom: '1px solid ' + C.border, flexShrink: 0 }, title: '刷新失败不影响已显示数据,下次轮询成功自动恢复' }, ic('alert-triangle', 12), React.createElement('span', null, tasksErr)) : null,
|
|
512
|
+
// 空态统一(反馈 n-mut9rzoq3flu):空板不再整块替换为「🎉 暂无任务」单行,统一走 content 渲染六列骨架;引导 CTA 行在 content 内部按空板条件插入
|
|
513
|
+
React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, content),
|
|
475
514
|
createOpen ? React.createElement(CreateTaskForm, null) : null)
|
|
476
515
|
}
|
|
477
516
|
|
|
@@ -67,6 +67,32 @@
|
|
|
67
67
|
msg ? React.createElement('div', { style: { fontSize: 10, color: C.err, marginTop: 3 } }, msg) : null)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// 父子区块(与 DepsSection 并列):
|
|
71
|
+
// 子卡 = 父链面包屑「↳ 史诗:<父标题>」,点击回跳父卡详情;
|
|
72
|
+
// 父卡 = 非归档子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 resolved/total)。
|
|
73
|
+
// 数据源是 state.tasks 按 parentId 现算(parentId 为老字段),不依赖 host 的 childStats,天然缺省兼容;
|
|
74
|
+
// 既无父也无子的普通卡整块不渲染。嵌套史诗(本身也是子卡的父卡)两段同区块上下排列。
|
|
75
|
+
function FamilySection(props) {
|
|
76
|
+
var task = props.task
|
|
77
|
+
var parentT = task.parentId ? getTask(task.parentId) : null
|
|
78
|
+
var kids = state.tasks.filter(function (x) { return x.parentId === task.id && x.status !== 'archived' })
|
|
79
|
+
if (!task.parentId && kids.length === 0) return null
|
|
80
|
+
var resolvedN = kids.filter(function (x) { return x.status === 'resolved' }).length
|
|
81
|
+
function jump(id) { state.detailId = id; notify() }
|
|
82
|
+
return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
|
|
83
|
+
task.parentId ? React.createElement('div', { style: { fontSize: 11, marginBottom: kids.length ? 6 : 0 } },
|
|
84
|
+
React.createElement('span', { style: { color: C.text2, fontWeight: 600 } }, '↳ 史诗:'),
|
|
85
|
+
React.createElement('span', { onClick: function () { if (parentT) jump(task.parentId) }, title: parentT ? parentT.title + ' (' + task.parentId + ')' : task.parentId, style: { color: parentT ? C.brand : C.text2, cursor: parentT ? 'pointer' : 'default', fontWeight: 600 } }, parentT && parentT.title ? parentT.title : shortId(task.parentId))) : null,
|
|
86
|
+
kids.length > 0 ? React.createElement('div', null,
|
|
87
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + resolvedN + '/' + kids.length + ' 已完成'),
|
|
88
|
+
kids.map(function (x) {
|
|
89
|
+
return React.createElement('div', { key: x.id, onClick: function () { jump(x.id) }, title: (statusLabels[x.status] || x.status) + ' · ' + x.id, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 10, cursor: 'pointer' } },
|
|
90
|
+
React.createElement('span', { style: { width: 7, height: 7, borderRadius: '50%', background: statusColors[x.status] || C.text2, flexShrink: 0 } }),
|
|
91
|
+
React.createElement('span', { style: { color: C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
|
|
92
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
|
|
93
|
+
})) : null)
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
function DetailView() {
|
|
71
97
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
72
98
|
var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
|
|
@@ -102,6 +128,8 @@
|
|
|
102
128
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
103
129
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
104
130
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + (prioColor[task.priority] || prioColor.low) + ' 20%, transparent)', color: (prioColor[task.priority] || prioColor.low) } }, prioLabel[task.priority] || '中'), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
|
|
131
|
+
// 最近失败(host settle 失败路径写入 task.lastError;缺字段静默不渲染,≤2 行截断,悬停看全文)
|
|
132
|
+
task.lastError ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden', wordBreak: 'break-all' }, title: String(task.lastError) }, '最近失败: ' + String(task.lastError)) : null,
|
|
105
133
|
task.frozen ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.brand, borderRadius: 6, background: 'color-mix(in srgb, ' + C.brand + ' 8%, transparent)', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
106
134
|
React.createElement('span', { style: { fontWeight: 600 } }, '❄ 已冻结:不参与自动派发'),
|
|
107
135
|
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '裁决挂起' + (task.frozenAt ? '(' + ago(task.frozenAt) + ')' : '') + ',留出补 dependsOn/上下文的时间;解冻后重新入池'),
|
|
@@ -142,8 +170,25 @@
|
|
|
142
170
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
|
|
143
171
|
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
144
172
|
task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
// ===== 调研注入清单(调研门禁·详情侧,与卡片「⚠️ 无调研」徽章同口径)=====
|
|
174
|
+
// 一行汇总派发时注入给 Worker/Verifier 的调研上下文:files 数量 + basename 清单(悬停看完整路径)、
|
|
175
|
+
// notes 字数统计(悬停看原文,超 500 字截断标注);两者皆空 → 明示「无调研注入」。
|
|
176
|
+
// context 缺省兼容:老任务无 context 对象按空处理——同样亮出空态,这是预期(立单质量问责)。
|
|
177
|
+
// 行风格对齐上方「最近失败」行(border + color-mix 淡底):空态 warn 黄调,有内容走中性 nested。
|
|
178
|
+
(function () {
|
|
179
|
+
var cx = task.context || {}
|
|
180
|
+
var files = Array.isArray(cx.files) ? cx.files : []
|
|
181
|
+
var notes = typeof cx.notes === 'string' ? cx.notes : ''
|
|
182
|
+
var empty = files.length === 0 && notes.trim().length === 0
|
|
183
|
+
var parts = []
|
|
184
|
+
if (files.length) parts.push(files.length + ' 个文件(' + files.map(function (f) { return actBase(f) }).join('、') + ')')
|
|
185
|
+
if (notes.trim()) parts.push('调研笔记 ' + notes.length + ' 字')
|
|
186
|
+
var tip = empty ? '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes'
|
|
187
|
+
: (files.length ? '完整路径:\n' + files.join('\n') : '') + (notes.trim() ? (files.length ? '\n\n' : '') + '调研笔记原文:\n' + (notes.length > 500 ? notes.slice(0, 500) + '…(共 ' + notes.length + ' 字)' : notes) : '')
|
|
188
|
+
return React.createElement('div', { style: { fontSize: 11, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + (empty ? C.warn : C.border), borderRadius: 6, background: empty ? 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' : C.nested, color: empty ? C.warn : C.text2, wordBreak: 'break-word' }, title: tip },
|
|
189
|
+
'📎 调研注入: ' + (empty ? '无调研注入——Worker 需自行定位,建议补 contextFiles/contextNotes' : parts.join(' · ')))
|
|
190
|
+
})(),
|
|
191
|
+
React.createElement(FamilySection, { task: task }),
|
|
147
192
|
React.createElement(DepsSection, { task: task }),
|
|
148
193
|
task.acceptance ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.ok, fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: 4 } }, ic('flask-conical', 11), '硬性验收: ' + task.acceptance) : null,
|
|
149
194
|
task.resolution ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.warn, maxHeight: 120, overflowY: 'auto', whiteSpace: 'pre-wrap' } }, '📝 ' + task.resolution) : null,
|