dsh-selfupdater 0.4.24 → 0.4.25

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.
@@ -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
@@ -15,6 +15,7 @@ import net from 'node:net';
15
15
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
16
16
  import { dirname, join, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
+ import { applyFnosPatches } from './fnos-patches.mjs';
18
19
 
19
20
  /** 插件自身安装位置(…/node_modules/dsh-selfupdater),用于定位状态文件所在目录。 */
20
21
  const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -479,6 +480,14 @@ async function main() {
479
480
 
480
481
  log(`发现新版本:${current} -> ${target}`);
481
482
  await downloadIntoStaging(target, release.base);
483
+
484
+ // fnOS 运行时补丁:插件升级路径绕过了 FPK 构建期的 patch.py,裸 npm 包
485
+ // 在局域网访问时会退化(settings unavailable / 403),必须在换装前对
486
+ // staging 补齐。任何关键补丁未命中都在此抛错中止——此时还没换装,旧版
487
+ // 原样保留,天然安全。
488
+ setState('downloading', '正在应用飞牛运行时补丁 …');
489
+ applyFnosPatches(stagingDir);
490
+
482
491
  swapNodeModules();
483
492
 
484
493
  setState('restarting', '正在重启 DeepSeek Harness …');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfupdater",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
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",