dsh-agent-board 1.2.0 → 1.2.1
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/index.mjs +63 -8
- package/lib/client.js +87 -11
- package/lib/core.mjs +8 -2
- package/package.json +1 -1
- package/test/core.test.mjs +17 -0
package/index.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export function apply(ctx) {
|
|
|
64
64
|
var teamModeCache = {}
|
|
65
65
|
function fileFor(sid) { return '.dsh/tasks-' + sid + '.json' }
|
|
66
66
|
async function rt(sid) { try { var t = await fs.resolve(fileFor(sid)); var r = await fs.readText(t); var d = JSON.parse(r); if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return normalizeBoard(d) }; return seed(sid) } catch (_) { return seed(sid) } }
|
|
67
|
-
async function wt(sid, d) { var c = JSON.stringify(d
|
|
67
|
+
async function wt(sid, d) { var c = JSON.stringify(d); try { var t = await fs.resolve(fileFor(sid)); await fs.writeText(t, c) } catch (e) { console.error('[task-board] write:', String(e)); throw e } }
|
|
68
68
|
// 每会话一条 promise 链,串行化所有 读-改-写,消除并发写竞争
|
|
69
69
|
var fileLocks = {}
|
|
70
70
|
function withLock(sid, fn) { var prev = fileLocks[sid] || Promise.resolve(); var p = prev.then(function () { return fn() }); fileLocks[sid] = p.catch(function () {}); return p }
|
|
@@ -139,13 +139,40 @@ export function apply(ctx) {
|
|
|
139
139
|
var packByChild = {}
|
|
140
140
|
var pendingPacks = []
|
|
141
141
|
|
|
142
|
+
// 历史会话留档:t.runs = [{ role, id, at, model, outcome, endedAt }],上限 20 条
|
|
143
|
+
// 目的:任务流转到 resolved/archived 后,详情页仍能选择跳转到任一历史阶段的会话
|
|
144
|
+
// (Worker 首次/重试、Verifier 各次),而不是只剩最后一次 run id。
|
|
145
|
+
async function recordRunHistory(sid, taskId, role, runId, model, hardMin) {
|
|
146
|
+
try {
|
|
147
|
+
await mutateLocked(sid, function (d) {
|
|
148
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
149
|
+
if (!t) return
|
|
150
|
+
if (!Array.isArray(t.runs)) t.runs = []
|
|
151
|
+
t.runs.push({ role: role, id: runId, at: new Date().toISOString(), model: model || '', outcome: 'running', hardMin: hardMin || 120 })
|
|
152
|
+
if (t.runs.length > 20) t.runs = t.runs.slice(-20)
|
|
153
|
+
})
|
|
154
|
+
} catch (e) { console.error('[task-board] recordRunHistory failed:', String(e)) }
|
|
155
|
+
}
|
|
156
|
+
async function closeRunHistory(sid, taskId, runId, outcome) {
|
|
157
|
+
try {
|
|
158
|
+
await mutateLocked(sid, function (d) {
|
|
159
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
160
|
+
if (!t || !Array.isArray(t.runs)) return
|
|
161
|
+
for (var i = t.runs.length - 1; i >= 0; i--) {
|
|
162
|
+
if (t.runs[i].id === runId) { t.runs[i].outcome = outcome; t.runs[i].endedAt = new Date().toISOString(); break }
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
} catch (e) { console.error('[task-board] closeRunHistory failed:', String(e)) }
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
async function spawnOneShot(sid, t, role) {
|
|
143
169
|
var subagents = ctx.subagents; if (!subagents) return null
|
|
144
170
|
var parent = rootForSession(sid); if (!parent) { console.error('[task-board] no root agent for session ' + sid + ', skip spawn'); return null }
|
|
145
171
|
var providerName = pickProvider(); if (!providerName) { console.error('[task-board] no subagent provider'); return null }
|
|
146
172
|
var modelOverride = ''
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
var dsnap = await rt(sid)
|
|
174
|
+
if (role === 'verifier') { modelOverride = (typeof dsnap.verifierModel === 'string' && dsnap.verifierModel.trim()) ? dsnap.verifierModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
175
|
+
else if (role === 'worker') { modelOverride = (typeof dsnap.workerModel === 'string' && dsnap.workerModel.trim()) ? dsnap.workerModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
149
176
|
var pack = ''
|
|
150
177
|
try { pack = await readContextPack(t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
151
178
|
// user prompt 只留一行指引,内容走上下文注入区块
|
|
@@ -167,13 +194,32 @@ export function apply(ctx) {
|
|
|
167
194
|
} finally {
|
|
168
195
|
if (ppEntry) { var ppi = pendingPacks.indexOf(ppEntry); if (ppi >= 0) pendingPacks.splice(ppi, 1) }
|
|
169
196
|
}
|
|
170
|
-
var
|
|
197
|
+
var c = cfg(dsnap)
|
|
198
|
+
var rec = { run: run, role: role, taskId: t.id, startedAt: Date.now(), model: modelOverride, settled: false }
|
|
171
199
|
runsFor(sid)[t.id] = rec
|
|
172
200
|
if (pack) packByChild[String(run.id)] = pack
|
|
173
201
|
if (!dispatchedEver[sid]) dispatchedEver[sid] = {}
|
|
174
202
|
dispatchedEver[sid][String(run.id)] = true
|
|
175
|
-
//
|
|
176
|
-
|
|
203
|
+
// 历史会话留档:每次派发都追加一条 {role,id,at,model},任务完成后仍可回看
|
|
204
|
+
// 各阶段(含重试的第 1/2/3 次 Worker)会话——否则 claimedBy/verifierRun 只留最后一次
|
|
205
|
+
recordRunHistory(sid, t.id, role, String(run.id), modelOverride, c.hardTimeoutMin).catch(function () {})
|
|
206
|
+
// ===== 两级超时:软超时只提醒主窗口(由人决定继续等待或终止),硬超时兜底 dispose =====
|
|
207
|
+
// 一次性 run 没有看门狗:完全依赖人工决策时,人不在线挂死的 run 会永久占用并发位,
|
|
208
|
+
// 所以保留硬上限作为最后防线(默认 120min,可配置)。
|
|
209
|
+
var startedAt = rec.startedAt
|
|
210
|
+
var softMs = c.softTimeoutMin * 60000
|
|
211
|
+
var hardMs = c.hardTimeoutMin * 60000
|
|
212
|
+
function finish(res, err) { if (rec.settled) return; rec.settled = true; settleRun(sid, rec, res, err) }
|
|
213
|
+
;(function softArm() {
|
|
214
|
+
var tm = ctx.timer; if (!tm) return
|
|
215
|
+
tm.timeout(softMs).then(function () {
|
|
216
|
+
if (rec.settled) return
|
|
217
|
+
var mins = Math.round((Date.now() - startedAt) / 60000)
|
|
218
|
+
pushSysNote(sid, '⏱ 任务「' + t.title + '」的 ' + role + '(' + t.id + ')已运行 ' + mins + ' 分钟仍未完成——如属正常长任务可忽略;需要干预可在看板详情页「立即终止」(硬超时 ' + c.hardTimeoutMin + ' 分钟后将自动终止并重试)')
|
|
219
|
+
softArm() // 持续提醒直到结算或硬超时
|
|
220
|
+
}).catch(function () {})
|
|
221
|
+
})()
|
|
222
|
+
withTimeout(run.result, hardMs, role + ':' + t.id).then(function (res) { finish(res, null) }).catch(function (e) { finish(null, e) })
|
|
177
223
|
return rec
|
|
178
224
|
}
|
|
179
225
|
|
|
@@ -190,6 +236,8 @@ export function apply(ctx) {
|
|
|
190
236
|
if (rec.role === 'worker') await settleWorker(sid, rec, output, failed, errText)
|
|
191
237
|
else await settleVerifier(sid, rec, output, failed, errText)
|
|
192
238
|
} catch (e) { console.error('[task-board] settle ' + rec.role + ' failed (task ' + rec.taskId + '):', String(e)) }
|
|
239
|
+
// 历史会话留档:记录该次 run 的结局(完成/失败/硬超时),详情页可据此标注阶段状态
|
|
240
|
+
closeRunHistory(sid, rec.taskId, String(rec.run.id), failed ? (err ? 'timeout/error' : 'incomplete') : 'completed').catch(function () {})
|
|
193
241
|
}
|
|
194
242
|
|
|
195
243
|
async function settleWorker(sid, rec, output, failed, errText) {
|
|
@@ -486,7 +534,14 @@ export function apply(ctx) {
|
|
|
486
534
|
var bucket = '--' + ws.replace(/[\\/:]/g, '-') + '--'
|
|
487
535
|
var log = path.join(os.homedir(), '.dsh', 'sessions', bucket, child, 'session.jsonl.zstd')
|
|
488
536
|
if (!fsNode.existsSync(log)) return { ok: true, activity: null, reason: 'log not found' }
|
|
489
|
-
|
|
537
|
+
// 只同步读末尾 2MB(日志追加写,末帧必在尾部)——整文件 readFileSync 在大日志上
|
|
538
|
+
// 会造成数十毫秒级同步 I/O,多任务轮询时叠加成全局卡顿
|
|
539
|
+
var sz = fsNode.statSync(log).size
|
|
540
|
+
var TAIL = 2 * 1024 * 1024
|
|
541
|
+
var start = sz > TAIL ? sz - TAIL : 0
|
|
542
|
+
var buf = Buffer.alloc(sz - start)
|
|
543
|
+
var fd = fsNode.openSync(log, 'r')
|
|
544
|
+
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
490
545
|
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
491
546
|
var last = buf.lastIndexOf(MAGIC) // 追加写多帧格式:最新事件在末帧,只解一帧控成本
|
|
492
547
|
if (last < 0) return { ok: true, activity: null }
|
|
@@ -685,7 +740,7 @@ export function apply(ctx) {
|
|
|
685
740
|
}
|
|
686
741
|
return { ok: true, models: out }
|
|
687
742
|
})
|
|
688
|
-
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; return { ok: true } }) })
|
|
743
|
+
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'softTimeoutMin') d.softTimeoutMin = Math.max(1, Math.min(480, Number(args.value) || 30)); else if (args.key === 'hardTimeoutMin') d.hardTimeoutMin = Math.max(1, Math.min(1440, Number(args.value) || 120)); return { ok: true } }) })
|
|
689
744
|
handle('create-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id' }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title || 'Untitled', description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: (Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []), docs: [], instructions: args.instructions || '', notes: (typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''), relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); return { ok: true, task: t } }) })
|
|
690
745
|
handle('list-children', async function (args) { var sid = rpcSessionId(args); var subs = ctx.subagents; if (!subs) return { ok: true, children: [] }; try { var list = await subs.listChildren(sid); var children = (list || []).map(function (c) { return { id: String(c.sessionId || c.id || ''), label: String(c.label || c.title || c.mode || '') } }).filter(function (c) { return c.id.length > 0 }); return { ok: true, children: children } } catch (e) { return { ok: true, children: [], error: String(e) } } })
|
|
691
746
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)=====
|
package/lib/client.js
CHANGED
|
@@ -200,7 +200,7 @@ function apply(ctx) {
|
|
|
200
200
|
}
|
|
201
201
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
202
202
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
203
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', 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: [] }
|
|
203
|
+
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: [] }
|
|
204
204
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
205
205
|
try {
|
|
206
206
|
var onKey = function (e) {
|
|
@@ -235,6 +235,8 @@ function apply(ctx) {
|
|
|
235
235
|
state.maxVerifiers = (d && d.maxVerifiers) || 2
|
|
236
236
|
state.verifierModel = (d && d.verifierModel) || ''
|
|
237
237
|
state.workerModel = (d && d.workerModel) || ''
|
|
238
|
+
state.softTimeoutMin = (d && d.softTimeoutMin) || 30
|
|
239
|
+
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
238
240
|
state.poolStatus = (d && d.poolStatus) || null
|
|
239
241
|
state.isRoot = !d || d.isRoot !== false
|
|
240
242
|
if (state.isRoot === false && state.open) { state.open = false; state.detailId = null } // 子代理会话:强制收起看板
|
|
@@ -249,7 +251,7 @@ function apply(ctx) {
|
|
|
249
251
|
function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; state.children = (d && d.children) || []; notify() }).catch(function () {}) }
|
|
250
252
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
251
253
|
function fetchActivity() {
|
|
252
|
-
if (!state.sessionId || !state.isRoot) return
|
|
254
|
+
if (!state.sessionId || !state.isRoot || !state.open) return // 面板关闭时不轮询活动(省同步 I/O)
|
|
253
255
|
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
254
256
|
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
255
257
|
var pending = running.length
|
|
@@ -485,6 +487,28 @@ function apply(ctx) {
|
|
|
485
487
|
function TeamSwitch(props) { var on = props.on; function toggle() { rpc('set-team-mode', { enabled: !on }).then(fetchTasks).catch(function () {}) } return React.createElement('button', { onClick: toggle, title: on ? 'Team 模式已开启:任务走看板派发,Worker 歧义自动上报主窗口裁决(不拦截主窗口编辑)' : '开启 Team 模式:任务全走看板,Worker 歧义上报主窗口裁决', style: { fontSize: 12, padding: '4px 10px', border: '1px solid ' + (on ? C.brand : C.border), borderRadius: 8, cursor: 'pointer', fontWeight: 500, background: on ? C.brand : 'transparent', color: on ? C_INV : C.text2, transition: 'all .15s' } }, icText('users', 12, 'Team' + (on ? ' ON' : ''))) }
|
|
486
488
|
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, '归档'))) }
|
|
487
489
|
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.border, 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.border), 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) }
|
|
490
|
+
function MinCfg(props) {
|
|
491
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
492
|
+
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
493
|
+
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
494
|
+
useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value])
|
|
495
|
+
function commit(v) {
|
|
496
|
+
var n = parseInt(v, 10)
|
|
497
|
+
if (isNaN(n) || n < props.min || n > props.max) { setVal(String(props.value)); setDirty(false); return }
|
|
498
|
+
setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {})
|
|
499
|
+
}
|
|
500
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
|
|
501
|
+
props.label,
|
|
502
|
+
React.createElement('input', {
|
|
503
|
+
value: val, title: props.tip || (props.min + ' ~ ' + props.max + ' 分钟'),
|
|
504
|
+
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
505
|
+
onBlur: function () { if (dirty) commit(val) },
|
|
506
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commit(val) },
|
|
507
|
+
style: { width: 38, padding: '0px 3px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text }
|
|
508
|
+
}),
|
|
509
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
510
|
+
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)
|
|
511
|
+
}
|
|
488
512
|
function PoolStatus() {
|
|
489
513
|
var ps = state.poolStatus
|
|
490
514
|
if (!ps) return null
|
|
@@ -541,7 +565,11 @@ function apply(ctx) {
|
|
|
541
565
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
|
|
542
566
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
543
567
|
React.createElement(ModelCfg, { label: 'V模型', cfgKey: 'verifierModel', value: props.verifierModel }),
|
|
544
|
-
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)'))
|
|
568
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
|
|
569
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
|
|
570
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
571
|
+
React.createElement(MinCfg, { label: '软超时', cfgKey: 'softTimeoutMin', value: props.softT, min: 1, max: 480, tip: '超过后在看板提醒主窗口,不终止 run(1~480 分钟)' }),
|
|
572
|
+
React.createElement(MinCfg, { label: '硬超时', cfgKey: 'hardTimeoutMin', value: props.hardT, min: 1, max: 1440, tip: '兜底:人不在线时自动终止挂死 run 并重试(1~1440 分钟)' }))) : null)
|
|
545
573
|
}
|
|
546
574
|
// #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签
|
|
547
575
|
function passFilter(t) {
|
|
@@ -557,6 +585,17 @@ function apply(ctx) {
|
|
|
557
585
|
// #19 管线档位元数据
|
|
558
586
|
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
559
587
|
function durOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); 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' : '') } catch (_) { return '' } }
|
|
588
|
+
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)) }
|
|
589
|
+
// 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
|
|
590
|
+
function historyRuns(t) {
|
|
591
|
+
var out = Array.isArray(t.runs) ? t.runs.slice() : []
|
|
592
|
+
if (!out.length) {
|
|
593
|
+
if (t.claimedBy && t.claimedBy !== 'auto-dispatch' && t.claimedBy !== 'system') out.push({ role: 'worker', id: t.claimedBy, at: t.claimedAt })
|
|
594
|
+
if (t.verifierRun) out.push({ role: 'verifier', id: t.verifierRun, at: t.verifiedAt })
|
|
595
|
+
}
|
|
596
|
+
return out.filter(function (r) { return r && r.id })
|
|
597
|
+
}
|
|
598
|
+
function elapsedSince(iso) { var m = elapsedMin(iso); if (!m) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') }
|
|
560
599
|
function pipeOf(t) { return pipeMeta[t.pipeline] || pipeMeta.full }
|
|
561
600
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status)
|
|
562
601
|
function BatchBar() {
|
|
@@ -639,9 +678,22 @@ function apply(ctx) {
|
|
|
639
678
|
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
640
679
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
|
|
641
680
|
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
642
|
-
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4 } },
|
|
681
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
643
682
|
React.createElement('button', { onClick: function () { restore(t.id) }, title: '恢复为待办', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
644
|
-
|
|
683
|
+
(function () {
|
|
684
|
+
var runs = historyRuns(t)
|
|
685
|
+
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
686
|
+
var wN = 0, vN = 0
|
|
687
|
+
return React.createElement('span', { style: { display: 'inline-flex', gap: 5, flexWrap: 'wrap' } }, runs.map(function (r, i) {
|
|
688
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
689
|
+
var isV = r.role === 'verifier'
|
|
690
|
+
return React.createElement('button', {
|
|
691
|
+
key: i, onClick: function () { if (sessionsSvc) sessionsSvc.open(r.id) },
|
|
692
|
+
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
693
|
+
style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
|
|
694
|
+
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
695
|
+
}))
|
|
696
|
+
})()))
|
|
645
697
|
}))
|
|
646
698
|
}
|
|
647
699
|
function TeamView() {
|
|
@@ -698,7 +750,6 @@ function apply(ctx) {
|
|
|
698
750
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
699
751
|
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) }) }
|
|
700
752
|
function jumpToAgent() { if (sessionsSvc && task.claimedBy) sessionsSvc.open(task.claimedBy) }
|
|
701
|
-
function jumpToVerifier() { if (sessionsSvc && task.verifierRun) sessionsSvc.open(task.verifierRun) }
|
|
702
753
|
function submitArbitration() { if (!arbAnswer.trim()) return; rpc('resolve-escalation', { taskId: task.id, answer: arbAnswer }).then(function (r) { setActionMsg(r && r.ok ? '✅ 裁决已转达给 Worker' : '⚠️ ' + ((r && r.error) || '失败')); setArbAnswer(''); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
703
754
|
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)) }) }
|
|
704
755
|
function doDismiss() { rpc('dismiss-suspect', { taskId: task.id }).then(function () { setActionMsg('✅ 已清除卡死标记,继续观察'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
@@ -709,6 +760,11 @@ function apply(ctx) {
|
|
|
709
760
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
710
761
|
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),
|
|
711
762
|
(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,
|
|
763
|
+
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' } },
|
|
764
|
+
ic('activity', 10),
|
|
765
|
+
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
766
|
+
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
767
|
+
React.createElement('button', { onClick: doTerminate, title: '终止该执行 Agent,任务回 pending 重新排队', style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', marginLeft: 'auto' } }, '⏹ 立即终止')) : null,
|
|
712
768
|
task.escalation ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
|
|
713
769
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
714
770
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
@@ -752,9 +808,28 @@ function apply(ctx) {
|
|
|
752
808
|
task.status === 'verifying' && !task.escalation ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('dispatch-task', { taskId: task.id, role: '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,
|
|
753
809
|
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,
|
|
754
810
|
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,
|
|
755
|
-
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,
|
|
756
|
-
|
|
757
|
-
|
|
811
|
+
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),
|
|
812
|
+
(function () {
|
|
813
|
+
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
814
|
+
var runs = historyRuns(task)
|
|
815
|
+
if (!runs.length) return null
|
|
816
|
+
var roleMeta = { worker: { label: 'W', color: C.brand, tip: 'Worker' }, verifier: { label: 'V', color: C.warn, tip: 'Verifier' } }
|
|
817
|
+
var wN = 0, vN = 0
|
|
818
|
+
return React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', background: C.nested, borderRadius: 6 } },
|
|
819
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('users', 10), '历史会话(点击跳转查看,含全部重试)'),
|
|
820
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, flexWrap: 'wrap' } },
|
|
821
|
+
runs.map(function (r, i) {
|
|
822
|
+
var rm = roleMeta[r.role] || roleMeta.worker
|
|
823
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
824
|
+
var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
|
|
825
|
+
var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
|
|
826
|
+
return React.createElement('button', {
|
|
827
|
+
key: i, onClick: function () { if (sessionsSvc) sessionsSvc.open(r.id) },
|
|
828
|
+
title: rm.tip + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
829
|
+
style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + (isCur ? rm.color : C.border), borderRadius: 3, background: isCur ? C.card : 'transparent', color: rm.color, cursor: 'pointer', fontWeight: isCur ? 600 : 400 }
|
|
830
|
+
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark)
|
|
831
|
+
})))
|
|
832
|
+
})(),
|
|
758
833
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
759
834
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), '高优先级介入(插入执行 Agent 队首)'),
|
|
760
835
|
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
@@ -766,7 +841,8 @@ function apply(ctx) {
|
|
|
766
841
|
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]
|
|
767
842
|
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]
|
|
768
843
|
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
769
|
-
|
|
844
|
+
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
845
|
+
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) } }, [])
|
|
770
846
|
if (!open) return null
|
|
771
847
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
772
848
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -792,7 +868,7 @@ function apply(ctx) {
|
|
|
792
868
|
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 } },
|
|
793
869
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, React.createElement('span', { style: { fontWeight: 600, fontSize: 13, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('clipboard-list', 15), '智能看板'), React.createElement(ViewTab, null), React.createElement(PoolStatus, null)),
|
|
794
870
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
795
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel }),
|
|
871
|
+
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT }),
|
|
796
872
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
797
873
|
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), '多选'),
|
|
798
874
|
React.createElement(TeamSwitch, { on: teamMode }),
|
package/lib/core.mjs
CHANGED
|
@@ -57,12 +57,18 @@ export function classifyPipeline(t) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ===== 配置(一次性派发模型:max*=并发上限;min* 字段保留仅为兼容旧看板文件,引擎不使用)=====
|
|
60
|
-
|
|
60
|
+
// 两级超时:软超时(默认 30min)只上报主窗口提醒,不杀 run;硬超时(默认 120min)才兜底
|
|
61
|
+
// dispose 释放并发位——人在线时由人决策,人不在时系统兜底。
|
|
62
|
+
export function cfg(d) {
|
|
63
|
+
var soft = Math.max(1, Math.min(480, d.softTimeoutMin || 30))
|
|
64
|
+
var hard = Math.max(soft, Math.min(1440, d.hardTimeoutMin || 120))
|
|
65
|
+
return { minWorkers: Math.max(0, Math.min(10, d.minWorkers || 1)), maxWorkers: Math.max(1, Math.min(10, d.maxWorkers || 3)), minVerifiers: Math.max(0, Math.min(5, d.minVerifiers || 0)), maxVerifiers: Math.max(0, Math.min(5, d.maxVerifiers || 2)), softTimeoutMin: soft, hardTimeoutMin: hard }
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
// ===== 看板文件种子 =====
|
|
63
69
|
// poolStatus 必须始终在种子/归一化里存在:task_list 工具输出 poolStatus: d.poolStatus,
|
|
64
70
|
// 缺字段 = undefined → 工具结果的 lossless-JSON 校验会拒("value is not lossless JSON")
|
|
65
|
-
export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', poolStatus: { workers: [], verifiers: [] }, tasks: [] } }
|
|
71
|
+
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: [] } }
|
|
66
72
|
// 旧文件缺 poolStatus 的归一化(读路径兜底,保证任何历史文件都满足工具输出契约)
|
|
67
73
|
export function normalizeBoard(d) { if (d && typeof d === 'object') { if (!d.poolStatus || typeof d.poolStatus !== 'object' || !Array.isArray(d.poolStatus.workers) || !Array.isArray(d.poolStatus.verifiers)) d.poolStatus = { workers: [], verifiers: [] } }; return d }
|
|
68
74
|
|
package/package.json
CHANGED
package/test/core.test.mjs
CHANGED
|
@@ -106,6 +106,23 @@ test('parseSections: 分段解析', () => {
|
|
|
106
106
|
assert.deepEqual(core.parseSections('没有分段的纯文本'), {})
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
+
test('cfg: 两级超时默认值/夹取/hard≥soft(软超时只提醒,硬超时兜底)', () => {
|
|
110
|
+
assert.equal(core.cfg({}).softTimeoutMin, 30)
|
|
111
|
+
assert.equal(core.cfg({}).hardTimeoutMin, 120)
|
|
112
|
+
assert.equal(core.cfg({ softTimeoutMin: 45, hardTimeoutMin: 240 }).softTimeoutMin, 45)
|
|
113
|
+
assert.equal(core.cfg({ softTimeoutMin: 0 }).softTimeoutMin, 30) // 0 → 默认
|
|
114
|
+
assert.equal(core.cfg({ softTimeoutMin: 9999 }).softTimeoutMin, 480) // 上限夹取
|
|
115
|
+
assert.equal(core.cfg({ hardTimeoutMin: 99999 }).hardTimeoutMin, 1440)
|
|
116
|
+
// hard 不得低于 soft,避免硬超时早于软提醒触发
|
|
117
|
+
assert.equal(core.cfg({ softTimeoutMin: 300, hardTimeoutMin: 60 }).hardTimeoutMin, 300)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('seed: 新看板自带两级超时默认值', () => {
|
|
121
|
+
const s = core.seed('session-x')
|
|
122
|
+
assert.equal(s.softTimeoutMin, 30)
|
|
123
|
+
assert.equal(s.hardTimeoutMin, 120)
|
|
124
|
+
})
|
|
125
|
+
|
|
109
126
|
test('parseSections: diff 概要分段解析(Worker 变更概要 → deliverable.diff)', () => {
|
|
110
127
|
const s = core.parseSections('## 开发描述\n做了\n## diff 概要\n gen.py | 3 + ++')
|
|
111
128
|
assert.equal(s.diff, 'gen.py | 3 + ++')
|