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 +26 -0
- package/README.md +8 -6
- package/client/client.js +9 -1
- package/lib/bus.js +1 -1
- package/lib/cloud/home.js +10 -1
- package/lib/web.js +139 -28
- package/package.json +1 -1
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.
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
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
|
-
? {
|
|
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
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
}),
|