@sidady/dsh-steward 0.9.14 → 0.9.16
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/ACCEPTANCE.md +136 -26
- package/README.md +114 -2
- package/lib/actions.js +49 -1
- package/lib/client.js +160 -74
- package/lib/index.js +104 -6
- package/lib/report.js +242 -9
- package/lib/settings.js +2 -2
- package/package.json +1 -1
- package/smoke.mjs +291 -3
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-steward · client entry — v0.9.
|
|
2
|
+
* dsh-steward · client entry — v0.9.16 (L0 observe-only duty loop + model-mode entry + L1 briefing;
|
|
3
3
|
* ChatBar rebuilt as a mature chat composer: one rounded input bar, mode chip on the
|
|
4
4
|
* left, round send button on the right, settings panel below the bar;
|
|
5
5
|
* v0.7.5 collapse: a collapsed group lists EVERY peer as a two-line summary row
|
|
@@ -99,10 +99,10 @@ window.__ModuleLoader__.load({
|
|
|
99
99
|
// finding → 动作直达映射(点按钮 = 切到动作中心并展开该动作)
|
|
100
100
|
const ACTION_FOR = {
|
|
101
101
|
'data-at-risk': 'backup',
|
|
102
|
+
'backup-freshness': 'backup',
|
|
102
103
|
'boot-crash-latest': 'disable',
|
|
103
104
|
'disk-advice': 'clean',
|
|
104
105
|
'knowledge': 'knowledge',
|
|
105
|
-
'model-health': 'model-default',
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
// v0.9.8: every request used to be able to hang forever. The server is allowed
|
|
@@ -210,7 +210,12 @@ window.__ModuleLoader__.load({
|
|
|
210
210
|
// a green primary button reads as "this is healthy", which is data.
|
|
211
211
|
border: '1px solid ' + (props.primary ? 'transparent' : C.line),
|
|
212
212
|
background: props.primary && !props.disabled ? C.solid : C.fill,
|
|
213
|
-
|
|
213
|
+
// A primary button inverts the surface: its foreground MUST be the
|
|
214
|
+
// inverted fg. C.text and C.solid both map to --dsw-alias-label-primary,
|
|
215
|
+
// so letting a caller's `tone` win here paints white-on-white (a blank
|
|
216
|
+
// bar instead of a button). Colour is reserved for data — emphasis is
|
|
217
|
+
// inversion, and inversion cannot be overridden by an accent.
|
|
218
|
+
color: props.primary && !props.disabled ? C.on : (props.tone || C.text), marginRight: 8,
|
|
214
219
|
},
|
|
215
220
|
}, props.children);
|
|
216
221
|
}
|
|
@@ -265,6 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
265
270
|
},
|
|
266
271
|
h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 4 } },
|
|
267
272
|
f.muted ? h(Badge, { color: C.faint }, '已静音') : h(Badge, { color: sev.color }, sev.label),
|
|
273
|
+
f.falsePositive ? h(Badge, { color: C.faint }, '已判错') : null,
|
|
268
274
|
typeof f.score === 'number' ? h('span', {
|
|
269
275
|
title: (f.scoreReasons || []).join(';') || '评分构成见体检报告',
|
|
270
276
|
style: { fontSize: 11, color: C.faint, marginLeft: 6, cursor: 'help' },
|
|
@@ -281,9 +287,11 @@ window.__ModuleLoader__.load({
|
|
|
281
287
|
style: { fontSize: 11.5, marginTop: 6, padding: '5px 8px', borderRadius: 6, background: C.fill, border: '1px dashed ' + C.info, color: C.sub, whiteSpace: 'pre-wrap' },
|
|
282
288
|
}, '🧠 上次也出现过 · ' + localStamp(f.recall.at) + ':「' + (f.recall.headline || '') + '」(当时严重 ' + ((f.recall.counts && f.recall.counts.critical) || 0) + ' / 留意 ' + ((f.recall.counts && f.recall.counts.warning) || 0) + '),详情见「审计趋势」的记录)') : null,
|
|
283
289
|
h('div', { style: { marginTop: 8, display: 'flex', gap: 0, flexWrap: 'wrap' } },
|
|
284
|
-
props.onGo ? h(Btn, { onClick: props.onGo, primary: true
|
|
290
|
+
props.onGo ? h(Btn, { onClick: props.onGo, primary: true }, props.goLabel || '去处置 →') : null,
|
|
285
291
|
props.onAck ? h(TinyBtn, { onClick: props.onAck }, '已知晓(静音此项)') : null,
|
|
286
292
|
props.onUnack ? h(TinyBtn, { onClick: props.onUnack, tone: C.info }, '恢复显示此项') : null,
|
|
293
|
+
props.onFalsePositive ? h(TinyBtn, { onClick: props.onFalsePositive, tone: C.warning }, '这条判错了') : null,
|
|
294
|
+
props.onUnfalsePositive ? h(TinyBtn, { onClick: props.onUnfalsePositive, tone: C.info }, '撤销判错') : null,
|
|
287
295
|
),
|
|
288
296
|
);
|
|
289
297
|
}
|
|
@@ -411,16 +419,25 @@ window.__ModuleLoader__.load({
|
|
|
411
419
|
return arr;
|
|
412
420
|
};
|
|
413
421
|
// 台账行必须说人话:内部 kind/error 码是机器字段,直接 dump 给用户等于没做呈现层。
|
|
414
|
-
const KIND_ZH = { clean: '磁盘清理', backup: '加密备份', disable: '停用插件', enable: '启用插件', knowledge: '沉淀知识', ack: '静音一项发现', unack: '恢复一项发现' };
|
|
422
|
+
const KIND_ZH = { clean: '磁盘清理', backup: '加密备份', disable: '停用插件', enable: '启用插件', knowledge: '沉淀知识', ack: '静音一项发现', unack: '恢复一项发现', feedback: '标记一条判错', unfeedback: '撤销判错标记' };
|
|
415
423
|
const ERR_ZH = {
|
|
416
424
|
'unknown-or-expired-plan': '计划已过期或指纹不对——指纹是一次性的,请重新「预览计划」',
|
|
417
425
|
'confirm-missing': '缺少授权确认——执行必须带 confirm:true',
|
|
418
426
|
};
|
|
419
427
|
const countsZh = (c) => (c ? ['严重 ' + (c.critical || 0), '留意 ' + (c.warning || 0), '提示 ' + (c.info || 0)].join(' · ') + ((c.muted || 0) > 0 ? ' · 静音 ' + c.muted : '') : '');
|
|
428
|
+
const tv = data && data.trendVerdict;
|
|
420
429
|
return h('div', {},
|
|
421
430
|
h('div', { style: { fontSize: 12, color: C.sub, lineHeight: 1.7, margin: '2px 0 10px', padding: '8px 10px', borderLeft: '2px solid ' + C.info, background: C.fill, borderRadius: 6 } },
|
|
422
431
|
'这里是管家的操作台账:它经你授权动过系统的每一次记录都写在这里,包括被拒绝的尝试。', h('br'),
|
|
423
432
|
'用途:核对「上次到底清了什么、什么时候、成没成」,以及看体检结论随时间是变好还是变坏。'),
|
|
433
|
+
tv && tv.text ? h('div', { style: { fontSize: 12, marginBottom: 10, padding: '7px 10px', borderRadius: 6, borderLeft: '2px solid ' + (tv.state === 'recovered' ? C.ok : tv.state === 'worsening' ? C.warning : C.info), background: C.fill, color: C.text } }, '趋势结论 · ' + tv.text) : null,
|
|
434
|
+
data && data.duty ? h('div', { style: { fontSize: 12, marginBottom: 10, padding: '8px 10px', borderRadius: 6, borderLeft: '2px solid ' + C.ok, background: C.fill, color: C.text, lineHeight: 1.7 } },
|
|
435
|
+
h('div', { style: { fontWeight: 600, marginBottom: 4 } }, '当班台账 · ' + (data.duty.latest ? localStamp(data.duty.latest.at) : '还没当过班') + '(L0 观察档 · 每 ' + data.duty.intervalMinutes + ' 分钟一班)'),
|
|
436
|
+
data.duty.latest ? h('div', {}, data.duty.latest.seen) : h('div', { style: { color: C.faint } }, '管家会在后台按周期自己醒来体检;这一版只观察、只把建议备好,不代替你按确认。'),
|
|
437
|
+
data.duty.latest ? h('div', {}, data.duty.latest.action) : null,
|
|
438
|
+
data.duty.suggestions && data.duty.suggestions.length ? h('div', { style: { marginTop: 4, color: C.sub } }, '预备好的处置建议(等你确认):' + data.duty.suggestions.map((s) => s.text).join(';')) : null,
|
|
439
|
+
Array.isArray(data.duty.log) && data.duty.log.length > 1 ? h('div', { style: { marginTop: 4, color: C.faint, fontSize: 11 } }, '最近 ' + data.duty.log.length + ' 班:' + data.duty.log.slice(-5).map((d) => localStamp(d.at) + ' ' + d.state).join(' · ')) : null,
|
|
440
|
+
) : null,
|
|
424
441
|
h('div', { style: { marginBottom: 10, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
425
442
|
h(Btn, { onClick: load, disabled: loading }, loading ? '读取中…' : '刷新台账'),
|
|
426
443
|
// 不用原生 <select>:Windows 下拉弹层由系统绘制,无视宿主主题(白底浅字不可读)。分段按钮全部内联样式,跟随面板配色。
|
|
@@ -463,7 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
463
480
|
}
|
|
464
481
|
|
|
465
482
|
// 模块级缓存:Panel 切走即卸载;报告与插件变更留在内存(刷新清空属预期)。
|
|
466
|
-
const cache = { report: null, error: '', hidden: {}, auditSort: 'newest', mode: 'rules', model: null };
|
|
483
|
+
const cache = { report: null, error: '', hidden: {}, auditSort: 'newest', mode: 'rules', model: null, autoScanMinutes: 10, wiring: null, duty: null };
|
|
467
484
|
// v0.8.4:当前生效的入口重挂函数,由 apply() 填、由设置页调用。
|
|
468
485
|
// 存在它就不需要「改完要 Ctrl+R」——那前提在桌面版里根本不成立。
|
|
469
486
|
// ---------- 右下角临时弹窗(v0.9.1) ----------
|
|
@@ -524,6 +541,31 @@ window.__ModuleLoader__.load({
|
|
|
524
541
|
toastedIds.add(f.id);
|
|
525
542
|
pushToast(toastOf(f));
|
|
526
543
|
}
|
|
544
|
+
// v0.9.15: 好转与连续恶化也主动说。结论由服务端算好人话,客户端只渲染。
|
|
545
|
+
const tv = report && report.trendVerdict;
|
|
546
|
+
if (tv && (tv.state === 'recovered' || (tv.state === 'worsening' && tv.worseningStreak >= 2))) {
|
|
547
|
+
const key = 'trend-' + tv.state + '-' + (report.generatedAt || '');
|
|
548
|
+
if (!toastedIds.has(key)) {
|
|
549
|
+
toastedIds.add(key);
|
|
550
|
+
pushToast({ kind: 'trend', findingId: null, actionKey: null,
|
|
551
|
+
severity: tv.state === 'recovered' ? 'ok' : 'info',
|
|
552
|
+
title: tv.state === 'recovered' ? '状况好转了' : '连续几次体检都在变差',
|
|
553
|
+
body: tv.text || '' });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// v0.9.16(未发布):插件新版本——服务端已算好利弊/风险/建议,客户端只弹一次。
|
|
557
|
+
// 不改 toastWorthy(info 级 finding 仍不全局放开),用独立 key 按版本指纹去重。
|
|
558
|
+
const ua = report && report.updates && report.updates.advice;
|
|
559
|
+
if (ua && ua.fingerprint && ua.outdatedCount > 0 && toastConfig.mode !== 'off') {
|
|
560
|
+
const vkey = 'version-' + ua.fingerprint;
|
|
561
|
+
if (!toastedIds.has(vkey)) {
|
|
562
|
+
toastedIds.add(vkey);
|
|
563
|
+
pushToast({ kind: 'version', findingId: null, actionKey: null,
|
|
564
|
+
severity: 'info',
|
|
565
|
+
title: ua.toastTitle || '有插件出了新版本',
|
|
566
|
+
body: ua.toastBody || '' });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
527
569
|
}
|
|
528
570
|
|
|
529
571
|
// 弹窗住在 shell.overlay 里,不在面板的树里 —— 点击「去看看」需要一条通道
|
|
@@ -1002,7 +1044,7 @@ window.__ModuleLoader__.load({
|
|
|
1002
1044
|
const gs = Array.isArray(res.groups) ? res.groups : [];
|
|
1003
1045
|
setGroups(gs);
|
|
1004
1046
|
setDraft(flatOf(gs));
|
|
1005
|
-
setSaved(res.savedAt ? "上次保存:" +
|
|
1047
|
+
setSaved(res.savedAt ? "上次保存:" + localStamp(res.savedAt) + "(本地时间)" : "还没改过(全部是推荐默认值)");
|
|
1006
1048
|
} catch (e) { setError(e && e.message ? e.message : String(e)); }
|
|
1007
1049
|
finally { setLoading(false); }
|
|
1008
1050
|
}, []);
|
|
@@ -1106,7 +1148,7 @@ window.__ModuleLoader__.load({
|
|
|
1106
1148
|
const timer = setTimeout(() => { setLeft((v) => v - 1); }, 1000);
|
|
1107
1149
|
return () => clearTimeout(timer);
|
|
1108
1150
|
}, [left, paused]);
|
|
1109
|
-
const tone = t.severity === 'critical' ? C.critical : C.warning;
|
|
1151
|
+
const tone = t.severity === 'critical' ? C.critical : t.severity === 'ok' ? C.ok : t.severity === 'info' ? C.info : C.warning;
|
|
1110
1152
|
return h('div', {
|
|
1111
1153
|
// 鼠标移上去暂停计时:正在读的时候消失,比不弹更烦人。
|
|
1112
1154
|
onMouseEnter: () => setPaused(true),
|
|
@@ -1119,7 +1161,7 @@ window.__ModuleLoader__.load({
|
|
|
1119
1161
|
},
|
|
1120
1162
|
},
|
|
1121
1163
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 } },
|
|
1122
|
-
h('span', { style: { fontSize: 10.5, color: tone, fontWeight: 600, flex: '0 0 auto' } }, t.severity === 'critical' ? '严重' : '留意'),
|
|
1164
|
+
h('span', { style: { fontSize: 10.5, color: tone, fontWeight: 600, flex: '0 0 auto' } }, t.severity === 'critical' ? '严重' : t.severity === 'ok' ? '好转' : t.severity === 'info' ? '提醒' : '留意'),
|
|
1123
1165
|
h('span', { style: { fontSize: 13, fontWeight: 600, flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t.title),
|
|
1124
1166
|
h('button', {
|
|
1125
1167
|
onClick: props.onDismiss, title: '关闭',
|
|
@@ -1128,15 +1170,15 @@ window.__ModuleLoader__.load({
|
|
|
1128
1170
|
),
|
|
1129
1171
|
t.body ? h('div', { style: { fontSize: 11.5, color: C.sub, marginBottom: 7, whiteSpace: 'pre-wrap' } }, t.body) : null,
|
|
1130
1172
|
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
1131
|
-
h('button', {
|
|
1173
|
+
t.actionKey ? h('button', {
|
|
1132
1174
|
onClick: () => { props.onOpen(); props.onDismiss(); },
|
|
1133
1175
|
style: { cursor: 'pointer', fontSize: 11.5, padding: '3px 10px', borderRadius: 6, border: '1px solid ' + tone, background: 'transparent', color: tone },
|
|
1134
|
-
}, '去看看'),
|
|
1135
|
-
h('button', {
|
|
1176
|
+
}, '去看看') : null,
|
|
1177
|
+
t.findingId ? h('button', {
|
|
1136
1178
|
onClick: () => { props.onAck(); props.onDismiss(); },
|
|
1137
1179
|
title: '静音这条,直到有效期结束',
|
|
1138
1180
|
style: { cursor: 'pointer', fontSize: 11.5, padding: '3px 10px', borderRadius: 6, border: '1px solid ' + C.line, background: 'transparent', color: C.sub },
|
|
1139
|
-
}, '已知晓'),
|
|
1181
|
+
}, '已知晓') : null,
|
|
1140
1182
|
h('span', { style: { flex: '1 1 auto' } }),
|
|
1141
1183
|
h('span', { style: { fontSize: 10, color: C.faint, flex: '0 0 auto' } }, paused ? '暂停' : left + 's'),
|
|
1142
1184
|
),
|
|
@@ -1175,6 +1217,9 @@ window.__ModuleLoader__.load({
|
|
|
1175
1217
|
try {
|
|
1176
1218
|
const data = await call('report');
|
|
1177
1219
|
if (!alive) return;
|
|
1220
|
+
if (data && typeof data.autoScanMinutes === 'number') cache.autoScanMinutes = data.autoScanMinutes;
|
|
1221
|
+
if (data && data.wiring) cache.wiring = data.wiring;
|
|
1222
|
+
if (data && data.duty) cache.duty = data.duty;
|
|
1178
1223
|
if (data && data.report) { cache.report = data.report; evaluateToasts(data.report); }
|
|
1179
1224
|
} catch { /* 采集失败不弹任何东西——宁可不提醒,也不误报 */ }
|
|
1180
1225
|
})();
|
|
@@ -1218,6 +1263,9 @@ window.__ModuleLoader__.load({
|
|
|
1218
1263
|
setLoading(true); setError('');
|
|
1219
1264
|
try {
|
|
1220
1265
|
const data = await call('report');
|
|
1266
|
+
if (data && typeof data.autoScanMinutes === 'number') cache.autoScanMinutes = data.autoScanMinutes;
|
|
1267
|
+
if (data && data.wiring) cache.wiring = data.wiring;
|
|
1268
|
+
if (data && data.duty) cache.duty = data.duty;
|
|
1221
1269
|
try { publishReport(data && data.report); } catch { /* noop */ }
|
|
1222
1270
|
cache.report = data.report || null;
|
|
1223
1271
|
cache.error = '';
|
|
@@ -1233,12 +1281,13 @@ window.__ModuleLoader__.load({
|
|
|
1233
1281
|
try { setBundles(await call('bundles')); } catch { setBundles(null); }
|
|
1234
1282
|
}, []);
|
|
1235
1283
|
|
|
1236
|
-
//
|
|
1284
|
+
// 回到面板不再要求手点:无缓存→挂载即自动体检一次;缓存超过设置的分钟数→后台刷新。
|
|
1237
1285
|
// 先例:disk-cleaner 面板同样挂载即扫,同一 GUI 正常运行——二分定案证明卡死元凶不是自动扫描。
|
|
1238
1286
|
useEffect(() => {
|
|
1239
1287
|
if (!cache.report) { void scan(); return; }
|
|
1240
1288
|
const age = Date.now() - Date.parse(String(cache.report.generatedAt || ''));
|
|
1241
|
-
|
|
1289
|
+
const maxAge = (Number.isFinite(cache.autoScanMinutes) ? cache.autoScanMinutes : 10) * 60 * 1000;
|
|
1290
|
+
if (Number.isFinite(age) && age > maxAge) void scan();
|
|
1242
1291
|
}, [scan]);
|
|
1243
1292
|
|
|
1244
1293
|
const crashFinding = report && Array.isArray(report.findings)
|
|
@@ -1270,6 +1319,16 @@ window.__ModuleLoader__.load({
|
|
|
1270
1319
|
await scan();
|
|
1271
1320
|
} catch (e) { setError(e && e.message ? e.message : String(e)); }
|
|
1272
1321
|
}, [scan]);
|
|
1322
|
+
const falsePositiveIt = useCallback(async (finding, undo) => {
|
|
1323
|
+
try {
|
|
1324
|
+
const p = await call('plan', { kind: undo ? 'unfeedback' : 'feedback', id: finding.id });
|
|
1325
|
+
if (p.ok === false) throw new Error(p.error || 'plan 失败');
|
|
1326
|
+
await call('execute', { planHash: p.planHash, confirm: true });
|
|
1327
|
+
await scan();
|
|
1328
|
+
} catch (e) { setError(e && e.message ? e.message : String(e)); }
|
|
1329
|
+
}, [scan]);
|
|
1330
|
+
const falsePositiveRef = useRef(falsePositiveIt);
|
|
1331
|
+
falsePositiveRef.current = falsePositiveIt;
|
|
1273
1332
|
|
|
1274
1333
|
// 把面板内的能力交给 overlay 里的弹窗宿主。
|
|
1275
1334
|
React.useEffect(() => {
|
|
@@ -1316,8 +1375,10 @@ window.__ModuleLoader__.load({
|
|
|
1316
1375
|
finding: f,
|
|
1317
1376
|
onAck: !f.muted ? () => ackIt(f) : null,
|
|
1318
1377
|
onUnack: f.muted ? () => unackIt(f) : null,
|
|
1378
|
+
onFalsePositive: !f.falsePositive ? () => falsePositiveIt(f, false) : null,
|
|
1379
|
+
onUnfalsePositive: f.falsePositive ? () => falsePositiveIt(f, true) : null,
|
|
1319
1380
|
onGo: ACTION_FOR[f.id] && !f.muted && f.severity !== 'info' ? () => jumpToAction(ACTION_FOR[f.id]) : null,
|
|
1320
|
-
goLabel: f.id === 'data-at-risk' ? '去备份 →' : f.id === 'boot-crash-latest' ? '去崩溃处置 →' : f.id === 'disk-advice' ? '去看可清理 →' : null,
|
|
1381
|
+
goLabel: f.id === 'data-at-risk' || f.id === 'backup-freshness' ? '去备份 →' : f.id === 'boot-crash-latest' ? '去崩溃处置 →' : f.id === 'disk-advice' ? '去看可清理 →' : null,
|
|
1321
1382
|
});
|
|
1322
1383
|
const groupBlock = (label, color, list) => {
|
|
1323
1384
|
if (!list.length) return null;
|
|
@@ -1423,6 +1484,12 @@ window.__ModuleLoader__.load({
|
|
|
1423
1484
|
h('div', {}, '动作执行全部写入 audit.jsonl,可在「审计趋势」回看。'),
|
|
1424
1485
|
h('div', { style: { marginTop: 4 } }, '依赖清单与降级行为、设计取舍见插件目录 README(dsh-steward/README.md)。')) : null,
|
|
1425
1486
|
report ? h('div', { style: { fontSize: 10.5, color: C.faint, marginTop: 8 } }, '生成时间 · ' + localStamp(report.generatedAt) + (agoText(report.generatedAt) ? '(' + agoText(report.generatedAt) + ')' : '')) : null,
|
|
1487
|
+
report && cache.wiring ? h('div', { style: { fontSize: 10.5, marginTop: 4, color: cache.wiring.route === 'registered' ? C.faint : C.critical } },
|
|
1488
|
+
cache.wiring.route === 'registered'
|
|
1489
|
+
? '接口已挂上 · 路由与工具随服务就绪后注册'
|
|
1490
|
+
: '接口未挂上(' + String(cache.wiring.route) + ')——停用再启用本插件,或重启 DSH Web') : null,
|
|
1491
|
+
cache.duty ? h('div', { style: { fontSize: 10.5, marginTop: 4, color: C.faint } },
|
|
1492
|
+
'当班 · ' + (cache.duty.lastAt ? localStamp(cache.duty.lastAt) : '待首班') + ' · L0 观察档(每 ' + cache.duty.intervalMinutes + ' 分钟自检一次,只观察不动作)') : null,
|
|
1426
1493
|
) : null,
|
|
1427
1494
|
|
|
1428
1495
|
tab === 'actions' ? h('div', { style: { marginTop: 12 } },
|
|
@@ -1496,81 +1563,99 @@ window.__ModuleLoader__.load({
|
|
|
1496
1563
|
return {
|
|
1497
1564
|
inject: ['slots'],
|
|
1498
1565
|
apply(ctx) {
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1566
|
+
// 三处注册彼此独立、按入口状态幂等地挂/卸,只在插件整体卸载时由框架级联销毁。
|
|
1567
|
+
// 曾经的 bug:把右侧栏 tab 的「类型 + 正文 + 浮层提醒」全塞进 mountRight(),又让
|
|
1568
|
+
// mountFor() 每次都 disposeAllNow() 全拆一遍。ui.entry='right' 时初次挂 both、异步
|
|
1569
|
+
// 读到设置后再挂 right,这第二次重挂会把刚注册好的正文槽位 dispose 掉;更糟的是
|
|
1570
|
+
// 正文注册还嵌在 ctx.inject(['sidebarRightTabs']) 的回调里,服务重载时机一错就丢。
|
|
1571
|
+
// 宿主把它当成「会话日志里恢复的、插件已不再挂载的旧 tab」,显示「这类内容还没有
|
|
1572
|
+
// 可用的查看方式。」——芯片标题是打开时捕获的,所以标题还在、正文空了。
|
|
1573
|
+
// 现在:右侧整套按 wantRight 幂等挂/卸(wantRight 不变就不拆不挂),正文槽位顶层
|
|
1574
|
+
// 直接挂、不再依赖 sidebarRightTabs 服务的回调,浮层提醒独立常驻不进任何拆装。
|
|
1575
|
+
// 对照:config-manager / better-sidebar / univer-office 都把正文槽位顶层一次性注册。
|
|
1576
|
+
|
|
1577
|
+
// ① 右侧栏 tab:类型 + 正文。用 rightMounted 做幂等门,避免每次 mountFor 反复拆装。
|
|
1578
|
+
let rightMounted = false;
|
|
1579
|
+
let rightDisposers = [];
|
|
1580
|
+
const unmountRight = () => {
|
|
1581
|
+
for (const d of rightDisposers) { try { d(); } catch { /* noop */ } }
|
|
1582
|
+
rightDisposers = [];
|
|
1583
|
+
rightMounted = false;
|
|
1509
1584
|
};
|
|
1510
|
-
|
|
1511
1585
|
const mountRight = () => {
|
|
1512
|
-
|
|
1586
|
+
if (rightMounted) return; // 幂等:wantRight 没变就别动,否则正文会被二次重挂 dispose 掉
|
|
1587
|
+
rightMounted = true;
|
|
1588
|
+
// 正文(keyed 槽位):key 恒为 PANEL_ID(= 类型实现 id)。顶层直接挂,
|
|
1589
|
+
// 不夹带在 sidebarRightTabs 的 inject 回调里——服务重载不会连累正文。
|
|
1590
|
+
try {
|
|
1591
|
+
rightDisposers.push(ctx.slots.inject('sidebar.right.pane.tab', () =>
|
|
1592
|
+
ctx.slots.register({ name: 'sidebar.right.pane.tab', key: PANEL_ID }, (p) =>
|
|
1593
|
+
h(Panel, { host: (p && p.host) || 'sidebar' })),
|
|
1594
|
+
));
|
|
1595
|
+
} catch { /* sidebar body unavailable — contained */ }
|
|
1596
|
+
// 类型(sidebarRightTabs):服务可能晚于 apply 就绪,必须用 ctx.inject 延迟注册;
|
|
1597
|
+
// 用注入进来的 scoped ctx 的 effect 管生命周期,服务重跑时先 dispose 旧类型再注册。
|
|
1513
1598
|
try {
|
|
1514
1599
|
const off = ctx.inject(['sidebarRightTabs'], (c) => {
|
|
1515
1600
|
try {
|
|
1516
1601
|
const svc = c && c.sidebarRightTabs;
|
|
1517
1602
|
if (!svc || typeof svc.register !== 'function') return;
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
h(Panel, { host: (p && p.host) || 'sidebar' })),
|
|
1526
|
-
));
|
|
1603
|
+
c.effect(() => {
|
|
1604
|
+
const dispose = svc.register({
|
|
1605
|
+
id: PANEL_ID, kind: PANEL_ID, title: () => '智能管家',
|
|
1606
|
+
guide: [{ id: PANEL_ID, order: 25, title: () => '智能管家', description: () => '稳定 · 可靠 · 整洁 · 知识的授权守护', icon: PanelIcon }],
|
|
1607
|
+
});
|
|
1608
|
+
return () => { try { if (typeof dispose === 'function') dispose(); } catch { /* noop */ } };
|
|
1609
|
+
}, 'dsh-steward: right tab type');
|
|
1527
1610
|
} catch { /* sidebar unavailable — contained */ }
|
|
1528
1611
|
});
|
|
1529
|
-
if (typeof off === 'function')
|
|
1612
|
+
if (typeof off === 'function') rightDisposers.push(off);
|
|
1530
1613
|
} catch { /* inject unsupported — contained */ }
|
|
1531
|
-
return () => { for (const d of disposers) { try { d(); } catch { /* noop */ } } };
|
|
1532
1614
|
};
|
|
1533
1615
|
|
|
1534
|
-
//
|
|
1535
|
-
// 单独挂一次,且只挂一次(重复注册会让同一个提醒弹两遍)。
|
|
1616
|
+
// ② 浮层提醒:与入口无关,独立挂一次;不再放进任何会被 disposeAllNow 清掉的列表。
|
|
1536
1617
|
try {
|
|
1537
|
-
|
|
1538
|
-
ctx.slots.
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
try { if (PANEL_NAVIGATOR) PANEL_NAVIGATOR(actionKey); } catch { /* noop */ }
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
)));
|
|
1618
|
+
ctx.effect(() => {
|
|
1619
|
+
const disposeToast = ctx.slots.inject('shell.overlay', () =>
|
|
1620
|
+
ctx.slots.register(
|
|
1621
|
+
{ name: 'shell.overlay', id: PANEL_ID + '-toast', order: 900, label: () => '智能管家提醒' },
|
|
1622
|
+
() => h(ToastHost, {
|
|
1623
|
+
onOpen: (actionKey) => { try { if (PANEL_NAVIGATOR) PANEL_NAVIGATOR(actionKey); } catch { /* noop */ } },
|
|
1624
|
+
onAck: (findingId) => { try { if (PANEL_ACK && findingId) PANEL_ACK(findingId); } catch { /* noop */ } },
|
|
1625
|
+
}),
|
|
1626
|
+
));
|
|
1627
|
+
return () => { try { if (typeof disposeToast === 'function') disposeToast(); } catch { /* noop */ } };
|
|
1628
|
+
}, 'dsh-steward: toast overlay');
|
|
1549
1629
|
} catch { /* overlay 不可用:提醒降级为只出现在面板里,不影响体检 */ }
|
|
1550
1630
|
|
|
1551
|
-
// entry
|
|
1552
|
-
|
|
1553
|
-
|
|
1631
|
+
// ③ 左侧入口 / main:随 ui.entry 切换。这部分拆装只管 left/main,不碰右侧 tab。
|
|
1632
|
+
let leftMounted = false;
|
|
1633
|
+
let currentDisposers = [];
|
|
1634
|
+
const disposeAllNow = () => {
|
|
1635
|
+
for (const d of currentDisposers) { try { d(); } catch { /* noop */ } }
|
|
1636
|
+
currentDisposers = [];
|
|
1637
|
+
};
|
|
1638
|
+
const mountLeft = () => {
|
|
1639
|
+
if (leftMounted) return;
|
|
1640
|
+
leftMounted = true;
|
|
1641
|
+
try {
|
|
1642
|
+
currentDisposers.push(ctx.slots.inject('sidebar.panellist', () =>
|
|
1643
|
+
ctx.slots.register(
|
|
1644
|
+
{ name: 'sidebar.panellist', id: PANEL_ID, order: 55, label: () => '智能管家' },
|
|
1645
|
+
(p) => h(PanelIcon, { size: p && p.size }),
|
|
1646
|
+
)));
|
|
1647
|
+
} catch { /* contained */ }
|
|
1648
|
+
try {
|
|
1649
|
+
currentDisposers.push(ctx.slots.inject('main', () =>
|
|
1650
|
+
ctx.slots.register({ name: 'main', key: PANEL_ID }, () => h(Panel))));
|
|
1651
|
+
} catch { /* contained */ }
|
|
1652
|
+
};
|
|
1653
|
+
const unmountLeft = () => { disposeAllNow(); leftMounted = false; };
|
|
1554
1654
|
const mountFor = (entry) => {
|
|
1555
|
-
disposeAllNow();
|
|
1556
1655
|
const wantLeft = entry !== 'right';
|
|
1557
1656
|
const wantRight = entry !== 'left';
|
|
1558
|
-
if (wantLeft)
|
|
1559
|
-
|
|
1560
|
-
currentDisposers.push(ctx.slots.inject('sidebar.panellist', () =>
|
|
1561
|
-
ctx.slots.register(
|
|
1562
|
-
{ name: 'sidebar.panellist', id: PANEL_ID, order: 55, label: () => '智能管家' },
|
|
1563
|
-
(p) => h(PanelIcon, { size: p && p.size }),
|
|
1564
|
-
)));
|
|
1565
|
-
} catch { /* contained */ }
|
|
1566
|
-
try {
|
|
1567
|
-
currentDisposers.push(ctx.slots.inject('main', () =>
|
|
1568
|
-
ctx.slots.register({ name: 'main', key: PANEL_ID }, () => h(Panel))));
|
|
1569
|
-
} catch { /* contained */ }
|
|
1570
|
-
}
|
|
1571
|
-
if (wantRight) {
|
|
1572
|
-
try { currentDisposers.push(mountRight()); } catch { /* contained */ }
|
|
1573
|
-
}
|
|
1657
|
+
if (wantLeft) mountLeft(); else unmountLeft();
|
|
1658
|
+
if (wantRight) mountRight(); else unmountRight();
|
|
1574
1659
|
};
|
|
1575
1660
|
|
|
1576
1661
|
try {
|
|
@@ -1581,7 +1666,8 @@ window.__ModuleLoader__.load({
|
|
|
1581
1666
|
};
|
|
1582
1667
|
ctx.effect(() => () => {
|
|
1583
1668
|
ENTRY_APPLIER = null;
|
|
1584
|
-
|
|
1669
|
+
unmountLeft();
|
|
1670
|
+
unmountRight();
|
|
1585
1671
|
}, 'dsh-steward: entries');
|
|
1586
1672
|
} catch { /* contained */ }
|
|
1587
1673
|
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Plans are zero-write; executes are audited; passwords never persist.
|
|
15
15
|
* Contract for routes/tools mirrors the running dsh-disk-cleaner@1.6.2 host entry.
|
|
16
16
|
*/
|
|
17
|
-
import { collectReport } from './report.js';
|
|
17
|
+
import { collectReport, dutyBriefing } from './report.js';
|
|
18
18
|
import { createActionHub } from './actions.js';
|
|
19
19
|
import { parseIntent } from './intent.js';
|
|
20
20
|
import { runModel, normalizeBrainOutput } from './brain.js';
|
|
@@ -212,6 +212,7 @@ function renderReportText(report) {
|
|
|
212
212
|
lines.push(report.summary.headline);
|
|
213
213
|
lines.push('');
|
|
214
214
|
lines.push(`生成时间: ${report.generatedAt}(UTC) | 状态: ${report.status} | 计数: critical=${report.summary.counts.critical} warning=${report.summary.counts.warning} info=${report.summary.counts.info}`);
|
|
215
|
+
if (report.trendVerdict && report.trendVerdict.text) lines.push(`趋势: ${report.trendVerdict.text}`);
|
|
215
216
|
lines.push('');
|
|
216
217
|
for (const f of report.findings) {
|
|
217
218
|
lines.push(`## [${f.severity.toUpperCase()}] ${f.title}`);
|
|
@@ -270,6 +271,8 @@ export function apply(ctx) {
|
|
|
270
271
|
configBaseline: path.join(dataDir(), 'config-baseline.json'),
|
|
271
272
|
settings: path.join(dataDir(), 'settings.json'),
|
|
272
273
|
modelProbes: path.join(dataDir(), 'model-probes.json'),
|
|
274
|
+
feedback: path.join(dataDir(), 'feedback.json'),
|
|
275
|
+
duty: path.join(dataDir(), 'duty.jsonl'),
|
|
273
276
|
};
|
|
274
277
|
// v0.8.0 设置:内存缓存 + 落盘。读不到/坏了 → 全默认(旧行为),绝不拦住体检。
|
|
275
278
|
let settingsCache = null;
|
|
@@ -363,6 +366,63 @@ export function apply(ctx) {
|
|
|
363
366
|
}
|
|
364
367
|
} catch { /* nothing persisted — start clean */ }
|
|
365
368
|
}
|
|
369
|
+
// ---------- v0.9.16 L0 当班环(observe-only,绝不碰授权面)----------
|
|
370
|
+
// 管家按 ui.autoScanMinutes 周期自己醒来体检,把「这一班看了什么 / 判断 /
|
|
371
|
+
// 做了没做 / 为什么没做」写进 duty.jsonl;预备建议由 dutyBriefing() 在服务端
|
|
372
|
+
// 算成人话,客户端只渲染。L0 永远不调用 plan/execute —— 观察档不代替你按确认。
|
|
373
|
+
const DUTY_LOG_CAP = 60;
|
|
374
|
+
let dutyRunning = false;
|
|
375
|
+
let dutyLastAt = 0;
|
|
376
|
+
let dutyTimer = null;
|
|
377
|
+
|
|
378
|
+
function dutyIntervalMs() {
|
|
379
|
+
const minutes = Number(settingValue(currentSettings(), 'ui.autoScanMinutes'));
|
|
380
|
+
const safe = Number.isFinite(minutes) && minutes >= 1 ? minutes : 10;
|
|
381
|
+
return Math.max(1, safe) * 60 * 1000;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function readDutyLog() {
|
|
385
|
+
try {
|
|
386
|
+
const raw = await fsp.readFile(files.duty, 'utf8');
|
|
387
|
+
const lines = raw.split('\n').filter(Boolean).slice(-DUTY_LOG_CAP);
|
|
388
|
+
const out = [];
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
try { out.push(JSON.parse(line)); } catch { /* 跳过坏行,不让一条脏数据毁掉台账 */ }
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
} catch { return []; }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function dutySnapshot() {
|
|
397
|
+
const log = await readDutyLog();
|
|
398
|
+
const latest = log.length ? log[log.length - 1] : null;
|
|
399
|
+
return {
|
|
400
|
+
mode: 'L0',
|
|
401
|
+
observeOnly: true,
|
|
402
|
+
intervalMinutes: Math.round(dutyIntervalMs() / 60000),
|
|
403
|
+
running: dutyRunning,
|
|
404
|
+
lastAt: latest ? latest.at : null,
|
|
405
|
+
latest,
|
|
406
|
+
log,
|
|
407
|
+
suggestions: latest && Array.isArray(latest.suggestions) ? latest.suggestions : [],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function runDuty(trigger) {
|
|
412
|
+
if (dutyRunning) return null; // 重入保护:上一班没结束就不开新班
|
|
413
|
+
if (Date.now() - dutyLastAt < 1000) return null; // 去抖:1 秒内不重复当班
|
|
414
|
+
dutyRunning = true;
|
|
415
|
+
try {
|
|
416
|
+
const res = await run('report', {}); // 复用既有只读体检(采集 + 落盘)
|
|
417
|
+
const report = res && res.report ? res.report : (lastReport || {});
|
|
418
|
+
const briefing = dutyBriefing(report, { trigger: String(trigger || 'scheduled') });
|
|
419
|
+
await io.appendJsonl(files.duty, briefing);
|
|
420
|
+
dutyLastAt = Date.now();
|
|
421
|
+
return briefing;
|
|
422
|
+
} catch { return null; } // 当班失败绝不冒泡影响宿主
|
|
423
|
+
finally { dutyRunning = false; }
|
|
424
|
+
}
|
|
425
|
+
|
|
366
426
|
const hub = createActionHub({
|
|
367
427
|
routeFetch: makeRouteFetch(ctx),
|
|
368
428
|
getPluginManager: () => getService(ctx, 'pluginManager'),
|
|
@@ -375,6 +435,7 @@ export function apply(ctx) {
|
|
|
375
435
|
trendFile: files.trend,
|
|
376
436
|
knowledgeFile: files.knowledge,
|
|
377
437
|
ackedFile: files.acked,
|
|
438
|
+
feedbackFile: files.feedback,
|
|
378
439
|
});
|
|
379
440
|
|
|
380
441
|
// ---------- v0.9.1 模型健康 ----------
|
|
@@ -493,7 +554,7 @@ export function apply(ctx) {
|
|
|
493
554
|
}
|
|
494
555
|
|
|
495
556
|
async function run(action, payload) {
|
|
496
|
-
if (action === 'ping') return { ok: true, plugin: name, version: pkgVersion(), readOnly: false, hub: true, wiring: { ...wiring } };
|
|
557
|
+
if (action === 'ping') return { ok: true, plugin: name, version: pkgVersion(), readOnly: false, hub: true, wiring: { ...wiring }, duty: await dutySnapshot() };
|
|
497
558
|
// v0.8.0 settings: 只读设置页需要的一切(值 + 范围 + 为什么),以及一次写入。
|
|
498
559
|
if (action === 'settings') {
|
|
499
560
|
await loadSettings();
|
|
@@ -508,6 +569,8 @@ export function apply(ctx) {
|
|
|
508
569
|
if (action === 'report') {
|
|
509
570
|
let acked = [];
|
|
510
571
|
try { acked = (await io.readJson(files.acked)) || []; } catch { acked = []; }
|
|
572
|
+
let feedback = [];
|
|
573
|
+
try { feedback = (await io.readJson(files.feedback)) || []; } catch { feedback = []; }
|
|
511
574
|
const knowledge = await loadKnowledge();
|
|
512
575
|
// ④ v0.7.6 config drift: compare against the baseline recorded on the
|
|
513
576
|
// previous scan, then renew it. The baseline is ours (data dir), never the
|
|
@@ -528,13 +591,13 @@ export function apply(ctx) {
|
|
|
528
591
|
if (saved && saved.report && typeof saved.report === 'object') previous = saved.report;
|
|
529
592
|
} catch { previous = null; }
|
|
530
593
|
const modelHealth = await modelHealthForReport();
|
|
531
|
-
lastReport = collectReport({ pluginChanges: changes.snapshot(), acked: Array.isArray(acked) ? acked : [], knowledge, configBaseline, previous, settings: currentSettings(), modelHealth });
|
|
594
|
+
lastReport = collectReport({ pluginChanges: changes.snapshot(), acked: Array.isArray(acked) ? acked : [], feedback: Array.isArray(feedback) ? feedback : [], knowledge, configBaseline, previous, settings: currentSettings(), modelHealth });
|
|
532
595
|
// Persist best-effort; a failed persist never fails the report itself.
|
|
533
596
|
io.writeJson(files.lastReport, { report: lastReport }).catch(() => {});
|
|
534
597
|
if (Array.isArray(lastReport.config) && lastReport.config.length > 0) {
|
|
535
598
|
io.writeJson(files.configBaseline, { at: lastReport.generatedAt, entries: lastReport.config }).catch(() => {});
|
|
536
599
|
}
|
|
537
|
-
return { report: lastReport, text: renderReportText(lastReport) };
|
|
600
|
+
return { report: lastReport, text: renderReportText(lastReport), autoScanMinutes: settingValue(currentSettings(), 'ui.autoScanMinutes'), wiring: { ...wiring }, duty: await dutySnapshot() };
|
|
538
601
|
}
|
|
539
602
|
if (action === 'plan') return await hub.plan(payload.kind, payload);
|
|
540
603
|
if (action === 'execute') {
|
|
@@ -656,7 +719,7 @@ export function apply(ctx) {
|
|
|
656
719
|
} catch { auditLines = []; }
|
|
657
720
|
let trend = [];
|
|
658
721
|
try { const t = await io.readJson(files.trend); if (Array.isArray(t)) trend = t.slice(-20); } catch { trend = []; }
|
|
659
|
-
return { ok: true, audit: auditLines, trend };
|
|
722
|
+
return { ok: true, audit: auditLines, trend, trendVerdict: lastReport && lastReport.trendVerdict ? lastReport.trendVerdict : null, duty: await dutySnapshot() };
|
|
660
723
|
}
|
|
661
724
|
return { ok: false, error: `unknown action: ${String(action)}` };
|
|
662
725
|
}
|
|
@@ -676,11 +739,46 @@ export function apply(ctx) {
|
|
|
676
739
|
// without waiting for a human to ask. Read-only, fail-soft, never throws.
|
|
677
740
|
try {
|
|
678
741
|
const bootTimer = setTimeout(() => {
|
|
679
|
-
|
|
742
|
+
runDuty('boot').catch(() => {});
|
|
680
743
|
}, 45_000);
|
|
681
744
|
if (typeof bootTimer.unref === 'function') bootTimer.unref();
|
|
682
745
|
} catch { /* timer unavailable */ }
|
|
683
746
|
|
|
747
|
+
// 1b-2) L0 当班:按设置周期自己醒来体检一遍(只读,绝不自动执行动作)。
|
|
748
|
+
// 周期跟随 ui.autoScanMinutes,改设置后下一班自动按新周期重排,不用重启。
|
|
749
|
+
let dutySpanMs = dutyIntervalMs();
|
|
750
|
+
const dutyTick = () => {
|
|
751
|
+
const next = dutyIntervalMs();
|
|
752
|
+
if (next !== dutySpanMs) {
|
|
753
|
+
dutySpanMs = next;
|
|
754
|
+
if (dutyTimer) clearInterval(dutyTimer);
|
|
755
|
+
dutyTimer = setInterval(dutyTick, dutySpanMs);
|
|
756
|
+
if (dutyTimer && typeof dutyTimer.unref === 'function') dutyTimer.unref();
|
|
757
|
+
}
|
|
758
|
+
runDuty('scheduled').catch(() => {});
|
|
759
|
+
};
|
|
760
|
+
// 必须挂在 ctx.effect 上:fiber 被 HMR 换掉 / 插件停用时,dispose 会把定时器一起清掉。
|
|
761
|
+
// 裸 setInterval 在旧 fiber 里不会停 —— 每热重载一次就多一个永不停止的当班环
|
|
762
|
+
// (与 v0.9.14 那个「旧路由被 dispose、新的没接上」是同一类生命周期错误)。
|
|
763
|
+
try {
|
|
764
|
+
ctx.effect(() => {
|
|
765
|
+
try {
|
|
766
|
+
dutyTimer = setInterval(dutyTick, dutySpanMs);
|
|
767
|
+
if (typeof dutyTimer.unref === 'function') dutyTimer.unref();
|
|
768
|
+
} catch { /* timer unavailable — 周期当班静默降级,不影响其它功能 */ }
|
|
769
|
+
return () => {
|
|
770
|
+
if (dutyTimer) { clearInterval(dutyTimer); dutyTimer = null; }
|
|
771
|
+
};
|
|
772
|
+
}, 'dsh-steward: L0 duty');
|
|
773
|
+
} catch {
|
|
774
|
+
// ctx.effect 不可用(极老宿主):退化为裸定时器,宁可多跑一班也不能让 boot 挂在这里。
|
|
775
|
+
try {
|
|
776
|
+
dutyTimer = setInterval(dutyTick, dutySpanMs);
|
|
777
|
+
if (typeof dutyTimer.unref === 'function') dutyTimer.unref();
|
|
778
|
+
} catch { /* ignore */ }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
684
782
|
// 1c) v0.4 proactive exit, part 2: systemPrompt.section — PROVEN shape from
|
|
685
783
|
// @changfenhuang/dsh-genui/lib/index.js:7772 ({name, order, text}, order via
|
|
686
784
|
// getSectionOrder). Guarded: if the service or getSectionOrder is missing we
|