dsh-selfupdater 0.4.24 → 0.4.26
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/lib/fnos-patches.mjs +143 -0
- package/lib/updater.mjs +56 -4
- package/package.json +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// 飞牛(fnOS)运行时补丁 —— 与仓库 scripts/patch.py 保持同步!
|
|
2
|
+
// patch.py 在 FPK 构建期打补丁;本模块在插件一键升级(DSH 自更新)路径上、
|
|
3
|
+
// 换装前对 staging 里的 node_modules 应用同一组补丁。插件升级路径绕过了
|
|
4
|
+
// 构建期,不打补丁的裸 npm 包在局域网访问时会退化(settings are
|
|
5
|
+
// unavailable、403 等)。上游代码变形导致匹配失败时,两处一起更新。
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { basename, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
// 补丁后的标志性替换文本(与 patch.py 的 CRITICAL_MARKERS 对应)
|
|
10
|
+
const FNOS_ISLOOPBACK = 'isLoopback: true, // fnOS fix (Issue #2): trust proxy/control panel access as loopback';
|
|
11
|
+
|
|
12
|
+
/** 遍历目录下所有 .js/.mjs 文件路径 */
|
|
13
|
+
function* walkJs(dir) {
|
|
14
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const p = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) yield* walkJs(p);
|
|
17
|
+
else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) yield p;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 对单个 JS 文本应用补丁规则,返回(可能修改后的)文本。
|
|
23
|
+
* 所有规则均幂等:已打补丁的文本重复进入不会叠加。
|
|
24
|
+
*/
|
|
25
|
+
function patchCode(code, filePath) {
|
|
26
|
+
let changed = false;
|
|
27
|
+
|
|
28
|
+
// 1. CSRF/Origin 拦截放行:经本应用反代/控制页访问即视为可信请求。
|
|
29
|
+
// 注意加"未打过"守卫,否则重复执行会在 return true 后叠加第二个 return true。
|
|
30
|
+
if (code.includes('function isTrustedApiRequest(')
|
|
31
|
+
&& !code.includes('function isTrustedApiRequest(request, trustedHosts) { return true;')) {
|
|
32
|
+
code = code.replace(
|
|
33
|
+
'function isTrustedApiRequest(request, trustedHosts) {',
|
|
34
|
+
'function isTrustedApiRequest(request, trustedHosts) { return true;',
|
|
35
|
+
);
|
|
36
|
+
changed = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 1b. 浏览器端回环判定放行(Issue #2):局域网/iframe 访问时
|
|
40
|
+
// location.hostname 非回环,settings 等 RPC 被判不可用。
|
|
41
|
+
if (code.includes('function isLoopbackHostname(')
|
|
42
|
+
&& !code.includes('function isLoopbackHostname(hostname) { return true;')) {
|
|
43
|
+
code = code.replace(
|
|
44
|
+
'function isLoopbackHostname(hostname) {',
|
|
45
|
+
'function isLoopbackHostname(hostname) { return true;',
|
|
46
|
+
);
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1c. isLoopback 调用点恒真(双保险)。alpha.4 起调用点在 pageLocation 前
|
|
51
|
+
// 多了 `transport?.ownsHost === true ||`:先精确匹配旧串,未命中再用正则
|
|
52
|
+
// 兜底兼容新旧两种形状。
|
|
53
|
+
if (filePath.includes('dsh-client-connection') && basename(filePath) === 'client.js') {
|
|
54
|
+
const before = code;
|
|
55
|
+
let next = code.replace(
|
|
56
|
+
'isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname),',
|
|
57
|
+
FNOS_ISLOOPBACK,
|
|
58
|
+
);
|
|
59
|
+
if (next === before) {
|
|
60
|
+
next = code.replace(
|
|
61
|
+
/isLoopback:\s*(?:transport\?\.ownsHost\s*===\s*true\s*\|\|\s*)?pageLocation\s*===\s*void\s*0\s*\|\|\s*isLoopbackHostname\(pageLocation\.hostname\),/,
|
|
62
|
+
FNOS_ISLOOPBACK,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
changed = changed || next !== before;
|
|
66
|
+
code = next;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. 目录选择器:把 home 目录指向飞牛共享盘(DeepSeekHarness 所在卷)。
|
|
70
|
+
// 先还原上游形态再重新注入,保证幂等。
|
|
71
|
+
if (filePath.includes('dsh-host-directory-picker-browse') && basename(filePath) === 'index.js') {
|
|
72
|
+
const before = code;
|
|
73
|
+
if (code.includes('import fs from "node:fs";\n')) {
|
|
74
|
+
code = code.replace('import fs from "node:fs";\n', '');
|
|
75
|
+
code = 'import fs from "node:fs";\n' + code;
|
|
76
|
+
} else if (code.includes('import fs from node:fs;\n')) {
|
|
77
|
+
code = code.replace('import fs from node:fs;\n', 'import fs from "node:fs";\n');
|
|
78
|
+
}
|
|
79
|
+
const fnosBlock = 'function fnosTargetHome() {\n'
|
|
80
|
+
+ '\ttry {\n'
|
|
81
|
+
+ '\t\tif (fs.existsSync("/vol1/@appshare/DeepSeekHarness")) return "/vol1/@appshare/DeepSeekHarness";\n'
|
|
82
|
+
+ '\t\tif (fs.existsSync("/vol1")) return "/vol1";\n'
|
|
83
|
+
+ '\t} catch (e) {}\n'
|
|
84
|
+
+ '\treturn homedir();\n'
|
|
85
|
+
+ '}\n'
|
|
86
|
+
+ '\t\tconst home = fnosTargetHome();\n'
|
|
87
|
+
+ '\t\t';
|
|
88
|
+
const target = 'const home = homedir();';
|
|
89
|
+
if (code.includes('function fnosTargetHome()')) {
|
|
90
|
+
code = code.replace(/function fnosTargetHome\(\) \{[\s\S]*?\n\}\n/, '');
|
|
91
|
+
}
|
|
92
|
+
if (code.includes('const home = fnosTargetHome();')) {
|
|
93
|
+
code = code.replace('const home = fnosTargetHome();', target);
|
|
94
|
+
}
|
|
95
|
+
if (code.includes(target)) {
|
|
96
|
+
code = code.replace(target, fnosBlock); // 首处替换为注入块
|
|
97
|
+
code = code.replaceAll(target, ''); // 其余残留清除
|
|
98
|
+
}
|
|
99
|
+
changed = changed || code !== before;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return changed ? code : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 对一个 app_root(含 node_modules)应用全部飞牛补丁,并按关键标记复核。
|
|
107
|
+
* 任何关键补丁未命中都抛错——调用方(updater.mjs)在换装前中止升级,
|
|
108
|
+
* 旧版保持原样,天然安全。
|
|
109
|
+
* @param {string} appRoot - 含 node_modules 的应用根目录(staging 与其同构)
|
|
110
|
+
*/
|
|
111
|
+
export function applyFnosPatches(appRoot) {
|
|
112
|
+
const at = join(appRoot, 'node_modules', '@deepseek-ai');
|
|
113
|
+
if (!existsSync(at)) throw new Error('找不到 node_modules/@deepseek-ai,staging 结构异常');
|
|
114
|
+
|
|
115
|
+
let patched = 0;
|
|
116
|
+
for (const p of walkJs(at)) {
|
|
117
|
+
const code = readFileSync(p, 'utf8');
|
|
118
|
+
const next = patchCode(code, p);
|
|
119
|
+
if (next !== null) {
|
|
120
|
+
writeFileSync(p, next, 'utf8');
|
|
121
|
+
patched++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 关键标记复核:以"标志存在"计数(幂等重跑不会误报),缺失即失败
|
|
126
|
+
const markers = {
|
|
127
|
+
'CSRF 信任放行': 'isTrustedApiRequest(request, trustedHosts) { return true;',
|
|
128
|
+
'loopback 信任修复 (Issue #2)': 'isLoopbackHostname(hostname) { return true;',
|
|
129
|
+
'浏览器端 isLoopback 直连': 'isLoopback: true, // fnOS fix',
|
|
130
|
+
};
|
|
131
|
+
const missed = [];
|
|
132
|
+
for (const [label, marker] of Object.entries(markers)) {
|
|
133
|
+
let hits = 0;
|
|
134
|
+
for (const p of walkJs(at)) {
|
|
135
|
+
if (readFileSync(p, 'utf8').includes(marker)) hits++;
|
|
136
|
+
}
|
|
137
|
+
if (hits === 0) missed.push(label);
|
|
138
|
+
}
|
|
139
|
+
if (missed.length > 0) {
|
|
140
|
+
throw new Error(`关键补丁未命中: ${missed.join('、')}(上游代码可能已变更,需同步更新 fnos-patches.mjs 与 patch.py)`);
|
|
141
|
+
}
|
|
142
|
+
console.log(`[fnos-patches] 补丁应用完成: ${patched} 个文件, 关键标记全部命中`);
|
|
143
|
+
}
|
package/lib/updater.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
14
|
import net from 'node:net';
|
|
15
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync
|
|
16
|
+
, rmSync, writeFileSync } from 'node:fs';
|
|
16
17
|
import { dirname, join, resolve } from 'node:path';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { applyFnosPatches } from './fnos-patches.mjs';
|
|
18
20
|
|
|
19
21
|
/** 插件自身安装位置(…/node_modules/dsh-selfupdater),用于定位状态文件所在目录。 */
|
|
20
22
|
const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -316,10 +318,19 @@ async function downloadIntoStaging(target, registryBase) {
|
|
|
316
318
|
// 版本,撞上"staged 必须严格等于 target"的校验而误报失败。
|
|
317
319
|
writeFileSync(
|
|
318
320
|
join(stagingDir, 'package.json'),
|
|
319
|
-
|
|
321
|
+
// pnpm.onlyBuiltDependencies 白名单放行原生包的 install 脚本:pnpm 10 默认
|
|
322
|
+
// 拦截构建脚本,koffi(cnoke 解压 vendor 预编译)和 fs-ext(node-gyp 编译)
|
|
323
|
+
// 不跑脚本就没有 native 产物,dsh 0.1.3-alpha.2 起依赖它们,boot 直接崩。
|
|
324
|
+
JSON.stringify({
|
|
325
|
+
name: 'dsh-selfupdate-staging',
|
|
326
|
+
private: true,
|
|
327
|
+
dependencies: { [PKG]: target },
|
|
328
|
+
pnpm: { onlyBuiltDependencies: ['koffi', 'fs-ext'] },
|
|
329
|
+
}, null, 2),
|
|
320
330
|
);
|
|
321
|
-
// 实体文件布局 +
|
|
322
|
-
|
|
331
|
+
// 实体文件布局 + 关闭交互确认 + 关闭 side-effects 缓存(防 store 里旧版本的
|
|
332
|
+
// 构建产物跨版本污染),保证无人值守可执行。
|
|
333
|
+
writeFileSync(join(stagingDir, '.npmrc'), 'node-linker=hoisted\nside-effects-cache=false\n');
|
|
323
334
|
const pnpm = pnpmCommand();
|
|
324
335
|
setState('downloading', `正在下载 ${PKG}@${target} …`);
|
|
325
336
|
// pnpm 不认 npm 风格的 --omit/--no-audit/--no-fund(0.4.18 实测踩坑):
|
|
@@ -340,6 +351,39 @@ async function downloadIntoStaging(target, registryBase) {
|
|
|
340
351
|
// 校验装到的确实是目标版本,防止镜像滞后悄悄装了旧版。
|
|
341
352
|
const staged = JSON.parse(readFileSync(join(stagingDir, 'node_modules', PKG, 'package.json'), 'utf8')).version;
|
|
342
353
|
if (staged !== target) throw new Error(`staging 版本不符:期望 ${target},实际 ${staged}`);
|
|
354
|
+
// 原生模块自检:0.1.3-alpha.2 起 dsh 依赖 koffi/fs-ext,缺 native 产物会让
|
|
355
|
+
// 宿主 boot 崩溃(0.4.25 实测踩坑),换装前在此拦截。
|
|
356
|
+
verifyNativeModules();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 校验 staging 内原生模块的构建产物是否齐全。
|
|
361
|
+
* - koffi:cnoke --prebuild 从 vendor 解压,产物在 build/koffi/ 下的 .node;
|
|
362
|
+
* - fs-ext:node-gyp 编译,产物在 build/Release/fs-ext.node(NAS 需编译工具链)。
|
|
363
|
+
* 任一缺失即抛错,终止本次升级(尚未换装,旧版原样运行,天然安全)。
|
|
364
|
+
*/
|
|
365
|
+
function verifyNativeModules() {
|
|
366
|
+
const problems = [];
|
|
367
|
+
if (!findFileByExt(join(stagingDir, 'node_modules', 'koffi', 'build'), '.node')) {
|
|
368
|
+
problems.push('koffi 缺少原生二进制(install 脚本未执行或 prebuild 解压失败)');
|
|
369
|
+
}
|
|
370
|
+
const fsExtDir = join(stagingDir, 'node_modules', 'fs-ext');
|
|
371
|
+
if (!existsSync(join(fsExtDir, 'fs-ext.js'))) {
|
|
372
|
+
problems.push('fs-ext 包文件不完整(缺少 fs-ext.js)');
|
|
373
|
+
} else if (!findFileByExt(join(fsExtDir, 'build'), '.node')) {
|
|
374
|
+
problems.push('fs-ext 缺少编译产物 build/Release/fs-ext.node(NAS 需要 python3/make/g++ 工具链)');
|
|
375
|
+
}
|
|
376
|
+
if (problems.length > 0) throw new Error(`原生模块校验失败:${problems.join(';')}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 递归查找目录下第一个指定扩展名的文件,目录不存在或未找到返回 null。 */
|
|
380
|
+
function findFileByExt(dir, ext) {
|
|
381
|
+
try {
|
|
382
|
+
for (const entry of readdirSync(dir, { recursive: true })) {
|
|
383
|
+
if (entry.endsWith(ext)) return join(dir, entry);
|
|
384
|
+
}
|
|
385
|
+
} catch { /* 目录不存在 */ }
|
|
386
|
+
return null;
|
|
343
387
|
}
|
|
344
388
|
|
|
345
389
|
/**
|
|
@@ -479,6 +523,14 @@ async function main() {
|
|
|
479
523
|
|
|
480
524
|
log(`发现新版本:${current} -> ${target}`);
|
|
481
525
|
await downloadIntoStaging(target, release.base);
|
|
526
|
+
|
|
527
|
+
// fnOS 运行时补丁:插件升级路径绕过了 FPK 构建期的 patch.py,裸 npm 包
|
|
528
|
+
// 在局域网访问时会退化(settings unavailable / 403),必须在换装前对
|
|
529
|
+
// staging 补齐。任何关键补丁未命中都在此抛错中止——此时还没换装,旧版
|
|
530
|
+
// 原样保留,天然安全。
|
|
531
|
+
setState('downloading', '正在应用飞牛运行时补丁 …');
|
|
532
|
+
applyFnosPatches(stagingDir);
|
|
533
|
+
|
|
482
534
|
swapNodeModules();
|
|
483
535
|
|
|
484
536
|
setState('restarting', '正在重启 DeepSeek Harness …');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
4
4
|
"description": "Self-update plugin for DeepSeek Harness: DSH core upgrades via detached swap script; plugin self-update installs in-place without killing the host and prompts for restart. DSH 主程序与已装插件的一站式在线更新插件。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|