dsh-chat-embed 0.6.0 → 0.6.1

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 CHANGED
@@ -20,7 +20,7 @@
20
20
  | `locale/{en,zh}.json` | 插件管理器里显示的标题与简介 |
21
21
  | `cordis.patch.yml` | 组合补丁:向 profile 插入 `id: chat-embed` 的 loader 行 |
22
22
  | `scripts/verify-bundle.mjs` | 门禁:语法 / 清单 / loader 契约 / 服务注册表 / 显示资源 |
23
- | `test/` | 纯 Node 契约测试(伪造 DOM + 伪造桌面桥),57 项 |
23
+ | `test/` | 纯 Node 契约测试(伪造 DOM + 伪造桌面桥 + 伪造 asar),81 项 |
24
24
  | `docs/architecture.md` | 内嵌契约、席位架构、壳补丁、升级复验清单(**接到项目先读这份**) |
25
25
  | `tools/shell-patch/` | 可选的桌面壳补丁:持久分区(跨重启免登录)+ 关闭磁盘缓存 |
26
26
  | `清理聊天缓存.cmd` / `restore-shell-patch.cmd` | 缓存回收 / 还原原版壳 |
@@ -190,6 +190,8 @@ node tools/shell-patch/patch-asar.cjs --help # 查看参数
190
190
 
191
191
  「检查补丁配置」调用 Host 侧只读探针(`GET /dsh-chat-embed/patch-check`),报告安装目录、`app.asar` 是否可写、是否已打补丁、补丁点是否还在(壳版本是否兼容)、备份与待替换文件是否存在、`app.asar` 的大小与内容哈希;报告同时复制到剪贴板,方便直接贴出去。
192
192
 
193
+ **不需要自己装 Node.js**:探针会找出桌面版自带的 Node 运行时(`~/.dsh/dsh-runtimes/*/dependencies/node/`),并把一条完整可复制的命令写进报告——含正确的 Node 路径、补丁脚本路径和 `--resources` 安装目录(自定义/便携安装也能用)。
194
+
193
195
  Host 侧还注册了只读工具 **`dsh_chat_embed_patch_check`**:可以直接问 agent「我的聊天为什么重启后要重新登录」,它会自己去查这些事实,而不是让你跑命令再贴输出。
194
196
 
195
197
  补丁做两件事:`persist:` 固定分区(登录落盘)+ `cache:false`(不落 Chromium 缓存)。
@@ -212,7 +214,7 @@ dsh-chat-embed/
212
214
  ├── package.json # 包名/版本、dsh.bundle.patch、dsh.client、exports/files
213
215
  ├── cordis.patch.yml # 组合补丁:插入 id=chat-embed 的 loader 行
214
216
  ├── index.js # Host 半侧:补丁探针(HTTP + agent 工具)
215
- ├── client.js # Client 半侧(约 3870 行,手写产物)
217
+ ├── client.js # Client 半侧(约 3912 行,手写产物)
216
218
  ├── locale/{en,zh}.json # 插件管理器显示用 meta
217
219
  ├── icon.svg # 插件图标(插件管理器显示)
218
220
  ├── assets/
@@ -262,7 +264,7 @@ git clone <repo-url> dsh-chat-embed
262
264
  cd dsh-chat-embed
263
265
 
264
266
  # 无需安装依赖;直接跑门禁与测试
265
- npm run check # 语法 + 清单 + 显示资源 + 服务注册表 + 76 项测试
267
+ npm run check # 语法 + 清单 + 显示资源 + 服务注册表 + 81 项测试
266
268
  npm test # 只跑测试
267
269
  ```
268
270
 
@@ -313,6 +315,7 @@ plugin_manager { "action": "install_bundle", "target": "<本仓库克隆路径>"
313
315
 
314
316
  | 版本 | 变化 |
315
317
  | --- | --- |
318
+ | `0.6.1` | 探针找出桌面版自带的 Node 运行时,自检报告与「复制补丁说明」直接给出可执行命令(含 `--resources`,支持自定义安装目录);无需用户另装 Node |
316
319
  | `0.6.0` | 补丁可观测性:四态检测(未打 / 已开启 / **升级后失效** / 未检测)、未开启时的风险提示、菜单内「检查补丁配置」自检(Host 只读探针 `GET /dsh-chat-embed/patch-check`)、agent 工具 `dsh_chat_embed_patch_check`;Host 半侧改为读取真实 `app.asar`(`original-fs`,绕过 Electron 的 asar 目录拦截) |
317
320
  | `0.5.4` | 语言切换即时生效:翻译器改为 locale 绑定的单一来源,监听 `locale/change` 重渲染(不再依赖未注入的 `props.t`);补语言切换测试 |
318
321
  | `0.5.3` | 修复 `locale` 探针(`t` 作用域错误)并补测试;诊断读数在测试环境下的 `<html lang>` 兼容 |
package/client.js CHANGED
@@ -103,7 +103,7 @@ window.__ModuleLoader__.load({
103
103
  let openRightTabImpl = () => {}
104
104
 
105
105
  /** Build identity, surfaced in the panel so a packaged app can be diagnosed. */
106
- const BUILD = '0.6.0'
106
+ const BUILD = '0.6.1'
107
107
 
108
108
  /** Where a re-applied instance finds the one it must take over from. */
109
109
  const INSTANCE_KEY = '__DSH_CHAT_EMBED_INSTANCE__'
@@ -2433,23 +2433,36 @@ window.__ModuleLoader__.load({
2433
2433
  * @param {(key: string) => string} t
2434
2434
  * @returns {string}
2435
2435
  */
2436
- function patchInstructions(t) {
2437
- return [
2438
- t('persistLabel') + ':' + t('persistHint'),
2439
- '',
2440
- '1) 在插件包目录(或仓库根目录)执行:',
2441
- ' node tools/shell-patch/patch-asar.cjs build',
2442
- ' 脚本会自动探测安装目录并逐字节校验,生成 app.asar.new。',
2443
- '',
2444
- '2) 完全退出 DSH 后替换(任选其一):',
2445
- ' - 运行 tools/shell-patch/watchdog.ps1,退出瞬间自动替换;',
2446
- ' - 手动把 resources\\app.asar.new 改名为 app.asar。',
2447
- '',
2448
- '3) 重启 DSH,回到这里应显示「' + t('persistOn') + '」。',
2449
- '',
2450
- '回滚:restore-shell-patch.cmd(把 app.asar.orig 换回去)。',
2451
- 'Windows 之外的安装目录请用 --resources 指定。',
2452
- ].join('\n')
2436
+ /**
2437
+ * The clipboard copy behind "copy the patch instructions".
2438
+ *
2439
+ * The Host's probe supplies the exact command — including the Node binary
2440
+ * that ships with the desktop app, so the user is not asked to install one.
2441
+ *
2442
+ * @param {(key: string) => string} t
2443
+ * @param {any} [probe]
2444
+ * @returns {string}
2445
+ */
2446
+ function patchInstructions(t, probe) {
2447
+ const info = probe !== undefined && probe !== null ? probe : {}
2448
+ const command =
2449
+ typeof info.patchCommand === 'string' && info.patchCommand !== ''
2450
+ ? info.patchCommand
2451
+ : 'node tools/shell-patch/patch-asar.cjs build'
2452
+ const lines = [t('persistLabel') + ':' + t('persistHint'), '']
2453
+ lines.push('1) 执行下面这条命令(Desk 自带的 Node 运行时;无需另装 Node):')
2454
+ lines.push(' ' + command)
2455
+ lines.push(' 脚本会自动探测安装目录并逐字节校验,生成 app.asar.new。')
2456
+ lines.push('')
2457
+ lines.push('2) 完全退出 DSH 后替换(任选其一):')
2458
+ lines.push(' - 运行 tools/shell-patch/watchdog.ps1,退出瞬间自动替换;')
2459
+ lines.push(' - 手动把 resources\\app.asar.new 改名为 app.asar。')
2460
+ lines.push('')
2461
+ lines.push('3) 重启 DSH,回到这里应显示「' + t('persistOn') + '」。')
2462
+ lines.push('')
2463
+ lines.push('回滚:restore-shell-patch.cmd(把 app.asar.orig 换回去)。')
2464
+ lines.push('Windows 之外的安装目录请用 --resources 指定。')
2465
+ return lines.join('\n')
2453
2466
  }
2454
2467
 
2455
2468
  /**
@@ -2461,6 +2474,9 @@ window.__ModuleLoader__.load({
2461
2474
  */
2462
2475
  const patchCheck = { running: false, report: '' }
2463
2476
 
2477
+ /** One-shot cache of the Host probe, so both actions read the same facts. */
2478
+ const patchProbe = { loaded: false, value: undefined }
2479
+
2464
2480
  /**
2465
2481
  * Ask the Host half for the shell facts and render a copyable report.
2466
2482
  * Nothing here writes: the button only inspects.
@@ -2468,11 +2484,15 @@ window.__ModuleLoader__.load({
2468
2484
  * @param {(key: string) => string} t
2469
2485
  * @returns {Promise<void>}
2470
2486
  */
2471
- async function runPatchCheck(t) {
2472
- if (patchCheck.running) return
2473
- patchCheck.running = true
2474
- notify()
2475
- let report = ''
2487
+ /**
2488
+ * Ask the Host for the shell facts once per session. Both the copy action
2489
+ * and the self-check read it, so the instructions never disagree with the
2490
+ * report.
2491
+ *
2492
+ * @returns {Promise<any>} the probe, or undefined when the route is absent
2493
+ */
2494
+ async function fetchProbe() {
2495
+ if (patchProbe.loaded) return patchProbe.value
2476
2496
  try {
2477
2497
  // The Host half serves this route; a relative fetch therefore works in
2478
2498
  // both the desktop app and the Web GUI, under any deployment prefix.
@@ -2481,7 +2501,23 @@ window.__ModuleLoader__.load({
2481
2501
  headers: { accept: 'application/json' },
2482
2502
  cache: 'no-store',
2483
2503
  })
2484
- const probe = response.ok ? await response.json() : undefined
2504
+ patchProbe.value = response.ok ? await response.json() : undefined
2505
+ } catch (error) {
2506
+ patchProbe.value = undefined
2507
+ void error
2508
+ }
2509
+ patchProbe.loaded = true
2510
+ return patchProbe.value
2511
+ }
2512
+
2513
+ async function runPatchCheck(t) {
2514
+ if (patchCheck.running) return
2515
+ patchCheck.running = true
2516
+ notify()
2517
+ let report = ''
2518
+ try {
2519
+ patchProbe.loaded = false
2520
+ const probe = await fetchProbe()
2485
2521
  report = formatPatchReport(t, probe)
2486
2522
  } catch (error) {
2487
2523
  report = formatPatchReport(t, undefined) + '\n' + t('copyFailed') + ': ' + messageOf(error)
@@ -2516,6 +2552,10 @@ window.__ModuleLoader__.load({
2516
2552
  row('补丁点', probe.patchSite === true ? '存在' : probe.patchSite === false ? '不存在(壳版本不匹配)' : '未知')
2517
2553
  row('已打补丁', probe.patched === true ? '是' : probe.patched === false ? '否' : '未知')
2518
2554
  row('node', probe.node)
2555
+ row('补丁命令', probe.patchCommand)
2556
+ if (probe.nodeBinary === undefined) {
2557
+ lines.push('(没有找到可用的 Node 运行时,需要先安装 Node.js 22 或更新)')
2558
+ }
2519
2559
  row('说明', probe.detail)
2520
2560
  return lines.join('\n')
2521
2561
  }
@@ -3417,7 +3457,7 @@ window.__ModuleLoader__.load({
3417
3457
  className: 'dce-menu-item',
3418
3458
  'data-patch-help': 'true',
3419
3459
  onClick: () => {
3420
- void copyText(patchInstructions(t))
3460
+ void fetchProbe().then((probe) => copyText(patchInstructions(t, probe)))
3421
3461
  },
3422
3462
  },
3423
3463
  h('span', { className: 'dce-menu-label' }, t('persistCopy')),
@@ -3712,6 +3752,8 @@ window.__ModuleLoader__.load({
3712
3752
  storePersistenceSeen,
3713
3753
  livePartitionState,
3714
3754
  patchInstructions,
3755
+ fetchProbe,
3756
+ patchProbe,
3715
3757
  formatPatchReport,
3716
3758
  runPatchCheck,
3717
3759
  patchCheck,
@@ -3,7 +3,7 @@
3
3
  本文记录 `client.js` 里每一条**反直觉**的实现决策、支撑它的证据,以及升级桌面壳时必须重新验证的清单。
4
4
 
5
5
  目标运行环境:**`@deepseek-ai/dsh-desktop 0.2.2-rc.1`(Electron / Chromium)**。
6
- 当前实现:**v0.6.0**(`client.js` 约 3871 行,76 项契约测试)。
6
+ 当前实现:**v0.6.1**(`client.js` 约 3912 行,81 项契约测试)。
7
7
 
8
8
  > **架构一句话**:聊天页直接渲染在**席位的布局盒**里(`.dce-panel.dce-inline`,`width/height:100%`),几何完全交给 CSS;席位只有两个 —— 中央主面板(`main`)和右栏聊天 tab —— 二者互斥,**哪边点哪边接管**,切换时 guest 重建一次并回到上次会话。
9
9
 
@@ -330,7 +330,7 @@ Host 半侧另有两条只读通道,专门用来回答"为什么重启后要
330
330
 
331
331
  | 通道 | 内容 |
332
332
  | --- | --- |
333
- | `GET /dsh-chat-embed/patch-check` | 安装目录、`app.asar` 是否可写、是否已打补丁、补丁点是否还存在、备份与待替换文件、大小与内容哈希 |
333
+ | `GET /dsh-chat-embed/patch-check` | 安装目录、`app.asar` 是否可写、是否已打补丁、补丁点是否还存在、备份与待替换文件、大小与内容哈希、**可执行的补丁命令**(含自带 Node 运行时路径与 `--resources`) |
334
334
  | agent 工具 `dsh_chat_embed_patch_check` | 同一份读数,供模型直接查询(只读,无需审批) |
335
335
 
336
336
  > 读取 `app.asar` 必须走 Electron 的 `original-fs`:被补丁过的 `node:fs` 会把以 `.asar` 结尾的路径当成目录,`readFileSync` 直接 ENOENT。探针优先用 `original-fs`,普通 Node 下回落到 `node:fs`。
@@ -5,13 +5,13 @@
5
5
  ## 版本与提交约定
6
6
 
7
7
  - 同时改 `package.json` 的 `version` 与 `client.js` 的 `BUILD`,两者必须一致;
8
- - 门禁必须绿:`npm run check`(`verify-bundle` + 76 项契约测试);
8
+ - 门禁必须绿:`npm run check`(`verify-bundle` + 81 项契约测试);
9
9
  - 代码注释/提交信息英文,文档中文。
10
10
 
11
11
  ## 发版步骤
12
12
 
13
13
  ```powershell
14
- npm run check # 门禁:verify-bundle + 76 项测试(prepublishOnly 也会跑)
14
+ npm run check # 门禁:verify-bundle + 81 项测试(prepublishOnly 也会跑)
15
15
  npm run pack:dist # 产出 dist\dsh-chat-embed-<版本>.tgz
16
16
  gh release create v0.5.0 dist\dsh-chat-embed-0.5.0.tgz --title "v0.5.0" --notes-file dist\release-notes.md
17
17
  ```
@@ -31,8 +31,8 @@ Get-FileHash $env:TEMP\check\dsh-chat-embed-0.5.0.tgz -Algorithm SHA256
31
31
  | --- | --- |
32
32
  | 仓库 | <https://github.com/poluce/dsh-chat-embed> |
33
33
  | Release | <https://github.com/poluce/dsh-chat-embed/releases> |
34
- | 分发包 | `dsh-chat-embed-0.6.0.tgz`(16 个文件,约 70 kB) |
35
- | npm | `dsh-chat-embed@0.6.0`(<https://www.npmjs.com/package/dsh-chat-embed>,维护者 `poluce_auto`) |
34
+ | 分发包 | `dsh-chat-embed-0.6.1.tgz`(16 个文件,约 70 kB) |
35
+ | npm | `dsh-chat-embed@0.6.1`(<https://www.npmjs.com/package/dsh-chat-embed>,维护者 `poluce_auto`) |
36
36
  | 商店图标 | `assets/icon-180.png`(180×180 PNG) |
37
37
  | 商店截图 | `assets/screenshots/` + 仓库根 `screenshots.json`(6 张) |
38
38
 
package/index.js CHANGED
@@ -18,10 +18,11 @@
18
18
  */
19
19
 
20
20
  import { createHash } from 'node:crypto'
21
- import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs'
21
+ import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
22
22
  import { homedir } from 'node:os'
23
23
  import { dirname, join } from 'node:path'
24
24
  import { createRequire } from 'node:module'
25
+ import { fileURLToPath } from 'node:url'
25
26
 
26
27
  export const name = 'dsh-chat-embed'
27
28
 
@@ -153,6 +154,72 @@ function readAsarEntry(file, entry) {
153
154
  return buffer.toString('utf8', offset, offset + Number(node.size))
154
155
  }
155
156
 
157
+ /**
158
+ * Locate a Node binary that can run the patch tool.
159
+ *
160
+ * DSH runs every profile on its own packaged runtime, so the archive can be
161
+ * patched without the user installing Node. The order is: the runtime this
162
+ * Host is already running on, the managed runtime directory, then a system
163
+ * `node` on PATH.
164
+ *
165
+ * @returns {string | undefined} an absolute path, or undefined when none exists
166
+ */
167
+ function findNodeBinary() {
168
+ const candidates = []
169
+ // A Node-hosted dsh reports its own executable.
170
+ if (typeof process.execPath === 'string' && process.execPath !== '' && /node(.exe)?$/i.test(process.execPath)) {
171
+ candidates.push(process.execPath)
172
+ }
173
+ // The managed runtime the desktop app installs beside its own data.
174
+ const home = process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== '' ? process.env.DSH_HOME : join(homedir(), '.dsh')
175
+ const runtimes = join(home, 'dsh-runtimes')
176
+ try {
177
+ for (const entry of readdirSync(runtimes)) {
178
+ const base = join(runtimes, entry, 'dependencies', 'node', 'bin')
179
+ candidates.push(join(base, process.platform === 'win32' ? 'node.exe' : 'node'))
180
+ }
181
+ } catch (error) {
182
+ void error
183
+ }
184
+ if (process.platform === 'win32' && process.env.ProgramFiles !== undefined) {
185
+ candidates.push(join(process.env.ProgramFiles, 'nodejs', 'node.exe'))
186
+ } else {
187
+ candidates.push('/usr/local/bin/node')
188
+ candidates.push('/usr/bin/node')
189
+ }
190
+ for (const candidate of candidates) {
191
+ if (typeof candidate === 'string' && candidate !== '' && existsSync(candidate)) return candidate
192
+ }
193
+ return undefined
194
+ }
195
+
196
+ /**
197
+ * @param {string} node absolute path to a Node binary
198
+ * @param {string} script absolute path to patch-asar.cjs
199
+ * @param {string} [resources] install directory, so a non-standard location works
200
+ * @returns {string} a copy-pasteable command line
201
+ */
202
+ function patchCommand(node, script, resources) {
203
+ const quote = (value) => (value.includes(' ') ? '"' + value + '"' : value)
204
+ const parts = [quote(node), quote(script), 'build']
205
+ // The tool only searches the usual install locations. When the Host found the
206
+ // shell somewhere else (a portable or custom install), it must say where.
207
+ if (typeof resources === 'string' && resources !== '') parts.push('--resources', quote(resources))
208
+ return parts.join(' ')
209
+ }
210
+
211
+ /**
212
+ * Locate the patch tool inside this package, so the reported command points at
213
+ * a file that actually exists on the user's disk.
214
+ *
215
+ * @returns {string | undefined}
216
+ */
217
+ function findPatchScript() {
218
+ const here = dirname(fileURLToPath(import.meta.url))
219
+ const candidate = join(here, 'tools', 'shell-patch', 'patch-asar.cjs')
220
+ return existsSync(candidate) ? candidate : undefined
221
+ }
222
+
156
223
  /**
157
224
  * Inspect the desktop shell and the optional patch. Read-only: no file here is
158
225
  * written, so this is safe to call from a diagnostics button.
@@ -222,6 +289,13 @@ function inspectShell(resources) {
222
289
  } catch (error) {
223
290
  report.detail = '解析 app.asar 失败:' + firstLine(error)
224
291
  }
292
+ // The user needs a Node binary to run the patch tool. The desktop app ships
293
+ // its own runtime, so nobody has to install one — report the exact command
294
+ // instead of telling them to install Node first.
295
+ report.patchScript = findPatchScript()
296
+ report.nodeBinary = findNodeBinary()
297
+ report.patchCommand =
298
+ report.nodeBinary === undefined ? undefined : patchCommand(report.nodeBinary, report.patchScript, resolved)
225
299
  return report
226
300
  }
227
301
 
@@ -332,4 +406,14 @@ export function apply(ctx) {
332
406
  }
333
407
 
334
408
  /** Exposed for tests and for the patch checker to reuse without a Context. */
335
- export const internals = { inspectShell, detectResources, readAsarEntry, readRealFile, resourcesCandidates, PROBE_PATH }
409
+ export const internals = {
410
+ inspectShell,
411
+ detectResources,
412
+ readAsarEntry,
413
+ readRealFile,
414
+ resourcesCandidates,
415
+ findNodeBinary,
416
+ findPatchScript,
417
+ patchCommand,
418
+ PROBE_PATH,
419
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-chat-embed",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "author": "poluce <fishsummer@126.com>",
5
5
  "description": "Multi-tab AI chat embed for the DeepSeek Harness desktop app: DeepSeek, 豆包, ChatGPT, MiniMax, Kimi, Gemini and Grok, each signed in separately.",
6
6
  "type": "module",