dsh-crossterm 0.7.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # dsh-crossterm 变更记录
2
2
 
3
+ ## 0.7.1 — 2026-10-19
4
+
5
+ ### 「更新」按钮在别的机器上装不上:三级兜底 + 全步骤回执
6
+
7
+ - 现象(User said, paraphrased):别的设备点卡片里的「更新」,看起来装不上。
8
+ - 根因:旧实现只有两条路 —— PATH 里的 `dsh`,或 `%USERPROFILE%\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\{node,pnpm}`
9
+ 这一固定目录。桌面版默认**不把 `dsh` 放进 PATH**,别的机器也不一定有这套运行时目录,
10
+ 于是直接落到「找不到可用的 dsh 命令」,卡片只显示一行字,看不出卡在哪一步。
11
+ - 桌面版的 `dsh` 真身(`F:\dsh\resources\runtime\cli\bin\dsh.cmd`):
12
+ `set ELECTRON_RUN_AS_NODE=1` + `DeepSeek Harness.exe --expose-internals <resources>\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js %*`,
13
+ 而 `dsh plugin ...` 内部就是在 profile 目录里跑 pnpm(`--profile` 必填)。所以不依赖 PATH 也能调起同一个 CLI。
14
+ - 现行三级兜底(`lib/web.js` 的 `updatePlugin(term, info, latest)`,按顺序尝试,第一个成功即止,每步都记档):
15
+ 1. `dsh plugin --profile <p> add dsh-crossterm@latest`;
16
+ 2. DSH 自带 CLI:`process.execPath --expose-internals <resources>\app.asar\...\cli.js plugin --profile <p> add ...`
17
+ (env 加 `ELECTRON_RUN_AS_NODE=1`);
18
+ 3. 直接 pnpm:`pnpmCandidates()` 依次找 `<DSH_HOME>\dsh-runtimes\...\dependencies\pnpm\bin\pnpm.cjs`
19
+ 与 `<安装目录>\resources\runtime\pnpm\bin\pnpm.cjs`;node 用自带 `node.exe`,没有就拿宿主 Electron 当 node。
20
+ - 每步记录 `{label, cmd, code, ms, out}`;装完读 `<profile>\node_modules\dsh-crossterm\package.json` **核对真实版本**,
21
+ 回执区分「已更新到 X,重启 DSH 后生效」「已经是最新(X)」「更新没成功(当前装着 X)」。
22
+ - 卡片:失败时把每步退出码、耗时、输出尾部(各 400 字)直接显示在卡片里,不用去翻宿主日志。
23
+ - 更新日志:`<busDir>\logs\update.log` 追加 JSON 行(含 profile / before / installed / latest / steps),
24
+ 别的会话排查时直接读这个文件。
25
+ - 顺带补 0.7.0 漏的字段:`GET /crossterm/api/status` 的 `home.layout` 现在带 `rulesVersion` / `rulesFailed` / `accountSource`。
26
+ - 探针 `F:\dsh\001X\crossterm-work\probe-update-plan.mjs` **14 项全 PASS**。
27
+ - 要不要跟着动手:**不用**,重启 DSH 即生效(本机是 `link:` 源码版,重启加载即可)。
28
+
3
29
  ## 0.7.0 — 2026-10-19
4
30
 
5
31
  ### 入口行升格为「总开关」,排到组件列表第一位
package/README.md CHANGED
@@ -28,12 +28,14 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
28
28
  > `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
29
29
  > 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
30
30
 
31
- **本版更新(0.7.0)**
32
-
33
- - **第一个组件变成总开关**:排到列表最前,标注「跨端互通(总开关)」,说明写明关掉它会停掉下面四个组件。
34
- - 它不再是「只关卡片的入口行」:现在用 cordis 服务注入实现 —— ui 行 `provide('crossterm')`,
35
- 其余四行 `inject` 它;**关掉总开关,那四个组件连回调都不会执行**,工具/心跳/云同步/中转站/备份一起停。
36
- - 四个业务组件仍可各自单独开关(总开关开着时)。
31
+ **本版更新(0.7.1)**
32
+
33
+ - **修「别的机器点更新装不上」**:更新不再只依赖 PATH 里的 `dsh`,改成三级兜底 ——
34
+ ① PATH 里的 `dsh`;② DSH 自带 CLI(`Electron --expose-internals <安装目录>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js`);
35
+ ③ 直接调自带 pnpm(`<DSH_HOME>\dsh-runtimes\...\dependencies\pnpm` 或 `<安装目录>\resources\runtime\pnpm`)。
36
+ - 装完会读 `<profile>\node_modules\dsh-crossterm\package.json` **核对真实版本**,分别提示「已更新到 X」「已经是最新」「更新没成功」。
37
+ - 失败时卡片直接把每步的退出码、耗时、输出尾部显示出来;全过程写 `<总线目录>\logs\update.log`。
38
+ - 顺带补上 `status.home.layout` 里缺的 `rulesVersion` / `rulesFailed` / `accountSource`。
37
39
 
38
40
  ## 一、安装与卸载
39
41
 
package/client/client.js CHANGED
@@ -255,7 +255,15 @@ window.__ModuleLoader__.load({
255
255
  setMsg("正在更新…");
256
256
  try {
257
257
  const data = await call("/crossterm/api/update", "POST");
258
- setMsg((data && data.message) || (data && data.ok ? "已更新" : "未能更新"));
258
+ const text = (data && data.message) || (data && data.ok ? "已更新" : "未能更新");
259
+ const steps = (data && data.steps) || [];
260
+ // 失败时把每条尝试(dsh / DSH 自带 cli / pnpm)的退出码与输出带上,方便远端排查
261
+ const detail = data && data.ok
262
+ ? ""
263
+ : steps.length
264
+ ? "\n\n" + steps.map((s) => `[${s.label}] 退出码 ${s.code} · ${Math.round((s.ms || 0) / 1000)}s` + (s.out ? `\n${String(s.out).slice(-400)}` : "")).join("\n\n")
265
+ : "";
266
+ setMsg(text + detail);
259
267
  } catch (error) {
260
268
  setMsg("更新失败:" + String((error && error.message) || error));
261
269
  } finally {
package/lib/bus.js CHANGED
@@ -22,7 +22,7 @@ import os from 'node:os'
22
22
  import path from 'node:path'
23
23
  import crypto from 'node:crypto'
24
24
 
25
- export const VERSION = '0.7.0'
25
+ export const VERSION = '0.7.1'
26
26
 
27
27
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
28
28
  export function dshHome() {
package/lib/cloud/home.js CHANGED
@@ -119,7 +119,16 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
119
119
  return {
120
120
  home: `${cfg.cloudHome || DEFAULT_HOME}`,
121
121
  layout: layout
122
- ? { account: layout.account, role: layout.role, deviceId: layout.deviceId, paths: layout.paths, ensuredAt: layout.ensuredAt }
122
+ ? {
123
+ account: layout.account,
124
+ role: layout.role,
125
+ deviceId: layout.deviceId,
126
+ paths: layout.paths,
127
+ ensuredAt: layout.ensuredAt,
128
+ rulesVersion: layout.rulesVersion || 0,
129
+ rulesFailed: layout.rulesFailed || 0,
130
+ accountSource: layout.accountSource || '',
131
+ }
123
132
  : null,
124
133
  relay: { ...relay.stats(), intervalMs: relayTimer },
125
134
  backup: { ...backupSummary(backupState), intervalMs: backupTimer },
package/lib/web.js CHANGED
@@ -163,6 +163,7 @@ function run(cmd, args, cwd, opts) {
163
163
  cwd,
164
164
  shell,
165
165
  windowsHide: true,
166
+ env: opts && opts.env ? { ...process.env, ...opts.env } : process.env,
166
167
  })
167
168
  } catch (error) {
168
169
  resolve({ code: -1, out: String((error && error.message) || error) })
@@ -197,19 +198,46 @@ function updateErrorText(result) {
197
198
  return tail
198
199
  }
199
200
 
200
- // 有些机器没把 `dsh` 命令放进 PATH(桌面版就是),这时退回 DSH 自带运行时里的
201
- // node + pnpm 直接装:<DSH_HOME>\dsh-runtimes\dsh-primary-runtime\dependencies\{node,pnpm}
202
- function runtimeTools(cfg) {
201
+ // DSH 安装根目录:process.resourcesPath = <root>\resources,上一级就是 <root>
202
+ function dshRoot() {
203
+ if (process.resourcesPath) return path.dirname(process.resourcesPath)
204
+ return path.dirname(process.execPath || '')
205
+ }
206
+
207
+ // 桌面版把 `dsh` 命令实现成「让 Electron 以 node 模式跑 cli.js」:
208
+ // <root>\DeepSeek Harness.exe --expose-internals <root>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js
209
+ // 所以不依赖 PATH 也能调起同一个 CLI(`dsh plugin add` 内部其实就是 pnpm add)。
210
+ function cliScript() {
211
+ const res = process.resourcesPath || path.join(dshRoot(), 'resources')
212
+ return path.join(res, 'app.asar', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
213
+ }
214
+
215
+ function dshHome(cfg) {
203
216
  const busDir = (cfg && cfg.busDir) || ''
204
- const home = busDir ? path.dirname(busDir) : path.join(os.homedir(), '.dsh')
205
- const deps = path.join(home, 'dsh-runtimes', 'dsh-primary-runtime', 'dependencies')
206
- return {
207
- node: path.join(deps, 'node', 'bin', 'node.exe'),
208
- pnpm: path.join(deps, 'pnpm', 'bin', 'pnpm.cjs'),
209
- ok:
210
- fs.existsSync(path.join(deps, 'node', 'bin', 'node.exe')) &&
211
- fs.existsSync(path.join(deps, 'pnpm', 'bin', 'pnpm.cjs')),
212
- }
217
+ return busDir ? path.dirname(busDir) : path.join(os.homedir(), '.dsh')
218
+ }
219
+
220
+ // pnpm 可能落在两处:DSH_HOME 的运行时依赖里,或安装目录的 resources\runtime\pnpm
221
+ function pnpmCandidates(cfg) {
222
+ const deps = path.join(dshHome(cfg), 'dsh-runtimes', 'dsh-primary-runtime', 'dependencies')
223
+ const res = process.resourcesPath || path.join(dshRoot(), 'resources')
224
+ return [
225
+ path.join(deps, 'pnpm', 'bin', 'pnpm.cjs'),
226
+ path.join(res, 'runtime', 'pnpm', 'bin', 'pnpm.cjs'),
227
+ ].filter((p) => p && fs.existsSync(p))
228
+ }
229
+
230
+ // 没有独立 node.exe 时用宿主自己的 Electron 以 node 模式跑脚本
231
+ function nodeTool(cfg) {
232
+ const deps = path.join(dshHome(cfg), 'dsh-runtimes', 'dsh-primary-runtime', 'dependencies')
233
+ const bundled = path.join(deps, 'node', 'bin', 'node.exe')
234
+ if (fs.existsSync(bundled)) return { cmd: bundled, env: {} }
235
+ return { cmd: process.execPath, env: { ELECTRON_RUN_AS_NODE: '1' } }
236
+ }
237
+
238
+ function runtimeTools(cfg) {
239
+ const pnpm = pnpmCandidates(cfg)[0] || ''
240
+ return { node: nodeTool(cfg).cmd, pnpm, ok: Boolean(pnpm) }
213
241
  }
214
242
 
215
243
  function looksMissingCommand(result) {
@@ -218,16 +246,91 @@ function looksMissingCommand(result) {
218
246
  )
219
247
  }
220
248
 
221
- async function updatePlugin(term, info) {
249
+ /** 读 profile 里实际装着的版本(registry 模式下 node_modules/<pkg>/package.json) */
250
+ function installedVersion(info) {
251
+ try {
252
+ const doc = JSON.parse(fs.readFileSync(path.join(info.dir, 'node_modules', PKG, 'package.json'), 'utf8'))
253
+ return typeof doc.version === 'string' ? doc.version : ''
254
+ } catch {
255
+ return ''
256
+ }
257
+ }
258
+
259
+ // 更新过程落一条日志,别的会话/排查时能直接看:<busDir>\logs\update.log
260
+ function appendUpdateLog(cfg, record) {
261
+ try {
262
+ const dir = path.join((cfg && cfg.busDir) || path.join(os.homedir(), '.dsh', 'crossterm'), 'logs')
263
+ fs.mkdirSync(dir, { recursive: true })
264
+ fs.appendFileSync(path.join(dir, 'update.log'), `${JSON.stringify({ at: new Date().toISOString(), ...record })}\n`, 'utf8')
265
+ } catch {
266
+ /* 日志写不进去不影响更新本身 */
267
+ }
268
+ }
269
+
270
+ /**
271
+ * 更新已安装的 registry 版:按「PATH 里的 dsh → DSH 自带 CLI → 直接 pnpm」依次尝试,
272
+ * 每条都记进 steps(命令、退出码、耗时、输出尾部),最后核对 node_modules 里的真实版本。
273
+ */
274
+ async function updatePlugin(term, info, latest) {
222
275
  const target = `${PKG}@latest`
223
- const direct = await run('dsh', ['plugin', '--profile', info.name, 'add', target], info.dir)
224
- if (direct.code === 0) return { ok: true, via: 'dsh', out: direct.out }
225
- const miss = looksMissingCommand(direct) || direct.out.trim() === ''
226
- if (!miss) return { ok: false, via: 'dsh', out: direct.out }
227
- const tools = runtimeTools(term.cfg)
228
- if (!tools.ok) return { ok: false, via: 'none', out: updateErrorText(direct) }
229
- const fallback = await run(tools.node, [tools.pnpm, 'add', target], info.dir, { shell: false })
230
- return { ok: fallback.code === 0, via: 'pnpm', out: fallback.out || updateErrorText(fallback) }
276
+ const before = installedVersion(info)
277
+ const steps = []
278
+ const attempt = async (label, cmd, args, opts) => {
279
+ const started = Date.now()
280
+ const res = await run(cmd, args, (opts && opts.cwd) || info.dir, opts)
281
+ steps.push({
282
+ label,
283
+ cmd: `${path.basename(cmd)} ${args.join(' ')}`.slice(0, 200),
284
+ code: res.code,
285
+ ms: Date.now() - started,
286
+ out: (res.out || '').trim().slice(-600),
287
+ })
288
+ return res
289
+ }
290
+ const finish = (via) => {
291
+ const installed = installedVersion(info)
292
+ const updated = Boolean(installed) && installed !== before
293
+ const upToDate = Boolean(installed) && Boolean(latest) && compareVersion(installed, latest) >= 0
294
+ const ok = updated || upToDate
295
+ appendUpdateLog(term.cfg, { profile: info.name, ok, updated, via, before, installed, latest: latest || '', steps })
296
+ return { ok, updated, via, before, installed, latest: latest || '', steps }
297
+ }
298
+
299
+ const { cmd: node, env } = nodeTool(term.cfg)
300
+ const cli = cliScript()
301
+ const plan = [{ label: 'dsh', cmd: 'dsh', args: ['plugin', '--profile', info.name, 'add', target], opts: {} }]
302
+ if (fs.existsSync(cli)) {
303
+ plan.push({
304
+ label: 'cli',
305
+ cmd: process.execPath,
306
+ args: ['--expose-internals', cli, 'plugin', '--profile', info.name, 'add', target],
307
+ opts: { shell: false, env: { ELECTRON_RUN_AS_NODE: '1' } },
308
+ })
309
+ }
310
+ for (const pnpm of pnpmCandidates(term.cfg)) {
311
+ plan.push({ label: 'pnpm', cmd: node, args: [pnpm, 'add', target], opts: { shell: false, env } })
312
+ }
313
+
314
+ let last = null
315
+ for (const step of plan) {
316
+ const res = await attempt(step.label, step.cmd, step.args, step.opts)
317
+ last = res
318
+ if (res.code === 0) {
319
+ const result = finish(step.label)
320
+ if (result.ok) return result
321
+ }
322
+ }
323
+ appendUpdateLog(term.cfg, { profile: info.name, ok: false, updated: false, before, latest: latest || '', steps })
324
+ return {
325
+ ok: false,
326
+ updated: false,
327
+ via: 'none',
328
+ before,
329
+ installed: installedVersion(info),
330
+ latest: latest || '',
331
+ steps,
332
+ out: steps.map((s) => `[${s.label}] 退出码 ${s.code}\n${s.out}`).join('\n\n') || updateErrorText(last),
333
+ }
231
334
  }
232
335
 
233
336
  function statusOf(term) {
@@ -566,17 +669,25 @@ export function registerWeb(ctx, term, log) {
566
669
  })
567
670
  return
568
671
  }
569
- const result = await updatePlugin(term, info)
672
+ const latest = await latestVersion().catch(() => '')
673
+ const result = await updatePlugin(term, info, latest)
674
+ const installed = result.installed || ''
675
+ const steps = (result.steps || []).map((s) => ({ label: s.label, code: s.code, ms: s.ms, out: s.out }))
676
+ let message
677
+ if (result.ok && result.updated) message = `已更新到 ${installed},重启 DSH 后生效`
678
+ else if (result.ok) message = `已经是最新(${installed || latest}),不用更新`
679
+ else if (installed) message = `更新没成功(当前装着 ${installed}):${result.out || '命令都返回失败'}`
680
+ else message = `更新失败:${result.out || '未知原因'}`
570
681
  json(res, 200, {
571
682
  ok: result.ok,
572
683
  mode: 'registry',
573
684
  via: result.via,
574
- needRestart: result.ok,
575
- message: result.ok
576
- ? `已更新${result.via === 'pnpm' ? '(用 DSH 自带 pnpm)' : ''},重启 DSH 后生效`
577
- : result.via === 'none'
578
- ? result.out
579
- : (result.out || '').slice(-1200) || `更新失败(退出码不明)`,
685
+ before: result.before || '',
686
+ installed,
687
+ latest,
688
+ needRestart: Boolean(result.ok && result.updated),
689
+ steps,
690
+ message,
580
691
  })
581
692
  },
582
693
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "跨端互通:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",