@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/report.js
CHANGED
|
@@ -416,10 +416,99 @@ function versionDirection(a, b) {
|
|
|
416
416
|
return verCmp(a, b);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
|
|
419
|
+
function shortHash(text) {
|
|
420
|
+
let h = 5381;
|
|
421
|
+
for (let i = 0; i < text.length; i += 1) h = ((h * 33) ^ text.charCodeAt(i)) >>> 0;
|
|
422
|
+
return h.toString(16).padStart(8, '0');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function hostVersionFromCrashes(crashes) {
|
|
426
|
+
for (const c of crashes || []) {
|
|
427
|
+
const app = c && c.meta && c.meta.app;
|
|
428
|
+
if (typeof app !== 'string') continue;
|
|
429
|
+
const m = /\s(\d+\.\d+\.\d+[^\s]*)/.exec(app);
|
|
430
|
+
if (m) return m[1];
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function collectFreeLaneHealth(paths, io) {
|
|
436
|
+
const NL2 = String.fromCharCode(10);
|
|
437
|
+
const file = path.join(paths.dshHome, 'our-free-model', 'availability.json');
|
|
438
|
+
const raw = io.readText(file);
|
|
439
|
+
const skipped = { available: false, reason: '没有读到 our-free-model 的运行台账', checked: 0, staleCount: 0, newestAt: null, failing: [], contradictory: [], allStale: false };
|
|
440
|
+
if (!raw || raw.__error || !String(raw).trim()) return skipped;
|
|
441
|
+
let parsed;
|
|
442
|
+
try { parsed = JSON.parse(raw); } catch (e) { return { ...skipped, reason: '运行台账无法解析' }; }
|
|
443
|
+
const results = (parsed && parsed.results) || {};
|
|
444
|
+
const entries = Object.entries(results).filter(([, v]) => v && typeof v === 'object');
|
|
445
|
+
let staleCount = 0;
|
|
446
|
+
let newestAt = null;
|
|
447
|
+
const failing = [];
|
|
448
|
+
const contradictory = [];
|
|
449
|
+
for (const [name, v] of entries) {
|
|
450
|
+
if (v.stale === true) staleCount += 1;
|
|
451
|
+
const at = typeof v.at === 'string' || typeof v.at === 'number' ? String(v.at) : '';
|
|
452
|
+
if (at && (!newestAt || at > newestAt)) newestAt = at;
|
|
453
|
+
const detail = typeof v.detail === 'string' ? v.detail.trim() : '';
|
|
454
|
+
if (!detail) continue;
|
|
455
|
+
failing.push({ name, state: String(v.state || ''), detail: detail.slice(0, 160) });
|
|
456
|
+
if (String(v.state) === 'available') contradictory.push({ name, detail: detail.slice(0, 160) });
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
available: true, reason: null, checked: entries.length, staleCount, newestAt,
|
|
460
|
+
failing, contradictory, allStale: entries.length > 0 && staleCount === entries.length,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function analyzeUpgrades(outdated, packages, hostVersion) {
|
|
465
|
+
const NL = String.fromCharCode(10);
|
|
466
|
+
const rows = (outdated || []).map((u) => {
|
|
467
|
+
const meta = (packages && packages[u.name]) || {};
|
|
468
|
+
const versions = Array.isArray(meta.versions) ? meta.versions : [];
|
|
469
|
+
let skipped = 0;
|
|
470
|
+
for (const v of versions) {
|
|
471
|
+
if (versionDirection(v, u.installed) > 0 && versionDirection(v, u.latest) < 0) skipped += 1;
|
|
472
|
+
}
|
|
473
|
+
const enginesDsh = meta.engines && typeof meta.engines.dsh === 'string' ? meta.engines.dsh : null;
|
|
474
|
+
const hostFit = enginesDsh && hostVersion ? satisfiesRange(enginesDsh, hostVersion) : null;
|
|
475
|
+
let risk = 'low';
|
|
476
|
+
const reasons = [];
|
|
477
|
+
if (hostFit === false) { risk = 'high'; reasons.push('新版本声明的宿主版本门槛与当前宿主不符,升了可能直接起不来'); }
|
|
478
|
+
if (String(u.latest).indexOf('-') >= 0) { if (risk === 'low') risk = 'medium'; reasons.push('目标版本带预览/预发布后缀,稳定性未经充分验证'); }
|
|
479
|
+
if (skipped >= 5) { if (risk === 'low') risk = 'medium'; reasons.push('中间跨过 ' + skipped + ' 个已发布版本,变更面较大'); }
|
|
480
|
+
if (!reasons.length) reasons.push('未发现宿主门槛/预览版/跨版本信号,属常规小步更新');
|
|
481
|
+
return { name: u.name, installed: u.installed, latest: u.latest, skipped, enginesDsh, hostFit, risk, reasons };
|
|
482
|
+
});
|
|
483
|
+
const fingerprint = shortHash(rows.map((r) => r.name + '@' + r.installed + '->' + r.latest).sort().join('|'));
|
|
484
|
+
const high = rows.filter((r) => r.risk === 'high');
|
|
485
|
+
const medium = rows.filter((r) => r.risk === 'medium');
|
|
486
|
+
const low = rows.filter((r) => r.risk === 'low');
|
|
487
|
+
const riskLines = [
|
|
488
|
+
high.length ? '· 高风险(先别升):' + high.map((r) => r.name + '(' + r.reasons[0] + ')').join('、') : null,
|
|
489
|
+
medium.length ? '· 需留意:' + medium.map((r) => r.name + '(' + r.reasons[0] + ')').join('、') : null,
|
|
490
|
+
low.length ? '· 可照常安排:' + low.map((r) => r.name).join('、') : null,
|
|
491
|
+
].filter(Boolean);
|
|
492
|
+
const text = [
|
|
493
|
+
'优点:跟随上游维护、与依赖/宿主版本对齐、减少长期停留在旧版带来的碎片;只读版本清单,没有 changelog,不编造具体修复内容。',
|
|
494
|
+
'风险:升级是「插件变更 → boot 风险」的上游;一次装错会让整个 Web UI 起不来。',
|
|
495
|
+
'分级:' + (riskLines.length ? riskLines.join(' ') : '无'),
|
|
496
|
+
].join(NL);
|
|
497
|
+
const recommendation = [
|
|
498
|
+
'按风险从低到高逐个升、一次只升一个,升完看「启动诊断」;高风险项先确认宿主版本或等后续修复,不要在要赶活的时候动它们。',
|
|
499
|
+
'升级前确认真能备份到当前可用状态。管家只读分析、不代升级。',
|
|
500
|
+
].join(NL);
|
|
501
|
+
return {
|
|
502
|
+
outdatedCount: rows.length, rows, fingerprint, text, recommendation,
|
|
503
|
+
toastTitle: rows.length + ' 个插件有新版本:附风险与处理建议',
|
|
504
|
+
toastBody: text + NL + recommendation,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function collectUpdates(paths, io, errors, hostVersion) {
|
|
420
509
|
const file = path.join(paths.dshHome, 'plugin-update-cache.json');
|
|
421
510
|
const raw = io.readText(file);
|
|
422
|
-
const empty = { available: false, reason: '没有找到市场更新缓存', outdated: [], checked: 0, cacheAgeHours: null };
|
|
511
|
+
const empty = { available: false, reason: '没有找到市场更新缓存', outdated: [], checked: 0, cacheAgeHours: null, advice: null };
|
|
423
512
|
// Optional source, same rule as the workspace index: absence is information
|
|
424
513
|
// (and gets surfaced by the confidence layer), not a degraded report.
|
|
425
514
|
if (!raw || raw.__error || !String(raw).trim()) return empty;
|
|
@@ -479,7 +568,8 @@ export function collectUpdates(paths, io, errors) {
|
|
|
479
568
|
}
|
|
480
569
|
outdated.sort((a, b) => a.name.localeCompare(b.name));
|
|
481
570
|
aheadOfCache.sort((a, b) => a.name.localeCompare(b.name));
|
|
482
|
-
|
|
571
|
+
const advice = analyzeUpgrades(outdated, packages, hostVersion);
|
|
572
|
+
return { available: true, reason: null, outdated, aheadOfCache, checked, undetermined, cacheAgeHours, advice };
|
|
483
573
|
}
|
|
484
574
|
|
|
485
575
|
/**
|
|
@@ -575,7 +665,7 @@ function scoreFindings(findings, ctx) {
|
|
|
575
665
|
// A stale crash does NOT count: its disable action is blocked by the
|
|
576
666
|
// attribution gate, so promising a shortcut there would be dishonest.
|
|
577
667
|
const gateBlocked = f.id === 'boot-crash-latest' && f.data && f.data.stale === true;
|
|
578
|
-
if (ACTIONABLE_IDS.has(f.id) && !f.muted && !gateBlocked) {
|
|
668
|
+
if (ACTIONABLE_IDS.has(f.id) && !f.muted && !f.falsePositive && !gateBlocked) {
|
|
579
669
|
score += 15; reasons.push('有直达处置动作 +15');
|
|
580
670
|
} else if (gateBlocked) {
|
|
581
671
|
reasons.push('处置被归因门封锁,不计可直达');
|
|
@@ -606,6 +696,7 @@ function scoreFindings(findings, ctx) {
|
|
|
606
696
|
if (kinds > 0) mag += Math.min(10, kinds * 4);
|
|
607
697
|
}
|
|
608
698
|
if (mag) { score += mag; reasons.push('体量/条数 +' + mag); }
|
|
699
|
+
if (f.falsePositive) { score -= 40; reasons.push('用户标记判错 -40'); }
|
|
609
700
|
|
|
610
701
|
score = Math.max(0, Math.min(100, Math.round(score)));
|
|
611
702
|
f.score = score;
|
|
@@ -857,6 +948,93 @@ function collectClientIdentity(paths, io, errors) {
|
|
|
857
948
|
return { entries, sameReal, drifted, collisions, unreadable, withoutId, scanned };
|
|
858
949
|
}
|
|
859
950
|
|
|
951
|
+
/**
|
|
952
|
+
* v0.9.15: turn 'better / worse / flat' into one Chinese sentence on the
|
|
953
|
+
* server. The client only renders it (USER.md: GUI is for humans, the host
|
|
954
|
+
* computes the words). previous is last run's persisted report; current is
|
|
955
|
+
* this run's { level, counts }. worseningStreak rides along in the report so
|
|
956
|
+
* consecutive declines can be counted without a second data store.
|
|
957
|
+
*/
|
|
958
|
+
/**
|
|
959
|
+
* v0.9.16 · L0 当班台账的「判断 + 回报」纯函数。
|
|
960
|
+
*
|
|
961
|
+
* 这一版的管家会自己按时醒来体检(不带人、不带面板),但**只观察、不动手**:
|
|
962
|
+
* 任何写动作都不做。为了不把「没做事」写成一片空白,每班都留一条能被人
|
|
963
|
+
* 读懂的记录:看了什么、判断是什么、做了没做、为什么没做。
|
|
964
|
+
*
|
|
965
|
+
* 纯函数,不碰文件、不碰时钟(at 由调用方传),便于 smoke 直接喂夹具断言。
|
|
966
|
+
* 服务端算好人话、客户端只渲染(USER.md):这里产出的就是那句人话。
|
|
967
|
+
*/
|
|
968
|
+
export function dutyBriefing(report, options = {}) {
|
|
969
|
+
const at = typeof options.at === 'string' ? options.at : new Date().toISOString();
|
|
970
|
+
const trigger = String(options.trigger || 'scheduled');
|
|
971
|
+
const findings = report && Array.isArray(report.findings) ? report.findings : [];
|
|
972
|
+
const counts = report && report.summary && report.summary.counts ? report.summary.counts : null;
|
|
973
|
+
const level = report && report.summary ? String(report.summary.level || 'unknown') : 'unknown';
|
|
974
|
+
const trend = report && report.trendVerdict ? report.trendVerdict : null;
|
|
975
|
+
|
|
976
|
+
// 看了什么:按级别数一遍,明确区分「静音/判错」与「还在报的」。
|
|
977
|
+
const live = findings.filter((f) => !f.muted && !f.falsePositive);
|
|
978
|
+
const seen = '看了 ' + findings.length + ' 条发现'
|
|
979
|
+
+ (counts ? '(严重 ' + (counts.critical || 0) + ' · 留意 ' + (counts.warning || 0) + ' · 提示 ' + (counts.info || 0) + ')' : '')
|
|
980
|
+
+ ',其中 ' + live.length + ' 条仍在报。';
|
|
981
|
+
|
|
982
|
+
// 判断:趋势结论服务端已经算成人话,直接引用;没有就给当前级别。
|
|
983
|
+
const verdict = trend && trend.text
|
|
984
|
+
? trend.text
|
|
985
|
+
: (level === 'warning' ? '这一班有需要留意的项目,但没有严重问题。'
|
|
986
|
+
: level === 'critical' ? '这一班出现了严重问题,已留待你处置。'
|
|
987
|
+
: '这一班没有需要留意的问题。');
|
|
988
|
+
|
|
989
|
+
// 预备建议:把仍在报、且给了 recommendation 的 finding 提炼成清单(零副作用)。
|
|
990
|
+
// 只取非静音、非判错的,且最多 6 条——避免把整份报告复述一遍。
|
|
991
|
+
const suggestions = live
|
|
992
|
+
.filter((f) => typeof f.recommendation === 'string' && f.recommendation.trim())
|
|
993
|
+
.slice(0, 6)
|
|
994
|
+
.map((f) => ({ id: f.id, severity: f.severity, text: f.recommendation.trim() }));
|
|
995
|
+
|
|
996
|
+
// 做了没做 / 为什么没做:L0 的硬边界,写死且说清,别让「没做事」显得像漏了。
|
|
997
|
+
const action = '未执行任何动作:L0 是观察档,管家只体检、只把建议备好,不代替你按确认。'
|
|
998
|
+
+ (suggestions.length
|
|
999
|
+
? '已把 ' + suggestions.length + ' 条处置建议预备好,等你来动作中心确认。'
|
|
1000
|
+
: '这一班没有需要预备的处置建议。');
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
at,
|
|
1004
|
+
trigger,
|
|
1005
|
+
level,
|
|
1006
|
+
counts: counts || null,
|
|
1007
|
+
seen,
|
|
1008
|
+
verdict,
|
|
1009
|
+
action,
|
|
1010
|
+
suggestions,
|
|
1011
|
+
state: 'observe-only',
|
|
1012
|
+
executed: 0,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function summarizeTrend(previous, current) {
|
|
1017
|
+
const cur = current || {};
|
|
1018
|
+
const rank = (lv) => (lv === 'critical' ? 3 : lv === 'warning' ? 2 : 0);
|
|
1019
|
+
const bad = (c) => (Number(c && c.critical) || 0) * 3 + (Number(c && c.warning) || 0) + rank(cur.level) * 2;
|
|
1020
|
+
const curBad = bad(cur.counts);
|
|
1021
|
+
const prev = previous && previous.summary ? previous.summary : null;
|
|
1022
|
+
if (!prev) return { state: 'stable', worseningStreak: 0, text: '这是第一次体检,还没有可比的上一轮结论。' };
|
|
1023
|
+
const prevBad = (Number(prev.counts && prev.counts.critical) || 0) * 3 + (Number(prev.counts && prev.counts.warning) || 0) + rank(prev.level) * 2;
|
|
1024
|
+
const prevStreak = Number(previous.trendVerdict && previous.trendVerdict.worseningStreak) || 0;
|
|
1025
|
+
if (curBad < prevBad) {
|
|
1026
|
+
return { state: 'recovered', worseningStreak: 0, text: '比上一次体检好转了:风险项减少或严重项已消除。' };
|
|
1027
|
+
}
|
|
1028
|
+
if (curBad > prevBad) {
|
|
1029
|
+
const streak = prevStreak + 1;
|
|
1030
|
+
return { state: 'worsening', worseningStreak: streak,
|
|
1031
|
+
text: streak >= 2
|
|
1032
|
+
? '连续 ' + streak + ' 次体检都在变差,值得回头看审计趋势里这几次改了什么。'
|
|
1033
|
+
: '比上一次体检多了风险项,先看看新出现的是哪一条。' };
|
|
1034
|
+
}
|
|
1035
|
+
return { state: 'stable', worseningStreak: 0, text: '与上一次体检相比没有明显变化。' };
|
|
1036
|
+
}
|
|
1037
|
+
|
|
860
1038
|
export function collectReport(options = {}) {
|
|
861
1039
|
const now = options.now || new Date();
|
|
862
1040
|
const env = options.env || process.env;
|
|
@@ -874,6 +1052,7 @@ export function collectReport(options = {}) {
|
|
|
874
1052
|
|
|
875
1053
|
const bootedAt = options.bootedAt || new Date(Date.now() - Math.floor(process.uptime()) * 1000);
|
|
876
1054
|
const crashes = collectCrashLogs(paths, io, errors, limits);
|
|
1055
|
+
const resolvedHostVersion = options.hostVersion || hostVersionFromCrashes(crashes);
|
|
877
1056
|
const patchBackups = collectPatchBackups(paths, io, errors);
|
|
878
1057
|
const pluginChanges = Array.isArray(options.pluginChanges) ? options.pluginChanges.slice(0, 50) : [];
|
|
879
1058
|
|
|
@@ -1060,7 +1239,7 @@ export function collectReport(options = {}) {
|
|
|
1060
1239
|
// this stays an information item with a pointer, not a button.
|
|
1061
1240
|
let updateInfo = { available: false, outdated: [], checked: 0, cacheAgeHours: null };
|
|
1062
1241
|
try {
|
|
1063
|
-
updateInfo = collectUpdates(paths, io, errors);
|
|
1242
|
+
updateInfo = collectUpdates(paths, io, errors, resolvedHostVersion);
|
|
1064
1243
|
if (!updateInfo.available) {
|
|
1065
1244
|
findings.push({
|
|
1066
1245
|
id: 'plugin-updates', severity: 'info', category: '版本适配',
|
|
@@ -1075,6 +1254,7 @@ export function collectReport(options = {}) {
|
|
|
1075
1254
|
// Those are not updates — they mean the cache is older than the disk, which is
|
|
1076
1255
|
// worth saying out loud exactly once instead of silently hiding them.
|
|
1077
1256
|
const ahead = updateInfo.aheadOfCache || [];
|
|
1257
|
+
const advice = updateInfo.advice || null;
|
|
1078
1258
|
const aheadNote = ahead.length > 0
|
|
1079
1259
|
? '\n另有 ' + ahead.length + ' 个包的已装版本比缓存里的 latest 还新('
|
|
1080
1260
|
+ ahead.slice(0, 3).map((u) => u.name + ' ' + u.installed + ' > ' + u.latest).join('、')
|
|
@@ -1092,10 +1272,10 @@ export function collectReport(options = {}) {
|
|
|
1092
1272
|
detail: updateInfo.outdated.slice(0, 10).map((u) => '· ' + u.name + ':' + u.installed + ' → ' + u.latest).join('\n')
|
|
1093
1273
|
+ (updateInfo.outdated.length > 10 ? '\n(共 ' + updateInfo.outdated.length + ' 个)' : '')
|
|
1094
1274
|
+ aheadNote + unknownNote
|
|
1095
|
-
+ '\n\n升级是「插件变更 → boot 风险」的上游;升级前先确认真能备份到当前可用状态。',
|
|
1275
|
+
+ (advice ? '\n\n' + advice.text : '\n\n升级是「插件变更 → boot 风险」的上游;升级前先确认真能备份到当前可用状态。'),
|
|
1096
1276
|
evidence: '来源:$DSH_HOME/plugin-update-cache.json(' + (updateInfo.cacheAgeHours != null ? updateInfo.cacheAgeHours + ' 小时前刷新' : '刷新时间未知') + ');对比 ' + updateInfo.checked + ' 个已装包;版本方向按 semver 比,只把「更新」算作可用',
|
|
1097
|
-
recommendation: '逐个升而不是批量升,升完看「启动诊断」;不要在要赶活的时候动它们。管家不代升级。',
|
|
1098
|
-
data: { outdated: updateInfo.outdated.slice(0, 20), aheadOfCache: ahead.slice(0, 20), undetermined: updateInfo.undetermined || 0, checked: updateInfo.checked, cacheAgeHours: updateInfo.cacheAgeHours },
|
|
1277
|
+
recommendation: advice ? advice.recommendation : '逐个升而不是批量升,升完看「启动诊断」;不要在要赶活的时候动它们。管家不代升级。',
|
|
1278
|
+
data: { outdated: updateInfo.outdated.slice(0, 20), aheadOfCache: ahead.slice(0, 20), undetermined: updateInfo.undetermined || 0, checked: updateInfo.checked, cacheAgeHours: updateInfo.cacheAgeHours, advice },
|
|
1099
1279
|
});
|
|
1100
1280
|
} else if (updateInfo.checked > 0) {
|
|
1101
1281
|
findings.push({
|
|
@@ -1115,6 +1295,48 @@ export function collectReport(options = {}) {
|
|
|
1115
1295
|
errors.push({ step: 'plugin-updates', error: String((e && e.message) || e) });
|
|
1116
1296
|
}
|
|
1117
1297
|
|
|
1298
|
+
// ①b v0.9.16(未发布):只读一家第三方插件的运行台账。没装这个插件是正常状态,
|
|
1299
|
+
// 不是体检退化——所以不加 coverage 键、不 push error。只在真读到、且出现
|
|
1300
|
+
// 「标着可用却失败」或「最近一次探测失败」时给一条 info,零副作用。
|
|
1301
|
+
let freeLane = { available: false, reason: 'not checked' };
|
|
1302
|
+
try {
|
|
1303
|
+
freeLane = collectFreeLaneHealth(paths, io);
|
|
1304
|
+
if (freeLane.available && (freeLane.contradictory.length > 0 || freeLane.failing.length > 0)) {
|
|
1305
|
+
const contra = freeLane.contradictory;
|
|
1306
|
+
const NLf = String.fromCharCode(10);
|
|
1307
|
+
const staleNote = freeLane.allStale
|
|
1308
|
+
? NLf + '台账里 ' + freeLane.checked + ' 个模型的探测结果全部标着 stale,说明这份结果本身可能已过期。'
|
|
1309
|
+
: '';
|
|
1310
|
+
const contraNote = contra.length
|
|
1311
|
+
? NLf + '其中 ' + contra.length + ' 条自相矛盾:' + contra.slice(0, 3).map((x) => x.name).join('、')
|
|
1312
|
+
+ (contra.length > 3 ? ' 等' : '') + ' 标着 available,却同时记着最近一次上游失败。'
|
|
1313
|
+
: '';
|
|
1314
|
+
findings.push({
|
|
1315
|
+
id: 'free-lane-health',
|
|
1316
|
+
severity: 'info',
|
|
1317
|
+
category: '运行期健康',
|
|
1318
|
+
title: contra.length
|
|
1319
|
+
? contra.length + ' 个免费模型标着「可用」、最近一次探测却失败'
|
|
1320
|
+
: freeLane.failing.length + ' 个免费模型最近一次探测失败',
|
|
1321
|
+
detail: '这是 our-free-model 插件自己的运行台账(availability.json),不是 boot 崩溃、也不是宿主 LLM 实时探活能看到的信号。'
|
|
1322
|
+
+ NLf + '· 共检查 ' + freeLane.checked + ' 个模型,' + freeLane.failing.length + ' 个记着上游失败 detail。'
|
|
1323
|
+
+ contraNote + staleNote
|
|
1324
|
+
+ NLf + '典型 detail:' + (freeLane.failing[0] ? freeLane.failing[0].detail : '(无)'),
|
|
1325
|
+
evidence: '只读 $DSH_HOME/our-free-model/availability.json(最新探测 ' + (freeLane.newestAt || '未知') + ');'
|
|
1326
|
+
+ 'staleCount ' + freeLane.staleCount + '/' + freeLane.checked + ';未改任何插件文件',
|
|
1327
|
+
recommendation: '先别把这些模型设成默认/首选;上游失败常见原因是缺本地 Bun 或免费通道被限流。'
|
|
1328
|
+
+ '这是插件私有台账,管家只读、不碰它、不代修;中期应等宿主把插件级上游失败写进 diagnostics 再统一接一层。',
|
|
1329
|
+
data: {
|
|
1330
|
+
checked: freeLane.checked, staleCount: freeLane.staleCount, allStale: freeLane.allStale,
|
|
1331
|
+
newestAt: freeLane.newestAt, failing: freeLane.failing.slice(0, 10),
|
|
1332
|
+
contradictory: contra.slice(0, 10),
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
// best-effort only — never degrade the report for an optional signal
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1118
1340
|
// ② v0.7.7 workspace / session inventory. The report previously mentioned
|
|
1119
1341
|
// sessions only inside a prose string; this measures them.
|
|
1120
1342
|
//
|
|
@@ -1644,9 +1866,16 @@ export function collectReport(options = {}) {
|
|
|
1644
1866
|
.map((a) => (a && typeof a === 'object' ? a.id : a))
|
|
1645
1867
|
.filter((x) => typeof x === 'string'),
|
|
1646
1868
|
);
|
|
1869
|
+
const feedbackRaw = Array.isArray(options.feedback) ? options.feedback : [];
|
|
1870
|
+
const feedbackIds = new Set(
|
|
1871
|
+
feedbackRaw
|
|
1872
|
+
.filter((a) => a && typeof a === 'object' && typeof a.id === 'string')
|
|
1873
|
+
.map((a) => a.id),
|
|
1874
|
+
);
|
|
1647
1875
|
let mutedCount = 0;
|
|
1648
1876
|
for (const f of findings) {
|
|
1649
1877
|
f.muted = ackedIds.has(f.id);
|
|
1878
|
+
f.falsePositive = feedbackIds.has(f.id);
|
|
1650
1879
|
if (f.muted) mutedCount += 1;
|
|
1651
1880
|
}
|
|
1652
1881
|
|
|
@@ -1669,6 +1898,7 @@ export function collectReport(options = {}) {
|
|
|
1669
1898
|
: counts.warning > 0
|
|
1670
1899
|
? '有需要留意的风险项'
|
|
1671
1900
|
: '当前未发现异常,以下为常备建议';
|
|
1901
|
+
const trendVerdict = summarizeTrend(options.previous, { level, counts });
|
|
1672
1902
|
|
|
1673
1903
|
return {
|
|
1674
1904
|
generatedAt: now.toISOString ? now.toISOString() : String(now),
|
|
@@ -1678,16 +1908,19 @@ export function collectReport(options = {}) {
|
|
|
1678
1908
|
config: configNow,
|
|
1679
1909
|
workspace: workspaceInfo,
|
|
1680
1910
|
updates: updateInfo,
|
|
1911
|
+
freeLane,
|
|
1681
1912
|
coverage,
|
|
1682
1913
|
status: errors.length > 0 ? 'degraded' : 'ok',
|
|
1683
|
-
summary: { level, headline, counts },
|
|
1684
1914
|
findings,
|
|
1915
|
+
summary: { level, headline, counts },
|
|
1916
|
+
trendVerdict,
|
|
1685
1917
|
limitations: [
|
|
1686
1918
|
'只读:不删文件、不代跑提权命令、不自动拉起/重启、不改启动流程。',
|
|
1687
1919
|
'不重复实现磁盘扫描 / 备份 / 归档 / 记忆,只指向现成能力。',
|
|
1688
1920
|
'不 import 任何其它 DSH 包内部模块,避免版本漂移。',
|
|
1689
1921
|
'崩溃检测是「启动后读日志」,不是实时守护;本进程内事件随宿主 fatal 消失。',
|
|
1690
1922
|
'采集限流:日志 ≤ ' + limits.maxLogs + ' 份、单份 ≤ ' + limits.maxBytesPerLog + ' B(可在面板「设置」里调)。',
|
|
1923
|
+
'运行期插件健康只接了一家:只读 $DSH_HOME/our-free-model/availability.json,没装该插件属正常、不算体检退化;DSH 宿主自身的新版本没有可读信号源,本版只分析市场缓存里的插件新版本。',
|
|
1691
1924
|
],
|
|
1692
1925
|
errors,
|
|
1693
1926
|
};
|
package/lib/settings.js
CHANGED
|
@@ -91,9 +91,9 @@ const SPEC = [
|
|
|
91
91
|
|
|
92
92
|
// ── 界面 ──────────────────────────────────────────────────────
|
|
93
93
|
{
|
|
94
|
-
key: 'ui.autoScanMinutes', group: '界面', label: '
|
|
94
|
+
key: 'ui.autoScanMinutes', group: '界面', label: '自动体检 / 后台当班周期',
|
|
95
95
|
unit: '分钟', min: 1, max: 240, def: 10,
|
|
96
|
-
why: '
|
|
96
|
+
why: '一个数字管两处:切回面板时缓存超过这个时间就后台重体检一次;管家不开面板时也按这个周期自己醒来体检一遍(v0.9.16 L0 当班)。设为 1 分钟 = 几乎每次都刷;设大 = 更省,但看到的可能是几分钟前的样子。改完下一班就按新周期走,不用重启。',
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
key: 'ui.entry', group: '界面', label: '面板入口', type: 'choice', def: 'both',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sidady/dsh-steward",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Guardian for DeepSeek Harness: a read-only health check (boot crashes, backup freshness, config drift, workspace growth, plugin updates, host-version compat, model availability) plus an explicitly authorized action layer (plan → single-use fingerprint → confirm → audit) covering cache cleaning, encrypted volatile-data backups, plugin enable/disable, knowledge capture, finding mute, and switching the default model.",
|
|
6
6
|
"keywords": [
|