@joekytc/dsh-swarm 0.3.2 → 0.3.6
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 +14 -7
- package/README.zh-CN.md +10 -5
- package/client/KanbanBoard.tsx +3 -1
- package/client/TaskDrawer.tsx +74 -2
- package/client/WorkflowRail.tsx +84 -1
- package/client/config-store.ts +3 -1
- package/client/kanban.css +28 -0
- package/client/timeline-model.ts +4 -0
- package/client/workflow-model.ts +20 -11
- package/lib/client.js +1100 -22
- package/lib/config.d.ts +5 -1
- package/lib/config.js +2 -1
- package/lib/dispatcher/agent-runner.js +25 -9
- package/lib/dispatcher/event-waker.js +4 -1
- package/lib/dispatcher/v-orchestrator.d.ts +2 -0
- package/lib/dispatcher/v-orchestrator.js +58 -9
- package/lib/domain/config-override.d.ts +6 -0
- package/lib/domain/config-override.js +16 -1
- package/lib/domain/im-message.d.ts +21 -2
- package/lib/domain/im-message.js +84 -4
- package/lib/domain/kanban-service.d.ts +8 -1
- package/lib/domain/kanban-service.js +67 -1
- package/lib/domain/permissions.d.ts +1 -1
- package/lib/domain/permissions.js +4 -0
- package/lib/domain/projection.js +8 -3
- package/lib/domain/review-evidence.js +9 -0
- package/lib/domain/review-target.d.ts +10 -0
- package/lib/domain/review-target.js +24 -0
- package/lib/domain/state-machine.js +1 -1
- package/lib/domain/types.d.ts +16 -3
- package/lib/routes/kanban-http.js +52 -2
- package/lib/services/config-provider.js +2 -0
- package/lib/services/im-bot-probe.d.ts +36 -0
- package/lib/services/im-bot-probe.js +128 -0
- package/lib/services/im-delivery.d.ts +69 -7
- package/lib/services/im-delivery.js +220 -34
- package/lib/tools/kanban-tools.js +38 -1
- package/lib/tools/main-session-tools.js +34 -9
- package/package.json +1 -1
- package/personas/kanban-p/agent.cordis.yml +16 -0
- package/personas/kanban-pt/agent.cordis.yml +19 -1
- package/personas/kanban-w/agent.cordis.yml +15 -0
- package/personas/persona-p.md +4 -0
- package/personas/persona-pt.md +4 -1
- package/personas/persona-w.md +1 -0
- package/personas/swarm/agent.cordis.yml +11 -0
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ All keys are optional; schema lives in `src/config.ts`. **Most users only need t
|
|
|
136
136
|
| `dispatcher.maxRetries` | `3` | Failure retries before circuit → `blocked(gave_up)` |
|
|
137
137
|
| `dispatcher.heartbeatIntervalSeconds` | `300` | Watchdog heartbeat period |
|
|
138
138
|
| `dispatcher.maxProtocolViolations` | `2` | Protocol-violation guardrail: after this many consecutive violations the next one is final (`gave_up`) |
|
|
139
|
-
| `dispatcher.maxReworksPerRole` | `{ pt:
|
|
139
|
+
| `dispatcher.maxReworksPerRole` | `{ pt: 3, dt: 3 }` | Max review rework rounds before `review/gave-up` + `[review-final]` |
|
|
140
140
|
| `prefixRoutes.plan` | `/plan:` | Command-mode planning prefix |
|
|
141
141
|
| `prefixRoutes.openspec` | `/openspec:` | Command-mode approve-and-execute prefix |
|
|
142
142
|
| `ui.enabled` | `true` | Enable the kanban web tab |
|
|
@@ -298,12 +298,19 @@ blocks the save, and chain creation mounts the checklist as the `file-prefetch`
|
|
|
298
298
|
#### Rework (review failure)
|
|
299
299
|
|
|
300
300
|
A failed review never mutates a `done` card. Instead the system records
|
|
301
|
-
`review/failed`, creates a **rework task** (`[返工] ...`)
|
|
302
|
-
|
|
303
|
-
(`reviewStatus: 'pending'`)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
`review/failed`, creates a **rework task** (`[返工] ...`) with its own session
|
|
302
|
+
(`kbn-<reworkId>`, `resumeSessionId = null`), `reviewAttempt + 1`, and starts as
|
|
303
|
+
`todo` (`reviewStatus: 'pending'`). The rework card body additionally carries the
|
|
304
|
+
previous round's issues verbatim in a `## 本轮修复清单` (fix-this-round) section.
|
|
305
|
+
A fresh review card is then dispatched for the rework.
|
|
306
|
+
When `reviewAttempt` reaches `maxReworksPerRole` (PT 3 / DT 3), the system records
|
|
307
|
+
`review/gave-up` and posts a `[review-final]` evidence-chain comment, and (with IM
|
|
308
|
+
delivery enabled) sends a `[评审超限待裁决]` notification with two exit paths:
|
|
309
|
+
waive the review (`kanban_waive_review` / GUI “豁免评审”) or reopen the chain
|
|
310
|
+
(`kanban_reopen_chain` / GUI “人工恢复”). **Convergence gate (2026-09-15)**: on PT
|
|
311
|
+
rework rounds, once all legacy issues are resolved and no unresolved `critical`
|
|
312
|
+
remains, a `fail` verdict is downgraded to `pass` (new findings flow downstream as
|
|
313
|
+
non-blocking suggestions), so the loop always converges.
|
|
307
314
|
|
|
308
315
|
#### Failure recovery
|
|
309
316
|
|
package/README.zh-CN.md
CHANGED
|
@@ -138,7 +138,7 @@ p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
|
|
|
138
138
|
| `dispatcher.maxRetries` | `3` | 失败重试上限,超出进入熔断 → `blocked(gave_up)` |
|
|
139
139
|
| `dispatcher.heartbeatIntervalSeconds` | `300` | 看门狗心跳周期 |
|
|
140
140
|
| `dispatcher.maxProtocolViolations` | `2` | 协议违规护栏:连续违规超过该次数后,下一次即终局(`gave_up`) |
|
|
141
|
-
| `dispatcher.maxReworksPerRole` | `{ pt:
|
|
141
|
+
| `dispatcher.maxReworksPerRole` | `{ pt: 3, dt: 3 }` | 评审返工轮数上限,超出进入 `review/gave-up` + `[review-final]` |
|
|
142
142
|
| `prefixRoutes.plan` | `/plan:` | 命令模式阶段 0 规划前缀 |
|
|
143
143
|
| `prefixRoutes.openspec` | `/openspec:` | 命令模式批准并执行前缀 |
|
|
144
144
|
| `ui.enabled` | `true` | 启用看板 Web 标签页 |
|
|
@@ -290,10 +290,15 @@ D 只有带 `tdd` 才能完成——`test_files`(含 `test_first`)或 `skipp
|
|
|
290
290
|
#### 返工(评审失败)
|
|
291
291
|
|
|
292
292
|
评审失败**从不改写** `done` 卡。系统改为记录 `review/failed`,创建**返工任务**
|
|
293
|
-
(`[返工]
|
|
294
|
-
`todo`(`reviewStatus: 'pending'
|
|
295
|
-
|
|
296
|
-
`
|
|
293
|
+
(`[返工] ...`)——独立会话(`kbn-<reworkId>`,`resumeSessionId = null`)、
|
|
294
|
+
`reviewAttempt + 1`,初始为 `todo`(`reviewStatus: 'pending'`);返工卡 body 追加
|
|
295
|
+
「本轮修复清单」节(携带上一轮 issues 全文),然后为返工重新派发全新评审卡。
|
|
296
|
+
当 `reviewAttempt` 达到 `maxReworksPerRole`(PT 3 / DT 3)时,系统记录
|
|
297
|
+
`review/gave-up` 并发布 `[review-final]` 证据链评论,同时(启用 IM 投递时)发出
|
|
298
|
+
`[评审超限待裁决]` 通知,附两条出口:豁免评审(`kanban_waive_review` / GUI「豁免评审」)
|
|
299
|
+
或人工恢复链(`kanban_reopen_chain` / GUI「人工恢复」)。**收敛闸(2026-09-15)**:
|
|
300
|
+
PT 复审轮中旧账清零且无未解决 `critical` 时,fail 自动降级为 pass(新发现转下游
|
|
301
|
+
非阻塞建议),循环必然收敛。
|
|
297
302
|
|
|
298
303
|
#### 故障恢复
|
|
299
304
|
|
package/client/KanbanBoard.tsx
CHANGED
|
@@ -126,7 +126,7 @@ export function KanbanBoard(props: {
|
|
|
126
126
|
readOnly={task.status === 'archived'}
|
|
127
127
|
onRetry={failedAction && failedAction.taskId === task.id ? () => void runAction(failedAction.action) : undefined}
|
|
128
128
|
onComment={(body) => void runAction({ type: 'comment', taskId: task.id, body })}
|
|
129
|
-
onAction={(action) =>
|
|
129
|
+
onAction={(action) => runAction(action)}
|
|
130
130
|
onClose={() => history.back()}
|
|
131
131
|
/>
|
|
132
132
|
);
|
|
@@ -160,6 +160,8 @@ export function KanbanBoard(props: {
|
|
|
160
160
|
onOpenView={props.onOpenView}
|
|
161
161
|
onConfirmAudit={(chainId) => void runAction({ type: 'confirm-audit', chainId })}
|
|
162
162
|
onRenameChain={(chainId, title) => void runAction({ type: 'rename', chainId, title })}
|
|
163
|
+
// 2026-09-15 恢复能力:直连 postAction(失败 throw 由弹窗展示;无乐观更新,走服务端事件收敛)
|
|
164
|
+
onReopenChain={(chainId, reason) => props.postAction({ type: 'reopen-chain', chainId, reason }).then(() => undefined)}
|
|
163
165
|
onDeleteChain={async (chainId) => {
|
|
164
166
|
// 直连 postAction:失败 throw 上抛删除弹窗展示(runAction 吞错仅服务 TaskDrawer 重试条)
|
|
165
167
|
await props.postAction({ type: 'delete', chainId });
|
package/client/TaskDrawer.tsx
CHANGED
|
@@ -37,14 +37,23 @@ export function TaskDrawer(props: {
|
|
|
37
37
|
readOnly?: boolean;
|
|
38
38
|
onRetry?: () => void;
|
|
39
39
|
onComment(body: string): void;
|
|
40
|
-
onAction(action: { type: string; taskId: string; reason?: string; summary?: string; metadata?: Record<string, unknown>; body?: string }): void
|
|
40
|
+
onAction(action: { type: string; taskId: string; reason?: string; summary?: string; metadata?: Record<string, unknown>; body?: string }): void | Promise<unknown>;
|
|
41
41
|
onClose(): void;
|
|
42
42
|
}) {
|
|
43
43
|
const { task, events, handoff, specCard, chain } = props;
|
|
44
44
|
const [tab, setTab] = useState<string>('overview');
|
|
45
|
-
const [pending, setPending] = useState<{ kind: 'archive'; value: string } | null>(null);
|
|
45
|
+
const [pending, setPending] = useState<{ kind: 'archive' | 'waive'; value: string } | null>(null);
|
|
46
|
+
// 2026-09-15:豁免弹窗反馈三态(校验提示 / 在途 / 成功)——此前点击无任何反馈
|
|
47
|
+
const [waiveError, setWaiveError] = useState<string | null>(null);
|
|
48
|
+
const [waiveBusy, setWaiveBusy] = useState(false);
|
|
49
|
+
const [waiveOk, setWaiveOk] = useState(false);
|
|
46
50
|
const timeline = events.filter((e) => e.taskId === task.id).toSorted((a, b) => a.seq - b.seq);
|
|
47
51
|
const comments = timeline.filter((e) => e.kind === 'task/commented');
|
|
52
|
+
// 2026-09-15 恢复能力:评审卡存在 failed/gave-up 结论 → 可人工豁免(理由必填,审计留痕)。
|
|
53
|
+
const reviewOutcome = task.mode === 'review-plan' || task.mode === 'review-impl'
|
|
54
|
+
? events.filter((e) => e.taskId === task.id && e.kind.startsWith('review/')).at(-1)?.kind ?? null
|
|
55
|
+
: null;
|
|
56
|
+
const canWaive = (reviewOutcome === 'review/failed' || reviewOutcome === 'review/gave-up');
|
|
48
57
|
const submitComment = (el: HTMLInputElement) => {
|
|
49
58
|
const value = el.value.trim();
|
|
50
59
|
if (!value) return;
|
|
@@ -62,6 +71,26 @@ export function TaskDrawer(props: {
|
|
|
62
71
|
setPending(null);
|
|
63
72
|
props.onAction({ type: 'archive', taskId: task.id });
|
|
64
73
|
};
|
|
74
|
+
/** 豁免提交(2026-09-15):校验必给反馈 + 在途禁用 + 成功显式提示;失败保留输入可重试。 */
|
|
75
|
+
const submitWaive = async () => {
|
|
76
|
+
if (pending?.kind !== 'waive') return;
|
|
77
|
+
const reason = pending.value.trim();
|
|
78
|
+
if (!reason) {
|
|
79
|
+
setWaiveError('请填写豁免理由(必填,用于审计留痕)');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setWaiveError(null);
|
|
83
|
+
setWaiveBusy(true);
|
|
84
|
+
try {
|
|
85
|
+
await props.onAction({ type: 'waive-review', taskId: task.id, reason });
|
|
86
|
+
setWaiveOk(true);
|
|
87
|
+
window.setTimeout(() => { setPending(null); setWaiveOk(false); }, 800);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
setWaiveError('豁免失败:' + String(err));
|
|
90
|
+
} finally {
|
|
91
|
+
setWaiveBusy(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
65
94
|
return (
|
|
66
95
|
<div className="dsh-kb-detail">
|
|
67
96
|
<header className="dsh-kb-detail__header">
|
|
@@ -83,6 +112,14 @@ export function TaskDrawer(props: {
|
|
|
83
112
|
{pending?.kind === 'archive' ? '确认归档' : '归档'}
|
|
84
113
|
</button>
|
|
85
114
|
)}
|
|
115
|
+
{canWaive && (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => { setPending({ kind: 'waive', value: '' }); setWaiveError(null); setWaiveOk(false); }}
|
|
119
|
+
>
|
|
120
|
+
豁免评审
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
86
123
|
</div>
|
|
87
124
|
)}
|
|
88
125
|
</header>
|
|
@@ -209,6 +246,41 @@ export function TaskDrawer(props: {
|
|
|
209
246
|
)}
|
|
210
247
|
</section>
|
|
211
248
|
)}
|
|
249
|
+
{/* 豁免评审弹窗(2026-09-15):由行内编辑改为弹窗确认——行内输入框会挤压 header 按钮(文字被迫竖排) */}
|
|
250
|
+
{!props.readOnly && pending?.kind === 'waive' && (
|
|
251
|
+
<div className="dsh-kb-rename-overlay" onClick={(e) => { e.stopPropagation(); if (!waiveBusy) { setPending(null); setWaiveError(null); } }}>
|
|
252
|
+
<div
|
|
253
|
+
className="dsh-kb-rename-modal"
|
|
254
|
+
role="dialog"
|
|
255
|
+
aria-modal="true"
|
|
256
|
+
aria-label="豁免评审"
|
|
257
|
+
onClick={(e) => e.stopPropagation()}
|
|
258
|
+
>
|
|
259
|
+
<div className="dsh-kb-rename-modal__label">豁免评审</div>
|
|
260
|
+
<div className="dsh-kb-delete-modal__text">
|
|
261
|
+
豁免「{task.title}」({task.id})的评审结论:被评审卡置为 waived,链路按评审通过继续推进(该评审的遗留问题仍作为非阻塞建议传下游)。请填写豁免理由。
|
|
262
|
+
</div>
|
|
263
|
+
<input
|
|
264
|
+
className="dsh-kb-comment-input"
|
|
265
|
+
aria-label="豁免理由"
|
|
266
|
+
placeholder="豁免理由(必填)"
|
|
267
|
+
autoFocus
|
|
268
|
+
value={pending.value}
|
|
269
|
+
onChange={(e) => { setPending({ kind: 'waive', value: e.target.value }); if (waiveError) setWaiveError(null); }}
|
|
270
|
+
onKeyDown={(e) => { if (e.key === 'Enter') void submitWaive(); }}
|
|
271
|
+
/>
|
|
272
|
+
<div className="dsh-kb-rename-modal__hint">豁免理由必填(审计留痕);填写后点「确认豁免」。</div>
|
|
273
|
+
{waiveError && <div className="dsh-kb-delete-modal__error" role="alert">{waiveError}</div>}
|
|
274
|
+
{waiveOk && <div className="dsh-kb-rename-modal__success" role="status">✓ 已豁免,链路将继续推进</div>}
|
|
275
|
+
<div className="dsh-kb-rename-modal__actions">
|
|
276
|
+
<button type="button" className="dsh-kb-rename-cancel" disabled={waiveBusy} onClick={() => { setPending(null); setWaiveError(null); }}>取消</button>
|
|
277
|
+
<button type="button" className="dsh-kb-rename-save" disabled={waiveBusy || waiveOk} onClick={() => void submitWaive()}>
|
|
278
|
+
{waiveOk ? '已豁免' : waiveBusy ? '豁免中…' : '确认豁免'}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
212
284
|
</div>
|
|
213
285
|
);
|
|
214
286
|
}
|
package/client/WorkflowRail.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export function WorkflowRail(props: {
|
|
|
30
30
|
onRenameChain?(chainId: string, title: string): void;
|
|
31
31
|
/** 整链硬删除(POST /kanban/action {type:'delete', chainId});失败 throw 由弹窗展示。 */
|
|
32
32
|
onDeleteChain?(chainId: string): Promise<void>;
|
|
33
|
+
/** 人工恢复被 blocked 的链(POST /kanban/action {type:'reopen-chain', chainId, reason});失败 throw 由弹窗展示。 */
|
|
34
|
+
onReopenChain?(chainId: string, reason: string): Promise<void>;
|
|
33
35
|
}) {
|
|
34
36
|
const searching = props.query.trim().length > 0;
|
|
35
37
|
const visible = props.chains.filter((view) => matches(view, props.query));
|
|
@@ -37,6 +39,38 @@ export function WorkflowRail(props: {
|
|
|
37
39
|
const renamingChain = renamingChainId ? props.chains.find((v) => v.chain.id === renamingChainId)?.chain : undefined;
|
|
38
40
|
const [deletingChainId, setDeletingChainId] = useState<string | null>(null);
|
|
39
41
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
42
|
+
// 2026-09-15 恢复能力:blocked 链 → 人工恢复(理由必填,审计留痕)。
|
|
43
|
+
const [reopeningChainId, setReopeningChainId] = useState<string | null>(null);
|
|
44
|
+
const [reopenReason, setReopenReason] = useState('');
|
|
45
|
+
const [reopenError, setReopenError] = useState<string | null>(null);
|
|
46
|
+
const [reopenBusy, setReopenBusy] = useState(false);
|
|
47
|
+
const [reopenOk, setReopenOk] = useState(false);
|
|
48
|
+
const reopeningView = reopeningChainId ? props.chains.find((v) => v.chain.id === reopeningChainId) : undefined;
|
|
49
|
+
const closeReopen = () => {
|
|
50
|
+
setReopeningChainId(null);
|
|
51
|
+
setReopenReason('');
|
|
52
|
+
setReopenError(null);
|
|
53
|
+
setReopenOk(false);
|
|
54
|
+
};
|
|
55
|
+
/** 校验必给反馈(此前按钮 disabled 且无禁用样式 → 用户点击毫无反应);请求在途禁用防重复;成功后显式提示。 */
|
|
56
|
+
const confirmReopen = async () => {
|
|
57
|
+
if (!reopeningChainId) return;
|
|
58
|
+
if (!reopenReason.trim()) {
|
|
59
|
+
setReopenError('请填写恢复理由(必填,用于审计留痕)');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setReopenError(null);
|
|
63
|
+
setReopenBusy(true);
|
|
64
|
+
try {
|
|
65
|
+
await props.onReopenChain?.(reopeningChainId, reopenReason.trim());
|
|
66
|
+
setReopenOk(true); // 明确成功反馈,800ms 后自动关闭
|
|
67
|
+
window.setTimeout(() => closeReopen(), 800);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
setReopenError('恢复失败:' + String(err)); // 失败保留弹窗,理由可改后重试
|
|
70
|
+
} finally {
|
|
71
|
+
setReopenBusy(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
40
74
|
const deletingView = deletingChainId ? props.chains.find((v) => v.chain.id === deletingChainId) : undefined;
|
|
41
75
|
const confirmDelete = async () => {
|
|
42
76
|
if (!deletingChainId) return;
|
|
@@ -97,6 +131,7 @@ export function WorkflowRail(props: {
|
|
|
97
131
|
type="button"
|
|
98
132
|
className="dsh-kb-chain__delete"
|
|
99
133
|
aria-label="删除需求"
|
|
134
|
+
title="删除需求(整链硬删除,不可恢复)"
|
|
100
135
|
onClick={(e) => { e.stopPropagation(); setDeleteError(null); setDeletingChainId(view.chain.id); }}
|
|
101
136
|
onKeyDown={(e) => { e.stopPropagation(); }}
|
|
102
137
|
>
|
|
@@ -111,15 +146,29 @@ export function WorkflowRail(props: {
|
|
|
111
146
|
type="button"
|
|
112
147
|
className="dsh-kb-chain__rename"
|
|
113
148
|
aria-label="改链标题"
|
|
149
|
+
title="修改链路标题"
|
|
114
150
|
onClick={(e) => { e.stopPropagation(); setRenamingChainId(view.chain.id); }}
|
|
115
151
|
onKeyDown={(e) => { e.stopPropagation(); }}
|
|
116
152
|
>
|
|
117
153
|
✎
|
|
118
154
|
</button>
|
|
119
155
|
)}
|
|
156
|
+
{/* 人工恢复(2026-09-15):仅 blocked 链显示;文字按钮(低频高价值,明确性优先,⟳ 易被误读为刷新) */}
|
|
157
|
+
{props.onReopenChain && view.chain.status === 'blocked' && (
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
className="dsh-kb-chain__reopen"
|
|
161
|
+
aria-label="人工恢复链路"
|
|
162
|
+
title="将 blocked 链恢复为 executing,编排器继续推进后续阶段"
|
|
163
|
+
onClick={(e) => { e.stopPropagation(); setReopenError(null); setReopenReason(''); setReopeningChainId(view.chain.id); }}
|
|
164
|
+
onKeyDown={(e) => { e.stopPropagation(); }}
|
|
165
|
+
>
|
|
166
|
+
恢复
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
120
169
|
</div>
|
|
121
170
|
{(blocked || view.blockedSummary) && (
|
|
122
|
-
<div className="dsh-kb-chain__warning">{summary}</div>
|
|
171
|
+
<div className="dsh-kb-chain__warning" title={summary}>{summary}</div>
|
|
123
172
|
)}
|
|
124
173
|
{/* D23:completed 链存在未确认 audit-warning → 顶部警告行 + 确认按钮(阻塞最终汇报直至确认) */}
|
|
125
174
|
{view.audit && !view.audit.confirmed && (
|
|
@@ -151,6 +200,40 @@ export function WorkflowRail(props: {
|
|
|
151
200
|
onCancel={() => setRenamingChainId(null)}
|
|
152
201
|
/>
|
|
153
202
|
)}
|
|
203
|
+
{reopeningView && props.onReopenChain && (
|
|
204
|
+
<div className="dsh-kb-rename-overlay" onClick={(e) => { e.stopPropagation(); closeReopen(); }}>
|
|
205
|
+
<div
|
|
206
|
+
className="dsh-kb-rename-modal"
|
|
207
|
+
role="dialog"
|
|
208
|
+
aria-modal="true"
|
|
209
|
+
aria-label="人工恢复链路"
|
|
210
|
+
onClick={(e) => e.stopPropagation()}
|
|
211
|
+
>
|
|
212
|
+
<div className="dsh-kb-rename-modal__label">人工恢复链路</div>
|
|
213
|
+
<div className="dsh-kb-delete-modal__text">
|
|
214
|
+
将「{reopeningView.chain.title || '未命名需求'}」从 blocked 恢复为 executing,编排器继续推进后续阶段。
|
|
215
|
+
</div>
|
|
216
|
+
<input
|
|
217
|
+
className="dsh-kb-comment-input"
|
|
218
|
+
aria-label="恢复理由"
|
|
219
|
+
placeholder="恢复理由(必填)"
|
|
220
|
+
autoFocus
|
|
221
|
+
value={reopenReason}
|
|
222
|
+
onChange={(e) => { setReopenReason(e.target.value); if (reopenError) setReopenError(null); }}
|
|
223
|
+
onKeyDown={(e) => { if (e.key === 'Enter') void confirmReopen(); }}
|
|
224
|
+
/>
|
|
225
|
+
<div className="dsh-kb-rename-modal__hint">恢复理由必填(审计留痕);填写后点「确认恢复」。</div>
|
|
226
|
+
{reopenError && <div className="dsh-kb-delete-modal__error" role="alert">{reopenError}</div>}
|
|
227
|
+
{reopenOk && <div className="dsh-kb-rename-modal__success" role="status">✓ 已恢复,编排器将继续推进后续阶段</div>}
|
|
228
|
+
<div className="dsh-kb-rename-modal__actions">
|
|
229
|
+
<button type="button" className="dsh-kb-rename-cancel" disabled={reopenBusy} onClick={closeReopen}>取消</button>
|
|
230
|
+
<button type="button" className="dsh-kb-rename-save" disabled={reopenBusy || reopenOk} onClick={() => void confirmReopen()}>
|
|
231
|
+
{reopenOk ? '已恢复' : reopenBusy ? '恢复中…' : '确认恢复'}
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
154
237
|
{deletingView && props.onDeleteChain && (
|
|
155
238
|
<div className="dsh-kb-rename-overlay" onClick={(e) => { e.stopPropagation(); setDeletingChainId(null); }}>
|
|
156
239
|
<div
|
package/client/config-store.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface EditableSnapshot {
|
|
|
7
7
|
wikiVault: { baseUrl: string; pagePrefix: string };
|
|
8
8
|
roles: { models: Partial<Record<string, EditableModelSnapshot>> };
|
|
9
9
|
reviewEngine: { mode: 'delegate' | 'managed'; managed: { provider: string; model: string } };
|
|
10
|
+
/** 默认投递机器人(预设未命中的落点)。本面板不编辑:用户经 /sms 交互写入,原样回传避免清空。 */
|
|
11
|
+
imDelivery: { fallbackBotId: string };
|
|
10
12
|
}
|
|
11
13
|
/** GET /kanban/ocr/status 响应镜像;null = 尚未拉到/拉取失败(状态未知)。 */
|
|
12
14
|
export interface OcrStatus { installed: boolean; version: string; mode: string; kbMode: 'remote' | 'local'; managedReady: boolean; }
|
|
@@ -23,7 +25,7 @@ export interface ConfigState {
|
|
|
23
25
|
|
|
24
26
|
/** 配置外部 store(GET /kanban/config + /kanban/llm-catalog + ocr 运维面;PUT 保存;POST reset),fetch 注入便于测试。 */
|
|
25
27
|
export function createConfigStore(fetchImpl: typeof fetch) {
|
|
26
|
-
let state: ConfigState = { effective: { wikiVault: { baseUrl: '', pagePrefix: '' }, roles: { models: {} }, reviewEngine: { mode: 'delegate', managed: { provider: '', model: '' } } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: 'idle', log: '' }, saving: false, error: null };
|
|
28
|
+
let state: ConfigState = { effective: { wikiVault: { baseUrl: '', pagePrefix: '' }, roles: { models: {} }, reviewEngine: { mode: 'delegate', managed: { provider: '', model: '' } }, imDelivery: { fallbackBotId: '' } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: 'idle', log: '' }, saving: false, error: null };
|
|
27
29
|
const listeners = new Set<() => void>();
|
|
28
30
|
const setState = (patch: Partial<ConfigState>) => { state = { ...state, ...patch }; for (const l of [...listeners]) l(); };
|
|
29
31
|
const get = () => state;
|
package/client/kanban.css
CHANGED
|
@@ -399,6 +399,7 @@
|
|
|
399
399
|
padding: 4px 10px;
|
|
400
400
|
cursor: pointer;
|
|
401
401
|
font: inherit;
|
|
402
|
+
white-space: nowrap; /* 2026-09-15:横向空间不足时按钮文字曾被逐字竖排 */
|
|
402
403
|
transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease;
|
|
403
404
|
}
|
|
404
405
|
.dsh-kb-detail__actions button:hover {
|
|
@@ -722,6 +723,21 @@
|
|
|
722
723
|
cursor: pointer;
|
|
723
724
|
}
|
|
724
725
|
.dsh-kb-chain__rename:hover { opacity: 1; }
|
|
726
|
+
/* 人工恢复按钮(2026-09-15):仅 blocked 链显示,常显(低频高价值场景,明确性优先于紧凑性) */
|
|
727
|
+
.dsh-kb-chain__reopen {
|
|
728
|
+
flex: 0 0 auto;
|
|
729
|
+
border: 0;
|
|
730
|
+
background: transparent;
|
|
731
|
+
color: currentColor;
|
|
732
|
+
font: inherit;
|
|
733
|
+
font-size: 12px;
|
|
734
|
+
line-height: 1;
|
|
735
|
+
padding: 2px 6px;
|
|
736
|
+
border-radius: 4px;
|
|
737
|
+
cursor: pointer;
|
|
738
|
+
transition: background 150ms ease;
|
|
739
|
+
}
|
|
740
|
+
.dsh-kb-chain__reopen:hover { background: rgba(255, 255, 255, 0.12); }
|
|
725
741
|
.dsh-kb-task__status-row {
|
|
726
742
|
display: flex;
|
|
727
743
|
align-items: center;
|
|
@@ -803,6 +819,18 @@
|
|
|
803
819
|
background: var(--dsw-alias-button-primary-fill);
|
|
804
820
|
color: var(--dsw-alias-label-primary-foreground);
|
|
805
821
|
}
|
|
822
|
+
/* 2026-09-15:弹窗操作按钮的禁用/在途视觉(此前完全缺失——disabled 无视觉差异,点击像"没反应") */
|
|
823
|
+
.dsh-kb-rename-modal__actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
824
|
+
.dsh-kb-rename-modal__hint {
|
|
825
|
+
color: var(--dsh-kb-text-secondary);
|
|
826
|
+
font-size: 12px;
|
|
827
|
+
line-height: 1.4;
|
|
828
|
+
}
|
|
829
|
+
.dsh-kb-rename-modal__success {
|
|
830
|
+
color: #2ea043;
|
|
831
|
+
font-size: 12px;
|
|
832
|
+
line-height: 1.4;
|
|
833
|
+
}
|
|
806
834
|
|
|
807
835
|
/* Swarm 配置面板「评审引擎(ocr)」卡:安装中 spinner 行与状态行(成功绿 / 警示黄) */
|
|
808
836
|
.dsh-kb-config__installing {
|
package/client/timeline-model.ts
CHANGED
|
@@ -41,6 +41,8 @@ const STATUS_OF: Record<EventKind, TimelineStatus> = {
|
|
|
41
41
|
'chain/audit-confirmed': 'neutral',
|
|
42
42
|
'chain/title-updated': 'neutral',
|
|
43
43
|
'chain/im-delivery-failed': 'exception', // IM 投递失败留痕(非状态转换注记)
|
|
44
|
+
'chain/reopened': 'running', // 人工恢复(blocked → executing)
|
|
45
|
+
'review/waived': 'success', // 人工评审豁免(放行)
|
|
44
46
|
'spec-card/created': 'neutral',
|
|
45
47
|
'spec-card/edited': 'neutral',
|
|
46
48
|
'spec-card/approved': 'success',
|
|
@@ -77,6 +79,7 @@ const KIND_LABEL: Record<EventKind, string> = {
|
|
|
77
79
|
'chain/audit-confirmed': '越权已确认',
|
|
78
80
|
'chain/title-updated': '链路改名',
|
|
79
81
|
'chain/im-delivery-failed': 'IM 投递失败',
|
|
82
|
+
'chain/reopened': '人工恢复链路',
|
|
80
83
|
'spec-card/created': '规格卡创建',
|
|
81
84
|
'spec-card/edited': '规格卡编辑',
|
|
82
85
|
'spec-card/approved': '规格卡批准',
|
|
@@ -95,6 +98,7 @@ const KIND_LABEL: Record<EventKind, string> = {
|
|
|
95
98
|
'review/passed': '评审通过',
|
|
96
99
|
'review/failed': '评审驳回',
|
|
97
100
|
'review/gave-up': '评审超限放弃',
|
|
101
|
+
'review/waived': '评审豁免',
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
/** author id → 友好名(复用 ROLE_NAME 语义,system/human 另映射)。 */
|
package/client/workflow-model.ts
CHANGED
|
@@ -117,19 +117,28 @@ function activityLabel(task: Task, state: BoardState, now: number): string {
|
|
|
117
117
|
return `${Math.floor(diff / 86_400_000)}d`;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** 链头警告摘要(2026-09-15 修正):只反映"当前"状态,不粘历史。
|
|
121
|
+
* 旧实现是纯历史扫描(取链上 seq 最大的 task/blocked|task/failed 的 reason),不看该卡当前状态——
|
|
122
|
+
* 历史阻塞解除/链恢复后警告行永久常驻,且真正停因(chain/blocked,如 stall-watchdog)从不显示。
|
|
123
|
+
* 新优先级:① 链级 blocked → chain/blocked 的 reason("链为什么停"的第一手原因);
|
|
124
|
+
* ② 否则 → 当前处于 blocked/failed 的卡自身最后一次 block/fail 原因;
|
|
125
|
+
* ③ 均无 → null(警告行不显示;历史原因仍可在卡轨迹查看)。 */
|
|
120
126
|
function blockedSummary(chainId: string, state: BoardState): string | null {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (ev.chainId !== chainId) continue;
|
|
125
|
-
if (ev.kind === 'task/blocked' || ev.kind === 'task/failed') {
|
|
126
|
-
if (ev.seq > latestSeq) {
|
|
127
|
-
latestSeq = ev.seq;
|
|
128
|
-
latest = String(ev.payload['reason'] ?? '');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
127
|
+
if (state.chains.get(chainId)?.status === 'blocked') {
|
|
128
|
+
const ev = [...state.events].reverse().find((e) => e.chainId === chainId && e.kind === 'chain/blocked');
|
|
129
|
+
return String(ev?.payload['reason'] ?? '') || '链级阻塞(原因未记录)';
|
|
131
130
|
}
|
|
132
|
-
|
|
131
|
+
const blockedIds = new Set(
|
|
132
|
+
[...state.tasks.values()]
|
|
133
|
+
.filter((t) => t.chainId === chainId && (t.status === 'blocked' || t.status === 'failed'))
|
|
134
|
+
.map((t) => t.id),
|
|
135
|
+
);
|
|
136
|
+
if (blockedIds.size === 0) return null;
|
|
137
|
+
const ev = [...state.events]
|
|
138
|
+
.reverse()
|
|
139
|
+
.find((e) => e.chainId === chainId && e.taskId !== null && blockedIds.has(e.taskId)
|
|
140
|
+
&& (e.kind === 'task/blocked' || e.kind === 'task/failed'));
|
|
141
|
+
return ev ? String(ev.payload['reason'] ?? '') : null;
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
function relatedIds(state: BoardState, chainId: string, selectedTaskId: string): Set<string> {
|