dsh-agent-board 1.3.0 → 1.4.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 +24 -14
- package/index.mjs +170 -43
- package/lib/client.js +162 -16
- package/lib/core.mjs +110 -7
- package/package.json +1 -1
- package/test/core.test.mjs +160 -0
package/lib/client.js
CHANGED
|
@@ -25,6 +25,8 @@ function apply(ctx) {
|
|
|
25
25
|
var prioLabel = { critical: '紧急', high: '高', medium: '中', low: '低' }
|
|
26
26
|
var statusLabels = { draft: '草稿', pending: '待办', 'in-progress': '进行中', verifying: '验证中', resolved: '已完成', blocked: '阻塞', cancelled: '已取消', archived: '已归档' }
|
|
27
27
|
var statusColors = { draft: C.text2, pending: C.text2, 'in-progress': C.brand, verifying: C.warn, resolved: C.ok, blocked: C.err, archived: C.text2 }
|
|
28
|
+
// 工作模式三档(v75 收敛):内部仍存 boardMode/teamMode 两个 flag,UI 只呈现一维三档
|
|
29
|
+
var workModeNames = { list: '清单模式', auto: '自动派发', team: 'Team 托管' }
|
|
28
30
|
// ===== 图标收口(docs/icon-style-guide.md:Lucide 线性 SVG,currentColor 跟随主题)=====
|
|
29
31
|
// ICONS 存 [tag, attrs] 数组,用 createElement 逐个渲染(避免 dangerouslySetInnerHTML 受限)
|
|
30
32
|
var ICONS = {
|
|
@@ -187,6 +189,10 @@ function apply(ctx) {
|
|
|
187
189
|
'stop-circle': [
|
|
188
190
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
189
191
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
192
|
+
],
|
|
193
|
+
'plus': [
|
|
194
|
+
['path', { d: 'M5 12h14' }],
|
|
195
|
+
['path', { d: 'M12 5v14' }]
|
|
190
196
|
]
|
|
191
197
|
}
|
|
192
198
|
// 图标+文字内联组合的快捷渲染
|
|
@@ -203,7 +209,7 @@ function apply(ctx) {
|
|
|
203
209
|
}
|
|
204
210
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
205
211
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
206
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, 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, archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [] }
|
|
212
|
+
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, 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, archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '' }
|
|
207
213
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
208
214
|
try {
|
|
209
215
|
var onKey = function (e) {
|
|
@@ -211,7 +217,8 @@ function apply(ctx) {
|
|
|
211
217
|
var ae = document.activeElement
|
|
212
218
|
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT')) return
|
|
213
219
|
if (!state.open) return
|
|
214
|
-
if (state.
|
|
220
|
+
if (state.createOpen) { state.createOpen = false; notify() }
|
|
221
|
+
else if (state.detailId) { state.detailId = null; notify() }
|
|
215
222
|
else if (state.selectMode) { state.selectMode = false; state.selected = {}; notify() }
|
|
216
223
|
else { state.open = false; notify() }
|
|
217
224
|
e.stopPropagation()
|
|
@@ -232,6 +239,9 @@ function apply(ctx) {
|
|
|
232
239
|
state.tasks = (d && d.tasks) || []
|
|
233
240
|
state.boardMode = (d && d.boardMode) || 'auto'
|
|
234
241
|
state.teamMode = !!(d && d.teamMode)
|
|
242
|
+
// 工作模式(三档)读取:优先用服务端派生字段 workMode,缺失时按同口径本地兜底派生
|
|
243
|
+
// (兼容老 host:老版本 get-tasks 不返回 workMode)
|
|
244
|
+
state.workMode = (d && d.workMode) || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))
|
|
235
245
|
state.minWorkers = (d && d.minWorkers) || 1
|
|
236
246
|
state.maxWorkers = (d && d.maxWorkers) || 3
|
|
237
247
|
state.minVerifiers = (d && d.minVerifiers) || 0
|
|
@@ -298,10 +308,17 @@ function apply(ctx) {
|
|
|
298
308
|
// 去抖:切会话时 body 子树 style 大面积变动会触发回调风暴,300ms 合并一次
|
|
299
309
|
var layoutTimer = null
|
|
300
310
|
function syncLayoutDebounced() { if (layoutTimer) return; layoutTimer = setTimeout(function () { layoutTimer = null; syncLayout() }, 300) }
|
|
301
|
-
ctx.effect(function () { return function () { if (layoutTimer) { clearTimeout(layoutTimer); layoutTimer = null } } })
|
|
311
|
+
ctx.effect(function () { return function () { if (layoutTimer) { clearTimeout(layoutTimer); layoutTimer = null } if (createFlashTimer) { clearTimeout(createFlashTimer); createFlashTimer = null } } })
|
|
302
312
|
try { var ro = new ResizeObserver(function () { syncLayoutDebounced() }); ro.observe(document.body); ctx.effect(function () { return function () { ro.disconnect() } }) } catch (_) {}
|
|
303
313
|
try { var mo = new MutationObserver(function () { syncLayoutDebounced() }); mo.observe(document.body, { attributes: true, attributeFilter: ['style'], subtree: true }); ctx.effect(function () { return function () { mo.disconnect() } }) } catch (_) {}
|
|
304
314
|
syncLayout()
|
|
315
|
+
// 创建任务后的轻提示(头部短暂显示"✅ 已存草稿/已创建任务",2.5s 后自动清空;不引入 toast 体系)
|
|
316
|
+
var createFlashTimer = null
|
|
317
|
+
function flashCreated(msg) {
|
|
318
|
+
state.createFlash = msg
|
|
319
|
+
if (createFlashTimer) clearTimeout(createFlashTimer)
|
|
320
|
+
createFlashTimer = setTimeout(function () { createFlashTimer = null; state.createFlash = ''; notify() }, 2500)
|
|
321
|
+
}
|
|
305
322
|
function ago(iso) { if (!iso) return ''; var ms = Date.now() - new Date(iso).getTime(); if (ms < 60000) return '刚刚'; if (ms < 3600000) return Math.floor(ms / 60000) + ' 分钟前'; if (ms < 86400000) return Math.floor(ms / 3600000) + ' 小时前'; return Math.floor(ms / 86400000) + ' 天前' }
|
|
306
323
|
function fmtTime(iso) { if (!iso) return '-'; try { return new Date(iso).toLocaleString() } catch (_) { return iso } }
|
|
307
324
|
function getTask(id) { return state.tasks.find(function (t) { return t.id === id }) }
|
|
@@ -424,7 +441,7 @@ function apply(ctx) {
|
|
|
424
441
|
var isSelf = b.session === state.sessionId
|
|
425
442
|
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, padding: '3px 0', borderBottom: '1px solid ' + C.nested } },
|
|
426
443
|
React.createElement('span', { onClick: function () { if (!isSelf && uiWorkspaceSvc) uiWorkspaceSvc.openSession(b.session) }, style: { color: isSelf ? C.text2 : C.brand, cursor: isSelf ? 'default' : 'pointer', textDecoration: isSelf ? 'none' : 'underline', minWidth: 90 } }, (isSelf ? '★ ' : '') + shortId(b.session)),
|
|
427
|
-
React.createElement('span', { style: { color: C.text2 } }, b.teamMode ? '
|
|
444
|
+
React.createElement('span', { style: { color: C.text2 } }, workModeNames[b.teamMode ? 'team' : (b.boardMode === 'auto' ? 'auto' : 'list')]),
|
|
428
445
|
React.createElement('span', { style: { display: 'inline-flex', gap: 5 } },
|
|
429
446
|
b.counts.inProgress ? React.createElement('span', { style: { color: C.brand } }, '▶' + b.counts.inProgress) : null,
|
|
430
447
|
b.counts.verifying ? React.createElement('span', { style: { color: C.warn } }, '验' + b.counts.verifying) : null,
|
|
@@ -436,7 +453,7 @@ function apply(ctx) {
|
|
|
436
453
|
})) : null)
|
|
437
454
|
}
|
|
438
455
|
function buildReport() {
|
|
439
|
-
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '
|
|
456
|
+
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '工作模式: ' + (workModeNames[state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))] || '自动派发'), '', '统计范围: ' + rangeLabel()]
|
|
440
457
|
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
441
458
|
var rg = activeRange()
|
|
442
459
|
state.tasks.forEach(function (t) {
|
|
@@ -486,8 +503,28 @@ function apply(ctx) {
|
|
|
486
503
|
if (!isRoot) return null // 子代理会话不显示看板入口
|
|
487
504
|
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)
|
|
488
505
|
}
|
|
489
|
-
|
|
490
|
-
|
|
506
|
+
// 工作模式选择器(v75):boardMode/teamMode 双开关收敛为一维三档,点击走 set-work-mode 单入口。
|
|
507
|
+
// 老 RPC(set-board-mode/set-team-mode)在服务端保留兼容,UI 不再使用。
|
|
508
|
+
function WorkModeSwitch(props) {
|
|
509
|
+
var cur = props.mode || 'auto'
|
|
510
|
+
function pick(m) { if (m === cur) return; rpc('set-work-mode', { mode: m }).then(fetchTasks).catch(function () {}) }
|
|
511
|
+
// 档位文案:名称 + 一句说明(说明常驻显示在按钮内,选中态高亮不可用 title 才看得到)
|
|
512
|
+
var opts = [
|
|
513
|
+
{ k: 'list', name: '清单模式', desc: '看板当 TODO 列表', icon: 'clipboard-list' },
|
|
514
|
+
{ k: 'auto', name: '自动派发', desc: '即建即派给 Worker', icon: 'zap' },
|
|
515
|
+
{ k: 'team', name: 'Team 托管', desc: '草稿起步,主窗口裁决', icon: 'users' }
|
|
516
|
+
]
|
|
517
|
+
var btnBase = { border: 'none', cursor: 'pointer', fontWeight: 500, textAlign: 'center', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 1, padding: '3px 9px', transition: 'all .15s', lineHeight: 1.25 }
|
|
518
|
+
return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier 验收、touches 排他在三档下全部生效', style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, opts.map(function (o) {
|
|
519
|
+
var on = cur === o.k
|
|
520
|
+
return React.createElement('button', {
|
|
521
|
+
key: o.k, onClick: function () { pick(o.k) }, title: o.name + ':' + o.desc,
|
|
522
|
+
style: Object.assign({}, btnBase, on ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 })
|
|
523
|
+
},
|
|
524
|
+
React.createElement('span', { style: { fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic(o.icon, 11), o.name),
|
|
525
|
+
React.createElement('span', { style: { fontSize: 9, opacity: on ? 0.85 : 0.7, whiteSpace: 'nowrap' } }, o.desc))
|
|
526
|
+
}))
|
|
527
|
+
}
|
|
491
528
|
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '团队')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
492
529
|
function PoolCfg(props) { var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]; var _b = useState(false), dirty = _b[0], setDirty = _b[1]; useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value]); function commit(v) { var n = parseInt(v, 10); if (!isNaN(n) && n >= 0 && n <= 10) { setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {}) } } function step(d) { var n = parseInt(val, 10) || 0; commit(String(Math.max(0, Math.min(10, n + d)))) } var miniBtn = { fontSize: 9, padding: '0px 4px', border: '1px solid ' + C.border2, borderRadius: 2, background: C.nested, color: C.text2, cursor: 'pointer', lineHeight: '14px' }; return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 1, fontSize: 9, color: C.text2 } }, props.label, React.createElement('button', { onClick: function () { step(-1) }, title: '减 1', style: miniBtn }, '−'), React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) }, onBlur: function () { if (dirty) commit(val) }, onKeyDown: function (e) { if (e.key === 'Enter') commit(val) }, style: { width: 22, padding: '0px 2px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }), React.createElement('button', { onClick: function () { step(1) }, title: '加 1', style: miniBtn }, '+'), dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null) }
|
|
493
530
|
function MinCfg(props) {
|
|
@@ -643,6 +680,8 @@ function apply(ctx) {
|
|
|
643
680
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
644
681
|
}
|
|
645
682
|
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); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, 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] || '中')), 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, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2 } }, '⛓ 被 ' + t.dependsOn.filter(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }).length + ' 个依赖阻塞') : 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' } },
|
|
683
|
+
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
684
|
+
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
646
685
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
647
686
|
(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,
|
|
648
687
|
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
@@ -770,24 +809,122 @@ function apply(ctx) {
|
|
|
770
809
|
candidates.map(function (x) { return React.createElement('option', { key: x.id, value: x.id }, x.title.slice(0, 36) + ' (' + (statusLabels[x.status] || x.status) + ')') })) : null,
|
|
771
810
|
msg ? React.createElement('div', { style: { fontSize: 10, color: C.err, marginTop: 3 } }, msg) : null)
|
|
772
811
|
}
|
|
812
|
+
// ===== GUI 创建任务:+新建任务 表单弹层(提交走 create-task RPC,Team 托管默认存草稿)=====
|
|
813
|
+
function CreateTaskForm() {
|
|
814
|
+
var _R = React; var useState = _R.useState
|
|
815
|
+
// 草稿默认值:workMode 优先,缺失时按 boardMode/teamMode 本地兜底派生(与 fetchTasks 同口径)
|
|
816
|
+
var wm = state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))
|
|
817
|
+
var _t = useState(''), title = _t[0], setTitle = _t[1]
|
|
818
|
+
var _d = useState(''), desc = _d[0], setDesc = _d[1]
|
|
819
|
+
var _p = useState('medium'), prio = _p[0], setPrio = _p[1]
|
|
820
|
+
var _pl = useState('full'), pipe = _pl[0], setPipe = _pl[1]
|
|
821
|
+
var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
|
|
822
|
+
var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
|
|
823
|
+
var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
|
|
824
|
+
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
825
|
+
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
826
|
+
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
827
|
+
// 依赖候选:当前看板 status 为 draft/pending 的任务
|
|
828
|
+
var cands = state.tasks.filter(function (t) { return t.status === 'draft' || t.status === 'pending' })
|
|
829
|
+
function close() { state.createOpen = false; notify() }
|
|
830
|
+
function toggleDep(id) { var next = Object.assign({}, depSel); if (next[id]) delete next[id]; else next[id] = true; setDepSel(next) }
|
|
831
|
+
function submit() {
|
|
832
|
+
if (busy) return
|
|
833
|
+
var t = title.trim()
|
|
834
|
+
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
835
|
+
var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
836
|
+
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
837
|
+
setBusy(true); setErr('')
|
|
838
|
+
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
839
|
+
setBusy(false)
|
|
840
|
+
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
841
|
+
state.createOpen = false
|
|
842
|
+
flashCreated(asDraft ? '✅ 已存草稿' : '✅ 已创建任务')
|
|
843
|
+
fetchTasks()
|
|
844
|
+
notify()
|
|
845
|
+
}).catch(function (e) { setBusy(false); setErr('⚠️ ' + String(e)) })
|
|
846
|
+
}
|
|
847
|
+
var inp = { width: '100%', boxSizing: 'border-box', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit' }
|
|
848
|
+
var lblStyle = { fontSize: 10, color: C.text2, marginBottom: 3 }
|
|
849
|
+
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
850
|
+
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
851
|
+
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 }
|
|
852
|
+
return React.createElement('div', { style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.35)', zIndex: 20, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: 16, overflowY: 'auto' }, onClick: function (e) { if (e.target === e.currentTarget) close() } },
|
|
853
|
+
React.createElement('div', { style: { width: 440, maxWidth: '100%', background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.28)', padding: 12 } },
|
|
854
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
|
|
855
|
+
React.createElement('span', { style: { fontSize: 13, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('plus', 14), '新建任务'),
|
|
856
|
+
React.createElement('button', { onClick: close, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', color: C.text2, display: 'inline-flex' } }, ic('x', 14))),
|
|
857
|
+
field('标题 *', React.createElement('input', { value: title, onChange: function (e) { setTitle(e.target.value) }, placeholder: '一句话说清要做什么', style: inp })),
|
|
858
|
+
field('描述', React.createElement('textarea', { value: desc, onChange: function (e) { setDesc(e.target.value) }, rows: 3, placeholder: '背景 / 目标 / 约束(可空)', style: Object.assign({}, inp, { resize: 'vertical', minHeight: 46 }) })),
|
|
859
|
+
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
860
|
+
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('优先级', React.createElement('select', { value: prio, onChange: function (e) { setPrio(e.target.value) }, style: inp }, ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('option', { key: p, value: p }, prioLabel[p] + '(' + p + ')') })))),
|
|
861
|
+
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
|
|
862
|
+
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
863
|
+
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
864
|
+
React.createElement('option', { value: 'direct' }, '主窗口处理'))))),
|
|
865
|
+
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp })),
|
|
866
|
+
field('依赖(全部完成后才派发,可空)', cands.length === 0
|
|
867
|
+
? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
|
|
868
|
+
: React.createElement('div', { style: { maxHeight: 96, overflowY: 'auto', border: '1px solid ' + C.border, borderRadius: 4, padding: '4px 6px', background: C.card } }, cands.map(function (x) {
|
|
869
|
+
return React.createElement('label', { key: x.id, style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, padding: '1px 0', cursor: 'pointer' } },
|
|
870
|
+
React.createElement('input', { type: 'checkbox', checked: !!depSel[x.id], onChange: function () { toggleDep(x.id) } }),
|
|
871
|
+
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (statusLabels[x.status] || x.status) + ' · ' + (x.title || x.id)))
|
|
872
|
+
}))),
|
|
873
|
+
field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) })),
|
|
874
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
875
|
+
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
876
|
+
'存为草稿',
|
|
877
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)')),
|
|
878
|
+
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
879
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
880
|
+
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
881
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发')))))
|
|
882
|
+
}
|
|
773
883
|
function DetailView() {
|
|
774
884
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
775
885
|
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]
|
|
776
|
-
var _e2 = useState(''), arbAnswer = _e2[0], setArbAnswer = _e2[1]; var _f2 = useState(''), interveneMsg = _f2[0], setInterveneMsg = _f2[1]; var _g2 = useState(''), actionMsg = _g2[0], setActionMsg = _g2[1]
|
|
886
|
+
var _e2 = useState(''), arbAnswer = _e2[0], setArbAnswer = _e2[1]; var _f2 = useState(''), interveneMsg = _f2[0], setInterveneMsg = _f2[1]; var _g2 = useState(''), actionMsg = _g2[0], setActionMsg = _g2[1]; var _h2 = useState('resume'), arbAction = _h2[0], setArbAction = _h2[1]
|
|
777
887
|
useEffect(function () { function update() { setMode(state.boardMode) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
778
888
|
if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
|
|
779
889
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
780
890
|
function saveEdit() { setSaving(true); rpc('update-task', { taskId: task.id, title: editTitle, description: editDesc, resetToPending: true }).then(function () { setSaving(false); fetchTasks() }).catch(function () { setSaving(false) }) }
|
|
781
891
|
function jumpToAgent() { if (uiWorkspaceSvc && task.claimedBy) uiWorkspaceSvc.openSession(task.claimedBy) }
|
|
782
|
-
function submitArbitration() { if (!arbAnswer.trim()) return; rpc('resolve-escalation', { taskId: task.id, answer: arbAnswer }).then(function (r) { setActionMsg(r && r.ok ? '✅
|
|
892
|
+
function submitArbitration() { if (!arbAnswer.trim()) return; var act = arbAction === 'hold' ? 'hold' : 'resume'; rpc('resolve-escalation', { taskId: task.id, answer: arbAnswer, action: act }).then(function (r) { setActionMsg(r && r.ok ? (act === 'hold' ? '✅ 裁决已记录,任务已挂起冻结(❄ 不参与自动派发)' : '✅ 裁决已转达,任务重新排队派发') : '⚠️ ' + ((r && r.error) || '失败')); setArbAnswer(''); setArbAction('resume'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
893
|
+
function doUnfreeze() { rpc('unfreeze-task', { taskId: task.id }).then(function (r) { setActionMsg(r && r.ok ? '✅ 已解除冻结,重新进入派发池' : '⚠️ ' + ((r && r.error) || '失败')); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
783
894
|
function doTerminate() { rpc('terminate-agent', { taskId: task.id }).then(function (r) { setActionMsg(r && r.ok ? '⏹ 已终止 ' + (r.terminated || '') + ',任务重新排队' : '⚠️ ' + ((r && r.error) || '无活动 Agent')); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
784
895
|
function doDismiss() { rpc('dismiss-suspect', { taskId: task.id }).then(function () { setActionMsg('✅ 已清除卡死标记,继续观察'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
896
|
+
// 手动派发(touches 文件级排他):host 默认尊重文件锁,冲突时返回 touches-conflict;
|
|
897
|
+
// 这里 confirm 列出"在等谁"后带 force:true 重发(人工越权通道:明知会并行改同一批文件)。
|
|
898
|
+
// force 只在用户明确确认时加——默认调用形态与旧版一致(不带 force)。
|
|
899
|
+
function doManualDispatch(role) {
|
|
900
|
+
function send(force) { var a = { taskId: task.id, role: role }; if (force) a.force = true; return rpc('dispatch-task', a) }
|
|
901
|
+
return send(false).then(function (r) {
|
|
902
|
+
if (r && r.ok === false && r.error === 'touches-conflict') {
|
|
903
|
+
var ids = Array.isArray(r.conflicts) ? r.conflicts : []
|
|
904
|
+
var names = ids.map(function (id) { var t2 = getTask(id); return (t2 ? t2.title : id) + ' (' + id + ')' }).join('、')
|
|
905
|
+
if (window.confirm('⚠️ 文件锁冲突:以下进行中任务正在改同一批文件(touches 重叠):\n\n' + (names || ids.join('、')) + '\n\n强行并行可能互相覆盖改动/diff 冲突。仍要越权派发吗?')) return send(true)
|
|
906
|
+
setActionMsg('⛔ 已取消派发(等文件锁释放,或调整 touches 声明)')
|
|
907
|
+
return { ok: false, cancelled: true }
|
|
908
|
+
}
|
|
909
|
+
return r
|
|
910
|
+
})
|
|
911
|
+
}
|
|
785
912
|
function submitIntervene() { if (!interveneMsg.trim()) return; rpc('intervene-agent', { taskId: task.id, message: interveneMsg }).then(function (r) { setActionMsg(r && r.ok ? '✅ 高优指令已插入 ' + shortId(r.agent) + ' 队首' : '⚠️ ' + ((r && r.error) || '无活动 Agent')); setInterveneMsg(''); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
786
913
|
var isManual = task.assignMode === 'manual'; var canJump = (task.status === 'in-progress' || task.status === 'verifying') && task.claimedBy && task.claimedBy !== state.sessionId
|
|
787
914
|
var canIntervene = task.status === 'in-progress' || task.status === 'verifying'
|
|
788
915
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
789
916
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
790
917
|
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),
|
|
918
|
+
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' } },
|
|
919
|
+
React.createElement('span', { style: { fontWeight: 600 } }, '❄ 已冻结:不参与自动派发'),
|
|
920
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '裁决挂起' + (task.frozenAt ? '(' + ago(task.frozenAt) + ')' : '') + ',留出补 dependsOn/上下文的时间;解冻后重新入池'),
|
|
921
|
+
React.createElement('button', { onClick: doUnfreeze, title: '清除冻结标记并触发派发', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer', fontWeight: 600, marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('refresh-cw', 10), '解除冻结')) : null,
|
|
922
|
+
(Array.isArray(task.waitingForTouches) && task.waitingForTouches.length) ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
|
923
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: 2 } }, '🔒 等文件锁释放:暂不自动派发'),
|
|
924
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, '本任务声明的 touches 与以下进行中任务重叠(同一批文件并行改会互踩);它们提交验收/完成后自动派发。'),
|
|
925
|
+
React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
|
|
926
|
+
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
|
|
927
|
+
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
|
|
791
928
|
(task.status === 'in-progress' || task.status === 'verifying') && state.activity[task.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginBottom: 8, padding: '4px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 4, animation: 'tskb-pulse 2s ease-in-out infinite' } }, ic('activity', 10), React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '当前动作: ' + state.activity[task.id])) : null,
|
|
792
929
|
task.status === 'in-progress' && task.claimedBy ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, padding: '5px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
793
930
|
ic('activity', 10),
|
|
@@ -799,7 +936,12 @@ function apply(ctx) {
|
|
|
799
936
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
800
937
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '上报于 ' + ago(task.escalation.at) + ' · ' + (task.escalation.by || '')),
|
|
801
938
|
React.createElement('textarea', { value: arbAnswer, onChange: function (e) { setArbAnswer(e.target.value) }, rows: 2, placeholder: '输入裁决指示,将直接转达给原 Worker(保有上下文)…', style: { width: '100%', fontSize: 11, padding: '5px 8px', marginBottom: 4, border: '1px solid ' + C.err, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
802
|
-
React.createElement('
|
|
939
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
940
|
+
React.createElement('select', { value: arbAction, onChange: function (e) { setArbAction(e.target.value) }, title: '裁决后任务去向:重新派发=立即回派发池;挂起冻结=不参与自动派发,留出补 dependsOn/上下文的时间', style: { fontSize: 10, padding: '4px 4px', border: '1px solid ' + C.err, borderRadius: 4, background: C.card, color: C.text } },
|
|
941
|
+
React.createElement('option', { value: 'resume' }, '裁决后:重新派发'),
|
|
942
|
+
React.createElement('option', { value: 'hold' }, '裁决后:挂起冻结 ❄')),
|
|
943
|
+
React.createElement('button', { onClick: submitArbitration, disabled: !arbAnswer.trim(), style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('scale', 12), '提交裁决'),
|
|
944
|
+
arbAction === 'hold' ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '冻结后不参与自动派发,补完上下文可在详情页「解除冻结」放行') : null)) : null,
|
|
803
945
|
actionMsg ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6 } }, actionMsg) : null,
|
|
804
946
|
task.stuckSince ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
|
805
947
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡死'),
|
|
@@ -833,8 +975,8 @@ function apply(ctx) {
|
|
|
833
975
|
React.createElement('button', { onClick: saveEdit, disabled: saving, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), ' 保存并重置']),
|
|
834
976
|
task.status === 'draft' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('update-task', { taskId: task.id, publish: true }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('rocket', 10), '发布(进入派发池)') : null,
|
|
835
977
|
(task.status === 'pending' || task.status === 'blocked') ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('claim-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer' } }, '领取') : null,
|
|
836
|
-
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return
|
|
837
|
-
task.status === 'verifying' && !task.escalation ? React.createElement('button', { onClick: function () { doAction(function () { return
|
|
978
|
+
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return doManualDispatch('worker') }) }, title: '手动派发给一次性 Worker 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发') : null,
|
|
979
|
+
task.status === 'verifying' && !task.escalation ? React.createElement('button', { onClick: function () { doAction(function () { return doManualDispatch('verifier') }) }, title: '手动派发给一次性 Verifier 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发验收') : null,
|
|
838
980
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
839
981
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
840
982
|
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
|
|
@@ -870,8 +1012,10 @@ function apply(ctx) {
|
|
|
870
1012
|
var _a = useState(state.open), open = _a[0], setOpen = _a[1]; var _b = useState(state.tasks), tasks = _b[0], setTasksState = _b[1]; var _c = useState(state.boardMode), mode = _c[0], setModeState = _c[1]; var _d = useState(state.detailId), detailId = _d[0], setDetailId = _d[1]; var _f = useState(state.dragOver), dragOver = _f[0], setDragOver = _f[1]; var _g = useState(state.dispatchInfo), dispatchInfo = _g[0], setDispatchInfo = _g[1]; var _j = useState(state.view), view = _j[0], setViewState = _j[1]; var _k = useState(state.layoutLeft), layL = _k[0], setLayL = _k[1]; var _l = useState(state.layoutRight), layR = _l[0], setLayR = _l[1]
|
|
871
1013
|
var _m = useState(state.minWorkers), minW = _m[0], setMinW = _m[1]; var _n = useState(state.maxWorkers), maxW = _n[0], setMaxW = _n[1]; var _o = useState(state.minVerifiers), minV = _o[0], setMinV = _o[1]; var _p = useState(state.maxVerifiers), maxV = _p[0], setMaxV = _p[1]; var _wm = useState(state.workerModel), workerModel = _wm[0], setWorkerModel = _wm[1]; var _vm = useState(state.verifierModel), verifierModel = _vm[0], setVerifierModel = _vm[1]
|
|
872
1014
|
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
1015
|
+
var _wmo = useState(state.workMode), workMode = _wmo[0], setWorkModeState = _wmo[1]
|
|
1016
|
+
var _cro = useState(state.createOpen), createOpen = _cro[0], setCreateOpen = _cro[1]; var _crf = useState(state.createFlash), createFlash = _crf[0], setCreateFlash = _crf[1]
|
|
873
1017
|
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
874
|
-
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); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
1018
|
+
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); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
875
1019
|
if (!open) return null
|
|
876
1020
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
877
1021
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -899,12 +1043,14 @@ function apply(ctx) {
|
|
|
899
1043
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
900
1044
|
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT }),
|
|
901
1045
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
1046
|
+
React.createElement('button', { onClick: function () { state.createOpen = true; notify() }, title: '新建任务(可存为草稿)', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer', background: 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('plus', 11), '新建任务'),
|
|
1047
|
+
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
902
1048
|
React.createElement('button', { onClick: function () { state.selectMode = !state.selectMode; if (!state.selectMode) state.selected = {}; notify() }, title: '多选批量操作', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + (state.selectMode ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: state.selectMode ? C.brand : 'transparent', color: state.selectMode ? C_INV : C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('square-check-big', 11), '多选'),
|
|
903
|
-
React.createElement(
|
|
904
|
-
React.createElement(ModeSwitch, { mode: mode, teamOn: teamMode }),
|
|
1049
|
+
React.createElement(WorkModeSwitch, { mode: workMode }),
|
|
905
1050
|
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)),
|
|
906
1051
|
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)))),
|
|
907
|
-
React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 24, color: C.text2, fontSize: 12 } }, '🎉 暂无任务') : content)
|
|
1052
|
+
React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 24, color: C.text2, fontSize: 12 } }, '🎉 暂无任务') : content),
|
|
1053
|
+
createOpen ? React.createElement(CreateTaskForm, null) : null)
|
|
908
1054
|
}
|
|
909
1055
|
slots.inject('conversation.session.header.actions', function () { return slots.register({ name: 'conversation.session.header.actions', id: 'task-board-btn', label: '任务看板', order: 30 }, function (props) { return React.createElement(BoardButton, props) }) })
|
|
910
1056
|
slots.inject('shell.overlay', function () { return slots.register({ name: 'shell.overlay', id: 'task-board-top-panel' }, function () { return React.createElement(TopPanel) }) })
|
package/lib/core.mjs
CHANGED
|
@@ -14,6 +14,54 @@ export function gsb(p, a) { return a.filter(function (x) { return x.parentId ===
|
|
|
14
14
|
export function gpt(t, a) { return isb(t) ? a.find(function (x) { return x.id === t.parentId }) : undefined }
|
|
15
15
|
export function vt(d) { return d && typeof d === 'object' && Array.isArray(d.tasks) }
|
|
16
16
|
|
|
17
|
+
// ===== touches 文件级排他(glob 最小匹配器,零依赖)=====
|
|
18
|
+
// 背景:并行 Worker 改同一批文件会互踩(diff 冲突/一方覆盖另一方)。任务可声明
|
|
19
|
+
// touches: string[](glob),派发器发现与「活动(in-progress)任务的 touches」冲突
|
|
20
|
+
// 则本轮跳过该候选(记入 pickDispatch 返回的 blockedTouches),等锁释放再派。
|
|
21
|
+
// 语义(宁可偏严不可漏拦):
|
|
22
|
+
// 1. 先归一化:\ → /、去 './' 前缀、去尾部 '/';空串或非字符串忽略。
|
|
23
|
+
// 2. a === b 视为冲突。
|
|
24
|
+
// 3. 'dir/**' 目录前缀覆盖(含两侧都 /** 时前缀互相包含);`*` 只当通配符处理,
|
|
25
|
+
// 不做通用 glob 展开——最小实现,够用且不会误判。
|
|
26
|
+
// 4. '*.ext' 后缀:两侧都是 '*.ext' 比扩展名;一侧是具体路径看 endsWith('/'+glob)
|
|
27
|
+
// 或本身 endsWith。
|
|
28
|
+
// 5. 裸文件名(无 '/' 且非 '*.ext')只在「另一侧也是裸名」时比 basename 相等;
|
|
29
|
+
// 含 '/' 的具体路径不参与裸名规则(否则 'a/core.mjs' 会误撞裸名 'core.mjs')。
|
|
30
|
+
// 保守倾向:不确定即算冲突(误拦只是晚一轮派发,漏拦会让两个 Worker 互踩)。
|
|
31
|
+
function normTouch(s) {
|
|
32
|
+
var v = String(s).replace(/\\/g, '/').trim()
|
|
33
|
+
while (v.slice(0, 2) === './') v = v.slice(2)
|
|
34
|
+
while (v.length > 1 && v.charAt(v.length - 1) === '/') v = v.slice(0, -1)
|
|
35
|
+
return v
|
|
36
|
+
}
|
|
37
|
+
function baseOf(p) { var i = p.lastIndexOf('/'); return i < 0 ? p : p.slice(i + 1) }
|
|
38
|
+
function isStarDot(p) { return p.length > 2 && p.indexOf('*') === 0 && p.charAt(1) === '.' && p.indexOf('/') < 0 }
|
|
39
|
+
function dirLockPrefix(p) { return p.length > 3 && p.slice(-3) === '/**' ? p.slice(0, -2) : '' }
|
|
40
|
+
function matchOne(pattern, subject) {
|
|
41
|
+
var p = normTouch(pattern), s = normTouch(subject)
|
|
42
|
+
if (!p || !s) return false
|
|
43
|
+
if (p === s) return true
|
|
44
|
+
// 目录锁:'dir/**' 覆盖 dir 下的任意路径(两个方向都试,任一侧声明目录锁即锁定整棵子树)
|
|
45
|
+
var pfx = dirLockPrefix(p); if (pfx && s.indexOf(pfx) === 0) return true
|
|
46
|
+
var sfx = dirLockPrefix(s); if (sfx && p.indexOf(sfx) === 0) return true
|
|
47
|
+
var ps = isStarDot(p), ss = isStarDot(s)
|
|
48
|
+
if (ps && ss) return p === s // 两侧都是通配扩展名:只有完全相同才算(*.js vs *.ts 不冲突)
|
|
49
|
+
if (ps) return s.length > p.length - 1 && s.slice(-(p.length - 1)) === p.slice(1)
|
|
50
|
+
if (ss) return p.length > s.length - 1 && p.slice(-(s.length - 1)) === s.slice(1)
|
|
51
|
+
// 含 '/' 的具体路径:一侧是另一侧目录下的文件即冲突(src/lib 覆盖 src/lib/core.mjs)
|
|
52
|
+
if (p.indexOf('/') >= 0 || s.indexOf('/') >= 0) return s.indexOf(p + '/') === 0 || p.indexOf(s + '/') === 0
|
|
53
|
+
return baseOf(p) === baseOf(s)
|
|
54
|
+
}
|
|
55
|
+
// 两组 glob 是否可能命中同一批文件(对称判定)
|
|
56
|
+
export function patOverlap(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return false; return matchOne(a, b) || matchOne(b, a) }
|
|
57
|
+
// 已声明 touches 的任务集合是否与本任务声明的 touches 冲突
|
|
58
|
+
export function overlapsTouches(mine, theirs) {
|
|
59
|
+
var m = Array.isArray(mine) ? mine : []
|
|
60
|
+
var th = Array.isArray(theirs) ? theirs : []
|
|
61
|
+
for (var i = 0; i < m.length; i++) { for (var j = 0; j < th.length; j++) { if (patOverlap(m[i], th[j])) return true } }
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
// ===== 依赖校验(#18)=====
|
|
18
66
|
// 存在性 + 自引用 + DFS 环检测;返回错误消息或 null
|
|
19
67
|
export function validateDeps(d, taskId, deps) {
|
|
@@ -70,14 +118,27 @@ export function cfg(d) {
|
|
|
70
118
|
// 缺字段 = undefined → 工具结果的 lossless-JSON 校验会拒("value is not lossless JSON")
|
|
71
119
|
export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, poolStatus: { workers: [], verifiers: [] }, tasks: [] } }
|
|
72
120
|
// 旧文件缺 poolStatus 的归一化(读路径兜底,保证任何历史文件都满足工具输出契约)
|
|
73
|
-
|
|
121
|
+
// touches 兼容:老任务没有该字段照常(这里只把「存在但非数组」的脏值收敛成数组,
|
|
122
|
+
// 避免 holdsFiles/touchesConflict 里 Array.isArray 判定之外还有第三种形态)
|
|
123
|
+
export function normalizeBoard(d) {
|
|
124
|
+
if (d && typeof d === 'object') {
|
|
125
|
+
if (!d.poolStatus || typeof d.poolStatus !== 'object' || !Array.isArray(d.poolStatus.workers) || !Array.isArray(d.poolStatus.verifiers)) d.poolStatus = { workers: [], verifiers: [] }
|
|
126
|
+
if (Array.isArray(d.tasks)) {
|
|
127
|
+
for (var i = 0; i < d.tasks.length; i++) {
|
|
128
|
+
var t = d.tasks[i]
|
|
129
|
+
if (t && t.touches !== undefined && !Array.isArray(t.touches)) t.touches = []
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return d
|
|
134
|
+
}
|
|
74
135
|
|
|
75
136
|
// ===== 状态流转 =====
|
|
76
137
|
export function claimCheck(d, t, sid) { if (CLAIMABLE.indexOf(t.status) < 0) return 'cannot claim in ' + t.status; if (t.claimedBy && t.claimedBy !== sid && t.status === 'in-progress') return 'claimed by ' + t.claimedBy; if (d.boardMode === 'manual' || t.assignMode === 'manual') { if (t.assignee && t.assignee !== sid) return 'assigned to ' + t.assignee }; if (isb(t)) { var p = gpt(t, d.tasks); if (!p) return 'parent not found'; if (p.status !== 'in-progress' && p.status !== 'verifying') return 'parent not in-progress' }; var mc = d.tasks.filter(function (x) { return x.claimedBy === sid && (x.status === 'in-progress' || x.status === 'verifying') && !isb(x) }); if (!isb(t) && mc.length >= MAX_CLAIMED) return 'max ' + MAX_CLAIMED + ' active'; return null }
|
|
77
138
|
export function claimApply(d, t, sid, note) { var ps = t.status; t.status = 'in-progress'; t.claimedBy = sid; t.claimedAt = new Date().toISOString(); ah(t, ps, 'in-progress', sid, note) }
|
|
78
139
|
export function checkParentAuto(d, t) { if (!isb(t)) return null; var p = gpt(t, d.tasks); if (!p || p.status !== 'in-progress') return null; var s = gsb(p.id, d.tasks); if (s.every(function (x) { return x.status === 'resolved' })) { p.status = 'verifying'; p.resolvedAt = new Date().toISOString(); p.resolution = 'all subtasks resolved'; ah(p, 'in-progress', 'verifying', 'system', 'auto: all subtasks resolved'); return p }; return null }
|
|
79
140
|
export function resolveApply(d, t, sid, status, resolution, note) { var ps = t.status; if (status === 'verifying' && t.pipeline && t.pipeline !== 'full') { status = 'resolved' } t.status = status; t.resolution = resolution || null; t.resolvedAt = new Date().toISOString(); ah(t, ps, status, sid, note); var r = { ok: true, task: t }; if (status === 'verifying' && isb(t)) { var s = gsb(t.parentId, d.tasks); if (s.every(function (x) { return x.status === 'resolved' || x.id === t.id })) { var p = gpt(t, d.tasks); if (p && p.status === 'in-progress') { p.status = 'verifying'; p.resolvedAt = new Date().toISOString(); p.resolution = 'all subtasks done'; ah(p, 'in-progress', 'verifying', 'system', 'auto'); r.parentUpdated = true } } }; return r }
|
|
80
|
-
export function verifyApply(d, t, sid, verdict, comment) { var ps = t.status; if (verdict === 'approved') { t.status = 'resolved'; t.verifiedAt = new Date().toISOString(); t.verifiedBy = sid; ah(t, ps, 'resolved', sid, 'approved' + (comment ? ': ' + comment : '')) } else { t.status = 'in-progress'; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'in-progress', sid, 'rejected' + (comment ? ': ' + comment : '')) }; var r = { ok: true, task: t }; if (verdict === 'approved' && isb(t)) { var p = checkParentAuto(d, t); if (p) { r.parentUpdated = true } }; return r }
|
|
141
|
+
export function verifyApply(d, t, sid, verdict, comment) { var ps = t.status; if (verdict === 'approved') { t.status = 'resolved'; t.verifiedAt = new Date().toISOString(); t.verifiedBy = sid; delete t.frozen; delete t.frozenAt; delete t.frozenBy; ah(t, ps, 'resolved', sid, 'approved' + (comment ? ': ' + comment : '')) } else { t.status = 'in-progress'; t.resolvedAt = null; t.resolution = null; ah(t, ps, 'in-progress', sid, 'rejected' + (comment ? ': ' + comment : '')) }; var r = { ok: true, task: t }; if (verdict === 'approved' && isb(t)) { var p = checkParentAuto(d, t); if (p) { r.parentUpdated = true } }; return r }
|
|
81
142
|
|
|
82
143
|
// ===== 输出解析(文本降级路径)=====
|
|
83
144
|
// parseSections: 解析 ## 分段输出为结构化字段(容错:无分段时返回空对象,调用方降级)
|
|
@@ -157,13 +218,55 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
157
218
|
return p
|
|
158
219
|
}
|
|
159
220
|
|
|
221
|
+
// ===== 文件锁持有集合(touches 排他)=====
|
|
222
|
+
// 仅 in-progress + claimedBy + 声明了 touches 的任务持有文件锁:
|
|
223
|
+
// - verifying 不持有(Worker 已按契约停笔,锁随 in-progress→verifying 自动释放;
|
|
224
|
+
// 驳回回 in-progress 时重新持有);
|
|
225
|
+
// - pending/blocked/draft 没有 Worker 在改文件,不持有。
|
|
226
|
+
// 返回 [{id, touches}],id 用于 blockedTouches.conflicts 展示"在等谁"。
|
|
227
|
+
export function holdsFiles(d) {
|
|
228
|
+
var out = []
|
|
229
|
+
if (!d || !Array.isArray(d.tasks)) return out
|
|
230
|
+
for (var i = 0; i < d.tasks.length; i++) {
|
|
231
|
+
var t = d.tasks[i]
|
|
232
|
+
if (t.status === 'in-progress' && t.claimedBy && Array.isArray(t.touches) && t.touches.length) out.push({ id: t.id, touches: t.touches })
|
|
233
|
+
}
|
|
234
|
+
return out
|
|
235
|
+
}
|
|
236
|
+
// 候选任务(pending)与一组锁持有者是否冲突;返回持有者 id 列表(空数组 = 无冲突)
|
|
237
|
+
export function touchesConflict(t, holds) {
|
|
238
|
+
var out = []
|
|
239
|
+
if (!t || !Array.isArray(holds)) return out
|
|
240
|
+
for (var i = 0; i < holds.length; i++) { if (holds[i] && holds[i].id !== t.id && overlapsTouches(t.touches, holds[i].touches)) out.push(holds[i].id) }
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
|
|
160
244
|
// ===== 派发决策(纯函数版,poolCycle 持锁段调用)=====
|
|
161
|
-
// 返回 { pendings, verifs }
|
|
245
|
+
// 返回 { pendings, verifs, blockedTouches }:按优先级排序的可派发任务清单。
|
|
246
|
+
// frozen(裁决挂起冻结)不参与任何自动派发:pending 不派 Worker、verifying 不派 Verifier;
|
|
247
|
+
// 只能由主窗口显式解冻(unfreeze-task RPC / task_update unfreeze:true)后重新入池。
|
|
248
|
+
// blockedTouches(touches 文件级排他):候选声明了 touches 且与活动任务的 touches 冲突 →
|
|
249
|
+
// 不进 pendings,改记 [{id, conflicts:[持有任务id...]}],由 index.mjs 的 poolCycle 写展示态字段
|
|
250
|
+
// t.waitingForTouches(每心跳刷新的 UI 展示,不参与其他逻辑)。verifs 不受 touches 影响(Verifier 只读)。
|
|
251
|
+
// 注意:frozen/dependsOn/escalation/上限 的优先级不变——先过滤再算 touches 冲突。
|
|
162
252
|
export function pickDispatch(d, capW, capV, busyTaskIds) {
|
|
163
|
-
var
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
253
|
+
var blockedTouches = []
|
|
254
|
+
var pendings = []
|
|
255
|
+
// 锁持有集合在本轮内动态增长:一旦某候选被纳入本轮派发,它立即成为持有者,
|
|
256
|
+
// 防止同一轮 cycle 派出的两个任务声明重叠 touches。
|
|
257
|
+
var holds = holdsFiles(d)
|
|
258
|
+
if (capW > 0) {
|
|
259
|
+
var cands = d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && !t.frozen && t.assignMode !== 'manual' && t.pipeline !== 'direct' && depsSatisfied(d, t) && !t.escalation })
|
|
260
|
+
.sort(function (a, b) { var p = (PRIO_RANK[b.priority] || 2) - (PRIO_RANK[a.priority] || 2); return p !== 0 ? p : (a.createdAt || '').localeCompare(b.createdAt || '') })
|
|
261
|
+
for (var i = 0; i < cands.length && pendings.length < capW; i++) {
|
|
262
|
+
var conflicts = touchesConflict(cands[i], holds)
|
|
263
|
+
if (conflicts.length) { blockedTouches.push({ id: cands[i].id, conflicts: conflicts }); continue }
|
|
264
|
+
pendings.push(cands[i])
|
|
265
|
+
if (Array.isArray(cands[i].touches) && cands[i].touches.length) holds.push({ id: cands[i].id, touches: cands[i].touches })
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && !t.frozen && (!t.pipeline || t.pipeline === 'full') && !t.escalation && t.verifierRun !== 'spawn-pending' && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
|
|
269
|
+
return { pendings: pendings, verifs: verifs, blockedTouches: blockedTouches }
|
|
167
270
|
}
|
|
168
271
|
// 孤儿回收判定:in-progress 且 claimedBy 非主会话、无活跃 run、无 escalation、超 2 分钟
|
|
169
272
|
export function isOrphan(d, t, runs, now) {
|
package/package.json
CHANGED