@fffangchenghao/dsh-autoupdater 0.6.3 → 0.6.4
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 +5 -1
- package/main.js +41 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,11 +101,15 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
|
|
|
101
101
|
|
|
102
102
|
开发机是 macOS;Windows 分支是按 Node/Electron 的跨平台语义改的,并做了逻辑测试,但**尚未在真实 Windows 机器上端到端跑过**。如果 Windows 上仍失败,请把面板日志和 `%USERPROFILE%\.dsh\autoupdater\history.jsonl` 发到 GitHub issue。
|
|
103
103
|
|
|
104
|
+
### 上下文销毁修复(0.6.4)
|
|
105
|
+
|
|
106
|
+
Cordis 在插件热重载/卸载时会销毁插件上下文。0.6.1 的启动 Promise 可能在销毁后才执行 `reschedule()`,留下一个已失效上下文的定时器;次日 04:30 它会报 `cannot get required service "pluginManager" in inactive context`,而同一时间新实例的插件更新其实已经成功。0.6.4 用 `disposed` 标志在销毁后中止定时器和在途检查,并忽略这种过期上下文错误。
|
|
107
|
+
|
|
104
108
|
## 维护说明
|
|
105
109
|
|
|
106
110
|
- 上游项目:[niubi6668889595/dsh-autoupdater](https://github.com/niubi6668889595/dsh-autoupdater)(MIT)。
|
|
107
111
|
- 本分支新增:桌面版 CLI 换版、npmmirror 优先、跳过 `link:`/git 来源、只有真正升级成功才算“已应用”、Windows 空闲判定/.cmd 调用适配;完整差异见 git log。
|
|
108
|
-
- 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.
|
|
112
|
+
- 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.4`。
|
|
109
113
|
- 与上游同步:把上游加为 remote,rebase/merge 到本分支后重新发版即可。
|
|
110
114
|
|
|
111
115
|
## 安全模型
|
package/main.js
CHANGED
|
@@ -271,6 +271,12 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
271
271
|
// 初始配置 = YAML 注入值;运行时覆盖来自 config.json(每次调度/检查前重读)
|
|
272
272
|
let config = normalizeConfig(rawConfig);
|
|
273
273
|
|
|
274
|
+
// Cordis 销毁插件上下文后,未完成的异步检查不能再访问 ctx(会抛
|
|
275
|
+
// "cannot get required service ... in inactive context"),也不能再排下一轮定时器。
|
|
276
|
+
// 0.6.1 的启动 Promise 在 dispose 后仍会 reschedule(),导致次日 04:30 留下一个
|
|
277
|
+
// 已销毁上下文的定时器;0.6.4 起用 disposed 统一拦截。
|
|
278
|
+
let disposed = false;
|
|
279
|
+
|
|
274
280
|
async function reloadHotConfig() {
|
|
275
281
|
const hot = await readJson(hotConfigPath);
|
|
276
282
|
if (hot) config = normalizeConfig({ ...rawConfig, ...hot });
|
|
@@ -393,15 +399,17 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
393
399
|
/** 等到空闲;true=达成,false=超时。双重采样防瞬时空档误判。 */
|
|
394
400
|
async function waitIdle(maxWaitSec) {
|
|
395
401
|
const deadline = nowSec() + maxWaitSec;
|
|
396
|
-
while (nowSec() < deadline) {
|
|
402
|
+
while (!disposed && nowSec() < deadline) {
|
|
403
|
+
if (disposed) return false;
|
|
397
404
|
if (isIdle()) {
|
|
398
405
|
await sleep(15000);
|
|
406
|
+
if (disposed) return false;
|
|
399
407
|
if (isIdle()) return true;
|
|
400
408
|
continue;
|
|
401
409
|
}
|
|
402
410
|
await sleep(30000);
|
|
403
411
|
}
|
|
404
|
-
return isIdle();
|
|
412
|
+
return !disposed && isIdle();
|
|
405
413
|
}
|
|
406
414
|
|
|
407
415
|
async function saveState() {
|
|
@@ -789,11 +797,13 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
789
797
|
const ATTEMPT_TTL = 24 * 3600 * 1000;
|
|
790
798
|
|
|
791
799
|
async function checkAndApplyPlugins(report, { exemptQuiet = false, successes = [] } = {}) {
|
|
800
|
+
if (disposed) return;
|
|
792
801
|
const pm = ctx.pluginManager;
|
|
793
802
|
if (!pm?.listBundles || !pm?.installBundle) return;
|
|
794
803
|
const now = Date.now();
|
|
795
804
|
for (const [k, ts] of recentAttempts) if (now - ts > ATTEMPT_TTL) recentAttempts.delete(k);
|
|
796
805
|
const bundles = await pm.listBundles();
|
|
806
|
+
if (disposed) return;
|
|
797
807
|
const items = (bundles ?? []).filter((b) => {
|
|
798
808
|
if (!b?.installed || !b?.removable) return false;
|
|
799
809
|
if (b.name === SELF_NAME || b.name === 'dsh-autoupdater') return false;
|
|
@@ -813,6 +823,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
813
823
|
let consecutiveNetFails = 0;
|
|
814
824
|
const NET_FAIL_LIMIT = 4;
|
|
815
825
|
for (const b of items) {
|
|
826
|
+
if (disposed) return;
|
|
816
827
|
if (consecutiveNetFails >= NET_FAIL_LIMIT) {
|
|
817
828
|
report.push(`⚠ 连续 ${NET_FAIL_LIMIT} 个包网络超时,中止本轮(网络恢复后下轮自动重试)`);
|
|
818
829
|
break;
|
|
@@ -821,6 +832,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
821
832
|
if (decision === 'skip') continue;
|
|
822
833
|
try {
|
|
823
834
|
const latest = await npmLatestVersion(b.name);
|
|
835
|
+
if (disposed) return;
|
|
824
836
|
consecutiveNetFails = 0;
|
|
825
837
|
if (!latest || !semverGt(latest, b.version)) continue;
|
|
826
838
|
const key = `${b.name}@${latest}`;
|
|
@@ -858,6 +870,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
858
870
|
: { application: 'failed', error: { code: 'cli-failed', diagnostic: cli.reason } };
|
|
859
871
|
}
|
|
860
872
|
}
|
|
873
|
+
if (disposed) return;
|
|
861
874
|
// 装完回读磁盘真实版本验证,不再信返回值
|
|
862
875
|
let diskVer = null;
|
|
863
876
|
if (profileDir) {
|
|
@@ -897,6 +910,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
897
910
|
report.push(` ⚠ ${b.name} 已写到 ${diskVer},但 CLI 收尾报错(退出码 ${result?.error?.code === 'cli-failed' ? '非 0' : '?'})——重启后确认`);
|
|
898
911
|
}
|
|
899
912
|
} catch (e) {
|
|
913
|
+
if (/inactive context/i.test(String(e?.message ?? e))) { disposed = true; return; }
|
|
900
914
|
log(`plugin check ${b.name} failed:`, e?.message);
|
|
901
915
|
consecutiveNetFails++;
|
|
902
916
|
}
|
|
@@ -908,10 +922,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
908
922
|
let running = false;
|
|
909
923
|
|
|
910
924
|
async function runCheck(trigger) {
|
|
911
|
-
if (running) return;
|
|
925
|
+
if (disposed || running) return;
|
|
912
926
|
running = true;
|
|
913
927
|
try {
|
|
914
928
|
await reloadHotConfig();
|
|
929
|
+
if (disposed) return;
|
|
915
930
|
log(`check start (${trigger})`);
|
|
916
931
|
state.lastRun = new Date().toISOString();
|
|
917
932
|
|
|
@@ -921,13 +936,14 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
921
936
|
// 有会话运行 → 轮询等待其结束,最多 idleTimeoutMinutes。
|
|
922
937
|
const deadline = nowSec() + config.idleTimeoutMinutes * 60;
|
|
923
938
|
got = false;
|
|
924
|
-
while (nowSec() < deadline) {
|
|
939
|
+
while (!disposed && nowSec() < deadline) {
|
|
925
940
|
if (!anyAgentRunning()) { got = true; break; }
|
|
926
941
|
await sleep(10000);
|
|
927
942
|
}
|
|
928
943
|
} else {
|
|
929
944
|
got = await waitIdle(config.idleTimeoutMinutes * 60);
|
|
930
945
|
}
|
|
946
|
+
if (disposed) return;
|
|
931
947
|
if (!got) {
|
|
932
948
|
log('idle window not reached, skip this round');
|
|
933
949
|
state.lastRun += ' (skipped: busy)';
|
|
@@ -938,6 +954,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
938
954
|
const report = [];
|
|
939
955
|
const successes = [];
|
|
940
956
|
await checkAndApplyPlugins(report, { exemptQuiet: trigger === 'manual', successes });
|
|
957
|
+
if (disposed) return;
|
|
941
958
|
try {
|
|
942
959
|
await checkHarness(report);
|
|
943
960
|
} catch (e) {
|
|
@@ -946,15 +963,18 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
946
963
|
log('harness check failed (network):', e?.message);
|
|
947
964
|
}
|
|
948
965
|
|
|
966
|
+
if (disposed) return;
|
|
949
967
|
// 桌面 App 不能从进程内部重启:把“需要重启才生效”写进本轮结果,面板里直接可见。
|
|
950
968
|
// 只统计真正成功的 upgrade(successes),失败/跳过/仅通知的版本行不算。
|
|
951
969
|
if (successes.length > 0 && isDesktopApp()) {
|
|
952
970
|
report.push(' ⏻ 插件已更新到磁盘,重启 DeepSeek Harness 后生效');
|
|
953
971
|
}
|
|
954
972
|
|
|
973
|
+
if (disposed) return;
|
|
955
974
|
state.updates = report;
|
|
956
975
|
state.lastError = null;
|
|
957
976
|
await saveState();
|
|
977
|
+
if (disposed) return;
|
|
958
978
|
await appendHistory({ kind: 'check', trigger, findings: report });
|
|
959
979
|
log('check done:', report.length ? report : 'no updates');
|
|
960
980
|
|
|
@@ -985,6 +1005,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
985
1005
|
return;
|
|
986
1006
|
}
|
|
987
1007
|
} catch (e) {
|
|
1008
|
+
if (/inactive context/i.test(String(e?.message ?? e))) disposed = true;
|
|
1009
|
+
if (disposed) {
|
|
1010
|
+
log('check aborted (plugin context disposed)');
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
988
1013
|
log('check error:', e?.message ?? e);
|
|
989
1014
|
state.lastError = String(e?.message ?? e);
|
|
990
1015
|
await appendHistory({ kind: 'error', trigger, message: String(e?.message ?? e) });
|
|
@@ -996,6 +1021,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
996
1021
|
// ---- 宽限期 tick ----
|
|
997
1022
|
let applyingHarness = false;
|
|
998
1023
|
async function tick() {
|
|
1024
|
+
if (disposed) return;
|
|
999
1025
|
try {
|
|
1000
1026
|
if (!harnessNotice || config.harnessMode !== 'auto') return;
|
|
1001
1027
|
if (applyingHarness) return;
|
|
@@ -1014,13 +1040,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1014
1040
|
let graceTimer = null;
|
|
1015
1041
|
|
|
1016
1042
|
function reschedule() {
|
|
1043
|
+
if (disposed) return;
|
|
1017
1044
|
if (scheduleTimer) { clearTimeout(scheduleTimer); scheduleTimer = null; }
|
|
1018
1045
|
const at = nextRunAt(config);
|
|
1019
1046
|
if (!(at > 0)) return;
|
|
1020
1047
|
const delay = Math.max(1000, at - Date.now());
|
|
1021
1048
|
scheduleTimer = setTimeout(() => {
|
|
1049
|
+
if (disposed) return;
|
|
1022
1050
|
runCheck(config.scheduleMode).catch(() => {});
|
|
1023
|
-
reschedule();
|
|
1051
|
+
if (!disposed) reschedule();
|
|
1024
1052
|
}, delay);
|
|
1025
1053
|
scheduleTimer.unref?.();
|
|
1026
1054
|
const when = new Date(at).toLocaleString('zh-CN', { hour12: false });
|
|
@@ -1045,6 +1073,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1045
1073
|
}
|
|
1046
1074
|
|
|
1047
1075
|
async function installedPluginNames() {
|
|
1076
|
+
if (disposed) return [];
|
|
1048
1077
|
try {
|
|
1049
1078
|
const bundles = await ctx.pluginManager.listBundles();
|
|
1050
1079
|
return (bundles ?? [])
|
|
@@ -1137,6 +1166,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1137
1166
|
kind: 'exact',
|
|
1138
1167
|
path: '/api/autoupdater/run-now',
|
|
1139
1168
|
handler: async (_req, res) => {
|
|
1169
|
+
if (disposed) { sendJson(res, 410, { ok: false, error: 'plugin disposed' }); return; }
|
|
1140
1170
|
if (running) { sendJson(res, 200, { ok: true, queued: false, reason: 'already-running' }); return; }
|
|
1141
1171
|
runCheck('manual').catch(() => {});
|
|
1142
1172
|
sendJson(res, 200, { ok: true, queued: true });
|
|
@@ -1148,15 +1178,18 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
1148
1178
|
|
|
1149
1179
|
// ---- 启动 ----
|
|
1150
1180
|
reloadHotConfig().then(() => {
|
|
1181
|
+
if (disposed) return;
|
|
1151
1182
|
reschedule();
|
|
1183
|
+
if (disposed) return;
|
|
1152
1184
|
graceTimer = setInterval(tick, 30000);
|
|
1153
1185
|
graceTimer.unref?.();
|
|
1154
|
-
}).catch((e) => log('startup reload failed:', e?.message));
|
|
1186
|
+
}).catch((e) => { if (!disposed) log('startup reload failed:', e?.message); });
|
|
1155
1187
|
|
|
1156
1188
|
// ---- 清理 ----
|
|
1157
1189
|
return () => {
|
|
1158
|
-
|
|
1159
|
-
if (
|
|
1190
|
+
disposed = true;
|
|
1191
|
+
if (scheduleTimer) { clearTimeout(scheduleTimer); scheduleTimer = null; }
|
|
1192
|
+
if (graceTimer) { clearInterval(graceTimer); graceTimer = null; }
|
|
1160
1193
|
for (const d of disposers) { try { d?.(); } catch {} }
|
|
1161
1194
|
};
|
|
1162
1195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fffangchenghao/dsh-autoupdater",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Maintained fork of dsh-autoupdater with macOS/Windows desktop fixes. Scheduled update checks for DSH itself and installed plugins, applied only in idle windows.",
|