@fffangchenghao/dsh-autoupdater 0.6.1 → 0.6.3
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 +19 -2
- package/main.js +156 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
> 用 scoped 包名是为了不和上游 `dsh-autoupdater` 抢注同名包;bundle id 仍是 `dsh-autoupdater`,配置与状态路径不变。
|
|
34
|
+
>
|
|
35
|
+
> Windows 桌面版更推荐直接在 **插件管理器 → 安装插件** 里填包名安装;装完重启 App。
|
|
34
36
|
|
|
35
37
|
安装后重启 DSH(systemd 环境:`sudo systemctl restart <你的dsh服务名>`),然后在 **插件管理器 → 自动更新** 卡片配置。
|
|
36
38
|
|
|
@@ -87,11 +89,23 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
|
|
|
87
89
|
|
|
88
90
|
因此桌面版的工作方式是:**静默把插件换版到磁盘 → 面板提示重启 → 你下次打开 App 时生效**。
|
|
89
91
|
|
|
92
|
+
### Windows 支持(0.6.2+)
|
|
93
|
+
|
|
94
|
+
0.6.2 起 Windows 桌面版与 macOS 使用同一条换版路径,并修掉了 0.6.1 里的几处 Unix-only 假设:
|
|
95
|
+
|
|
96
|
+
- **空闲判定**不再调用 `find -newermt`(Windows 的 `find.exe` 是文本搜索,不支持这些参数,会让插件永远认为“忙”),改为纯 Node 递归扫描 `%DSH_HOME%\sessions`。目录不存在 = 没有会话 = 空闲;只有根目录不可读才保守当忙。
|
|
97
|
+
- **`dsh.cmd` 调用**:PATH / sibling 找到的 `.cmd` 经 `cmd.exe` 执行并正确加引号;超时用 `taskkill /T /F` 结束整棵进程树,避免留下 pnpm 子进程。
|
|
98
|
+
- **环境变量**:没有 `HOME` 时用 `homedir()`(Windows 的 `USERPROFILE`);PATH 兼容 `Path` 键,并额外查找 `%APPDATA%\npm` / `%LOCALAPPDATA%\pnpm`。
|
|
99
|
+
- Windows 上不执行 DSH 本体更新(交给安装器/App 自带更新器),插件换版后同样只提示重启。
|
|
100
|
+
- **多 profile 选择**:优先使用桌面 App 注入的 `DSH_PROFILE_DIR` / `DSH_PROFILE`;桌面版在两者都缺失时默认 `desktop`,不会因为某个测试 profile 的 `node_modules` 更新时间更近而误选。
|
|
101
|
+
|
|
102
|
+
开发机是 macOS;Windows 分支是按 Node/Electron 的跨平台语义改的,并做了逻辑测试,但**尚未在真实 Windows 机器上端到端跑过**。如果 Windows 上仍失败,请把面板日志和 `%USERPROFILE%\.dsh\autoupdater\history.jsonl` 发到 GitHub issue。
|
|
103
|
+
|
|
90
104
|
## 维护说明
|
|
91
105
|
|
|
92
106
|
- 上游项目:[niubi6668889595/dsh-autoupdater](https://github.com/niubi6668889595/dsh-autoupdater)(MIT)。
|
|
93
|
-
- 本分支新增:桌面版 CLI 换版、npmmirror 优先、跳过 `link:`/git
|
|
94
|
-
- 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.
|
|
107
|
+
- 本分支新增:桌面版 CLI 换版、npmmirror 优先、跳过 `link:`/git 来源、只有真正升级成功才算“已应用”、Windows 空闲判定/.cmd 调用适配;完整差异见 git log。
|
|
108
|
+
- 发布渠道:npm `@fffangchenghao/dsh-autoupdater`,GitHub `fangchenghao/dsh-autoupdater`,tag `v0.6.3`。
|
|
95
109
|
- 与上游同步:把上游加为 remote,rebase/merge 到本分支后重新发版即可。
|
|
96
110
|
|
|
97
111
|
## 安全模型
|
|
@@ -106,6 +120,7 @@ dsh plugin --profile web add github:fangchenghao/dsh-autoupdater
|
|
|
106
120
|
- 插件更新只覆盖声明 `dsh.bundle` 的 npm 形态插件;GitHub 源码安装的插件版本比对不适用
|
|
107
121
|
- 本体更新依赖 npm 全局安装形态(git checkout 形态请用 [dsh-updater](https://github.com/GodCC6/dsh-updater);桌面 App 不参与本体更新,由 App 自带更新器负责)
|
|
108
122
|
- 空闲判定基于本机 `$DSH_HOME/sessions` 的文件 mtime,远程会话(SSH 场景)不计入
|
|
123
|
+
- Windows 原生环境尚未实机验证(0.6.2 已移除 Unix-only 依赖,仍建议先看面板日志确认 desktop CLI 路径)
|
|
109
124
|
|
|
110
125
|
## 致谢
|
|
111
126
|
|
|
@@ -133,4 +148,6 @@ MIT
|
|
|
133
148
|
|
|
134
149
|
Install: `dsh plugin --profile web add @fffangchenghao/dsh-autoupdater` (or `github:fangchenghao/dsh-autoupdater`), then restart DSH and configure under **Plugin Manager → dsh-autoupdater**.
|
|
135
150
|
|
|
151
|
+
**Windows (0.6.2+)**: the desktop path is shared with macOS. 0.6.2 removes the Unix `find` idle check, invokes `dsh.cmd` through `cmd.exe`, falls back to `homedir()` when `HOME` is absent, and leaves DSH core updates to the installer/app. Windows has not been tested end-to-end on a real machine yet; logs are welcome in GitHub issues.
|
|
152
|
+
|
|
136
153
|
See the Chinese sections above for full details — the configuration table and idle-detection notes apply to both languages.
|
package/main.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, appendFile } from 'node:fs/promises';
|
|
2
2
|
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import { basename, delimiter, dirname, join } from 'node:path';
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
4
|
+
import { basename, delimiter, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
|
|
6
7
|
/** 本插件的真实包名(上游名或发布用的 scoped 名),用于把自己排除在自动更新之外。 */
|
|
7
8
|
const SELF_NAME = (() => {
|
|
@@ -139,6 +140,62 @@ async function readJson(path) {
|
|
|
139
140
|
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 与 @deepseek-ai/dsh-home-paths 的 resolveDshHome 对齐:
|
|
145
|
+
* $DSH_HOME 优先(支持 ~ / ~\ 前缀,空/全空白视为未设置),否则 ~/.dsh。
|
|
146
|
+
* 这样 Windows 上没有 HOME、只设置 DSH_HOME 或 USERPROFILE 时都能落到同一目录。
|
|
147
|
+
*/
|
|
148
|
+
function resolveDshHome(env = process.env) {
|
|
149
|
+
const raw = typeof env.DSH_HOME === 'string' ? env.DSH_HOME : '';
|
|
150
|
+
if (raw.trim().length === 0) return join(homedir(), '.dsh');
|
|
151
|
+
let expanded = raw;
|
|
152
|
+
if (raw === '~') expanded = homedir();
|
|
153
|
+
else if (raw.startsWith('~/') || raw.startsWith('~\\')) expanded = join(homedir(), raw.slice(2));
|
|
154
|
+
return resolve(expanded);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 递归找出目录里最新的文件 mtime(毫秒)。
|
|
159
|
+
* 目录不存在 → 0(没有会话 = 空闲);根目录读不了 → 抛错(调用方保守当忙)。
|
|
160
|
+
* 用纯 Node 实现,避免 Windows 上没有 Unix `find` 导致空闲判定永远为忙。
|
|
161
|
+
*/
|
|
162
|
+
function newestFileMtimeMs(dir) {
|
|
163
|
+
if (!existsSync(dir)) return 0;
|
|
164
|
+
let newest = 0;
|
|
165
|
+
const stack = [dir];
|
|
166
|
+
while (stack.length > 0) {
|
|
167
|
+
const current = stack.pop();
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (current === dir) throw e;
|
|
173
|
+
continue; // 子目录权限不足不应让整个探测失败
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const full = join(current, entry.name);
|
|
177
|
+
if (entry.isDirectory()) { stack.push(full); continue; }
|
|
178
|
+
if (!entry.isFile()) continue;
|
|
179
|
+
try {
|
|
180
|
+
const m = statSync(full).mtimeMs;
|
|
181
|
+
if (m > newest) newest = m;
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return newest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Windows 的 npm 全局命令是 dsh.cmd,不能直接 spawn(必须经 cmd.exe)。
|
|
190
|
+
* 返回已加引号的命令行;其他平台或 .exe 返回 null。
|
|
191
|
+
* 平台参数只用于测试,生产调用走默认值。
|
|
192
|
+
*/
|
|
193
|
+
function windowsShellCommand(command, args, platform = process.platform) {
|
|
194
|
+
if (platform !== 'win32' || !/\.(cmd|bat)$/i.test(command)) return null;
|
|
195
|
+
const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
196
|
+
return [quote(command), ...args.map(quote)].join(' ');
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
/** 运行子进程并收集 stdout+stderr;超时或无法启动 → code -1。 */
|
|
143
200
|
function spawnCapture(command, args, { env = process.env, timeoutMs = 300000 } = {}) {
|
|
144
201
|
return new Promise((resolve) => {
|
|
@@ -150,15 +207,30 @@ function spawnCapture(command, args, { env = process.env, timeoutMs = 300000 } =
|
|
|
150
207
|
settled = true;
|
|
151
208
|
resolve({ code, output });
|
|
152
209
|
};
|
|
210
|
+
const shellCommand = windowsShellCommand(command, args);
|
|
153
211
|
try {
|
|
154
|
-
child =
|
|
212
|
+
child = shellCommand
|
|
213
|
+
? spawn(shellCommand, [], { stdio: ['ignore', 'pipe', 'pipe'], env, shell: true, windowsHide: true })
|
|
214
|
+
: spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true });
|
|
155
215
|
} catch (e) {
|
|
156
216
|
output += String(e?.message ?? e);
|
|
157
217
|
finish(-1);
|
|
158
218
|
return;
|
|
159
219
|
}
|
|
220
|
+
const killTree = () => {
|
|
221
|
+
if (!child?.pid) return;
|
|
222
|
+
if (process.platform === 'win32') {
|
|
223
|
+
try {
|
|
224
|
+
execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
225
|
+
stdio: 'ignore', windowsHide: true,
|
|
226
|
+
});
|
|
227
|
+
} catch {}
|
|
228
|
+
} else {
|
|
229
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
160
232
|
const timer = setTimeout(() => {
|
|
161
|
-
|
|
233
|
+
killTree();
|
|
162
234
|
output += `\n[dsh-autoupdater] timeout after ${timeoutMs}ms`;
|
|
163
235
|
finish(-1);
|
|
164
236
|
}, timeoutMs);
|
|
@@ -185,12 +257,13 @@ function lastMeaningfulLine(text, limit = 220) {
|
|
|
185
257
|
export const inject = ['agents', 'pluginManager', 'webServer'];
|
|
186
258
|
|
|
187
259
|
export function apply(ctx, rawConfig = {}) {
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const
|
|
260
|
+
// Windows 上通常没有 HOME,只有 USERPROFILE;homedir() 会选对。
|
|
261
|
+
const home = resolveDshHome();
|
|
262
|
+
const stateDir = join(home, 'autoupdater');
|
|
263
|
+
const statePath = join(stateDir, 'status.json');
|
|
264
|
+
const historyPath = join(stateDir, 'history.jsonl');
|
|
265
|
+
const pendingPath = join(stateDir, 'pending-update.json');
|
|
266
|
+
const hotConfigPath = join(stateDir, 'config.json');
|
|
194
267
|
|
|
195
268
|
let state = { lastRun: null, lastNotice: null, updates: [], lastError: null };
|
|
196
269
|
readJson(statePath).then((s) => { if (s) state = { ...state, ...s }; }).catch(() => {});
|
|
@@ -203,28 +276,38 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
203
276
|
if (hot) config = normalizeConfig({ ...rawConfig, ...hot });
|
|
204
277
|
}
|
|
205
278
|
|
|
279
|
+
/** 是否运行在 Electron 桌面 App 的进程里(macOS/Windows 安装包)。 */
|
|
280
|
+
const isDesktopApp = () => typeof process.versions?.electron === 'string';
|
|
281
|
+
|
|
206
282
|
// ── profile 目录探测(磁盘版本回读用)──
|
|
207
|
-
// DSH_PROFILE_DIR
|
|
208
|
-
//
|
|
283
|
+
// 优先级与 DSH 自身一致:DSH_PROFILE_DIR → DSH_PROFILE → 扫描 profiles。
|
|
284
|
+
// 桌面版在 DSH_PROFILE 缺失时默认 desktop,避免多 profile 时误选最近修改的测试 profile。
|
|
209
285
|
let profileDir = process.env.DSH_PROFILE_DIR || null;
|
|
286
|
+
if (profileDir && !existsSync(profileDir)) profileDir = null;
|
|
287
|
+
if (!profileDir) {
|
|
288
|
+
const configuredProfile = process.env.DSH_PROFILE || (isDesktopApp() ? 'desktop' : '');
|
|
289
|
+
if (configuredProfile) {
|
|
290
|
+
const candidate = join(home, 'profiles', configuredProfile);
|
|
291
|
+
if (existsSync(candidate)) profileDir = candidate;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
210
294
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
295
|
+
if (!profileDir) {
|
|
296
|
+
const base = join(home, 'profiles');
|
|
297
|
+
const candidates = readdirSync(base)
|
|
298
|
+
.map((name) => join(base, name))
|
|
299
|
+
.filter((dir) => existsSync(join(dir, 'node_modules')));
|
|
300
|
+
if (candidates.length === 1) {
|
|
301
|
+
profileDir = candidates[0];
|
|
302
|
+
} else if (candidates.length > 1) {
|
|
303
|
+
profileDir = candidates
|
|
304
|
+
.map((dir) => ({ dir, m: statSync(join(dir, 'node_modules')).mtimeMs }))
|
|
305
|
+
.sort((a, b) => b.m - a.m)[0].dir;
|
|
306
|
+
}
|
|
221
307
|
}
|
|
222
308
|
if (profileDir && config.verbose) log('profileDir detected:', profileDir);
|
|
223
309
|
} catch {}
|
|
224
310
|
|
|
225
|
-
/** 是否运行在 Electron 桌面 App 的进程里(macOS/Windows 安装包)。 */
|
|
226
|
-
const isDesktopApp = () => typeof process.versions?.electron === 'string';
|
|
227
|
-
|
|
228
311
|
/**
|
|
229
312
|
* 找一条能真正写 profile 的 `dsh plugin` 入口。
|
|
230
313
|
*
|
|
@@ -237,9 +320,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
237
320
|
*/
|
|
238
321
|
function resolvePluginCli() {
|
|
239
322
|
if (isDesktopApp() && process.resourcesPath) {
|
|
240
|
-
const
|
|
241
|
-
'
|
|
242
|
-
|
|
323
|
+
const desktopCliCandidates = [
|
|
324
|
+
join(process.resourcesPath, 'app.asar', 'dsh', 'node_modules',
|
|
325
|
+
'@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js'),
|
|
326
|
+
// 未打包/开发模式:resources/app/dsh/...
|
|
327
|
+
join(process.resourcesPath, 'app', 'dsh', 'node_modules',
|
|
328
|
+
'@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js'),
|
|
329
|
+
];
|
|
330
|
+
const desktopCli = desktopCliCandidates.find((candidate) => existsSync(candidate));
|
|
331
|
+
if (desktopCli) {
|
|
243
332
|
return {
|
|
244
333
|
command: process.execPath,
|
|
245
334
|
prefixArgs: [desktopCli],
|
|
@@ -247,15 +336,21 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
247
336
|
label: 'desktop-cli',
|
|
248
337
|
};
|
|
249
338
|
}
|
|
250
|
-
log('desktop CLI not found at',
|
|
339
|
+
log('desktop CLI not found at', desktopCliCandidates[0]);
|
|
251
340
|
}
|
|
252
341
|
// 普通 npm 全局安装:dsh 与 node 同目录(Linux 常见布局)
|
|
253
|
-
const
|
|
342
|
+
const binName = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
343
|
+
const sibling = join(dirname(process.execPath), binName);
|
|
254
344
|
if (existsSync(sibling)) return { command: sibling, prefixArgs: [], env: {}, label: 'sibling-cli' };
|
|
255
345
|
// 最后退回 PATH(macOS 上 npm 全局安装常见于 ~/.local/bin,而非 node 同目录)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
346
|
+
// Windows 环境变量大小写不敏感,但键名通常是 Path 而不是 PATH。
|
|
347
|
+
const pathEnv = process.env.Path ?? process.env.PATH ?? '';
|
|
348
|
+
const pathDirs = String(pathEnv).split(delimiter).filter(Boolean);
|
|
349
|
+
if (process.platform === 'win32') {
|
|
350
|
+
if (process.env.APPDATA) pathDirs.push(join(process.env.APPDATA, 'npm'));
|
|
351
|
+
if (process.env.LOCALAPPDATA) pathDirs.push(join(process.env.LOCALAPPDATA, 'pnpm'));
|
|
352
|
+
}
|
|
353
|
+
for (const dir of pathDirs) {
|
|
259
354
|
const candidate = join(dir, binName);
|
|
260
355
|
if (existsSync(candidate)) return { command: candidate, prefixArgs: [], env: {}, label: 'path-cli' };
|
|
261
356
|
}
|
|
@@ -277,20 +372,18 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
277
372
|
// 借鉴 dsh-self-upgrade 的判定(实测比 agent.status 可靠):
|
|
278
373
|
// 会话日志 2 分钟内无任何写入 = 无进行中的工作。agent.status 存在 turn 结束
|
|
279
374
|
// 后仍停留 'running' 的情况(本机实测),会永久阻塞空闲判定,故弃用。
|
|
280
|
-
const sessionsDir =
|
|
375
|
+
const sessionsDir = join(home, 'sessions');
|
|
281
376
|
let sessionsBusyCache = { at: 0, busy: true };
|
|
282
377
|
const anyAgentRunning = () => {
|
|
283
378
|
const now = Date.now();
|
|
284
379
|
if (now - sessionsBusyCache.at < 8000) return sessionsBusyCache.busy; // 8s 缓存
|
|
285
380
|
try {
|
|
286
|
-
//
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
sessionsBusyCache = { at: now, busy: out !== '' };
|
|
293
|
-
} catch { sessionsBusyCache = { at: now, busy: true }; } // 探测失败保守当忙
|
|
381
|
+
// 2 分钟内有文件写入 = 有进行中的工作;目录不存在 = 没有会话 = 空闲。
|
|
382
|
+
const newest = newestFileMtimeMs(sessionsDir);
|
|
383
|
+
sessionsBusyCache = { at: now, busy: newest > now - 120_000 };
|
|
384
|
+
} catch {
|
|
385
|
+
sessionsBusyCache = { at: now, busy: true }; // 根目录探测失败保守当忙
|
|
386
|
+
}
|
|
294
387
|
return sessionsBusyCache.busy;
|
|
295
388
|
};
|
|
296
389
|
|
|
@@ -407,11 +500,13 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
407
500
|
async function runPluginCliAdd(pkg, version) {
|
|
408
501
|
const cli = resolvePluginCli();
|
|
409
502
|
if (!cli) {
|
|
410
|
-
const reason =
|
|
503
|
+
const reason = `找不到可用的 dsh CLI(platform=${process.platform}, resourcesPath=${process.resourcesPath ?? 'n/a'}, execPath=${process.execPath})`;
|
|
411
504
|
log('CLI add:', reason);
|
|
412
505
|
return { ok: false, code: -1, output: '', reason };
|
|
413
506
|
}
|
|
414
|
-
const profileName = profileDir
|
|
507
|
+
const profileName = profileDir
|
|
508
|
+
? basename(profileDir)
|
|
509
|
+
: (process.env.DSH_PROFILE || (isDesktopApp() ? 'desktop' : 'web'));
|
|
415
510
|
// pnpm 自身不会在 registry 之间回退。国内链路 registry.npmjs.org 实测会连续
|
|
416
511
|
// CONNECT_TIMEOUT(一次 130 个包安装用了 3 分 14 秒),而版本查询用的 npmmirror
|
|
417
512
|
// 同一版本有同步副本、1~3 秒装完。先问 npmmirror,失败再退回 pnpm 默认源。
|
|
@@ -487,6 +582,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
487
582
|
|
|
488
583
|
/** 自动探测管理本进程的 systemd 服务名(无则返回空 = 裸进程模式) */
|
|
489
584
|
function detectSystemdUnit() {
|
|
585
|
+
if (process.platform !== 'linux') return '';
|
|
490
586
|
try {
|
|
491
587
|
const cgroup = readFileSync('/proc/self/cgroup', 'utf8');
|
|
492
588
|
const m = cgroup.match(/\/system\.slice\/([a-z0-9@._-]+\.service)/i);
|
|
@@ -524,7 +620,6 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
524
620
|
*/
|
|
525
621
|
async function applyHarnessUpdateNow() {
|
|
526
622
|
const pendingExists = existsSync(pendingPath);
|
|
527
|
-
const unit = config.systemdUnit || detectSystemdUnit();
|
|
528
623
|
|
|
529
624
|
// 桌面 App:DSH 本体由 electron-updater 负责,npm -g 不能也不该覆盖 .app 内的运行时;
|
|
530
625
|
// 插件更新也不能从进程内安全 relaunch(会连带杀掉正在跑的会话)。统一标记“待重启”。
|
|
@@ -534,6 +629,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
534
629
|
return;
|
|
535
630
|
}
|
|
536
631
|
|
|
632
|
+
// Windows 非桌面模式:没有 systemd,也不支持 apply-update.mjs 的 POSIX 重启方式。
|
|
633
|
+
if (process.platform === 'win32') {
|
|
634
|
+
log('Windows: in-process restart/apply unsupported — restart required');
|
|
635
|
+
await appendHistory({ kind: 'restart-required', reason: 'win32', pending: pendingExists });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const unit = config.systemdUnit || detectSystemdUnit();
|
|
640
|
+
|
|
537
641
|
// 路径 A:仅插件更新(无 pending 文件)→ 无需 npm install,直接重启激活新插件
|
|
538
642
|
if (!pendingExists) {
|
|
539
643
|
log('no pending harness update — plugin-only restart');
|
|
@@ -616,6 +720,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
616
720
|
if (config.verbose) log('desktop build: DSH core updates are owned by the app updater — skip harness check');
|
|
617
721
|
return;
|
|
618
722
|
}
|
|
723
|
+
if (process.platform === 'win32') {
|
|
724
|
+
// Windows 的 npm 全局布局与 POSIX 不同,且没有 systemd 重启路径;本体交给安装器/App 更新。
|
|
725
|
+
if (config.verbose) log('Windows: DSH core updates are handled by the installer/app updater — skip harness check');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
619
728
|
if (existsSync(pendingPath)) {
|
|
620
729
|
if (config.verbose) log('pending update already staged, skip harness check');
|
|
621
730
|
return;
|
|
@@ -654,7 +763,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
654
763
|
/** 扫描 profile 的 cordis.patch.yml,列出 !!js ctx.<service> 这类跨版本易失效的引用 */
|
|
655
764
|
function scanPatchRisks() {
|
|
656
765
|
try {
|
|
657
|
-
const patchPath =
|
|
766
|
+
const patchPath = join(profileDir || join(home, 'profiles', 'web'), 'cordis.patch.yml');
|
|
658
767
|
if (!existsSync(patchPath)) return [];
|
|
659
768
|
const text = readFileSync(patchPath, 'utf8');
|
|
660
769
|
const found = new Set();
|
|
@@ -752,7 +861,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
752
861
|
// 装完回读磁盘真实版本验证,不再信返回值
|
|
753
862
|
let diskVer = null;
|
|
754
863
|
if (profileDir) {
|
|
755
|
-
const diskPkg = await readJson(
|
|
864
|
+
const diskPkg = await readJson(join(profileDir, 'node_modules', b.name, 'package.json')).catch(() => null);
|
|
756
865
|
diskVer = diskPkg?.version ?? null;
|
|
757
866
|
}
|
|
758
867
|
const reallyUpdated = diskVer == null ? true : diskVer !== b.version; // 无 profileDir 时退化为信安装结果
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fffangchenghao/dsh-autoupdater",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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.",
|