dsh-mobile-plugin 0.1.61 → 0.1.63
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/index.js +12 -1
- package/lib/panel/index.html +1 -1
- package/lib/routes/control-plane.js +33 -19
- package/lib/shell/updater.js +15 -6
- package/lib/update-install.cjs +179 -19
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ const desktopHalf = require('../desktop-half-stats.cjs');
|
|
|
30
30
|
// 「查有没有新版」与「装新版」这两格都归插件自己:查直接问 npm 注册表,装在 profile 里跑
|
|
31
31
|
// 它自己的包管理器(见 `../update-registry.cjs` / `../update-install.cjs`)。不再借道市场。
|
|
32
32
|
const { checkForUpdate } = require('../update-registry.cjs');
|
|
33
|
-
const {
|
|
33
|
+
const { installLatestIfNeeded, declaredSource } = require('../update-install.cjs');
|
|
34
34
|
const { clearCompletionForAllDevices, invalidateSessionList } = require('../mobile/session-cache.js');
|
|
35
35
|
|
|
36
36
|
/** 一次 desktop-read 上报最多带多少个会话 id(防灌爆;客户端半边一次最多也就几个)。 */
|
|
@@ -395,6 +395,20 @@ function registerControlPlaneRoutes(deps) {
|
|
|
395
395
|
return null
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* 取一个宿主服务,拿不到就当没有。
|
|
400
|
+
*
|
|
401
|
+
* 更新这条路要用两个:`profileContext`(宿主发布的包管理器调用方式,装的时候必须优先用它)
|
|
402
|
+
* 与 `agents`(「有对话在跑就别换插件文件」那道闸的判据)。宿主没提供某个服务时如实降级,
|
|
403
|
+
* **不许让路由因此抛错** —— 服务不存在时 `ctx.get` 本身可能就不在。
|
|
404
|
+
*/
|
|
405
|
+
const hostService = key => {
|
|
406
|
+
try {
|
|
407
|
+
return ctx && typeof ctx.get === 'function' ? ctx.get(key) ?? null : null
|
|
408
|
+
} catch {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
}
|
|
398
412
|
const runtimeUnavailable = { ok: false, error: { code: 'update_unavailable', message: '这一版没有可换代的外壳(插件是被直接挂载的),装完没法就地生效,所以这里不装:无头部署请 rsync 源码后重启' } }
|
|
399
413
|
registerRoute({ kind: 'exact', path: '/api/relay/update/status', handler: localOnly('GET', async (_req, res) => {
|
|
400
414
|
const runtime = pluginRuntime()
|
|
@@ -465,25 +479,21 @@ function registerControlPlaneRoutes(deps) {
|
|
|
465
479
|
return sendJson(res, 500, { ok: false, error: { code: 'update_apply_failed', message: outcome.failure.message }, data: { ...outcome, status: snapshot } })
|
|
466
480
|
}
|
|
467
481
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
installedVersion: beforeVersion,
|
|
472
|
-
source: snapshot.marketResult?.source ?? null,
|
|
473
|
-
})
|
|
474
|
-
target = check.latestVersion
|
|
475
|
-
if (!check.updateAvailable && !force) {
|
|
476
|
-
answer({ skipped: true, state: 'succeeded', installedVersion: check.installedVersion ?? beforeVersion })
|
|
477
|
-
return sendJson(res, 200, { ok: true, data: { ...outcome, status: runtime.status() } })
|
|
478
|
-
}
|
|
479
|
-
} catch (error) {
|
|
480
|
-
answer({ failure: { code: 'update_check_failed', message: String(error?.message || error) } })
|
|
481
|
-
return sendJson(res, 500, { ok: false, error: { code: 'update_apply_failed', message: outcome.failure.message }, data: { ...outcome, status: snapshot } })
|
|
482
|
-
}
|
|
483
|
-
|
|
482
|
+
// 查一次(有没有新版)、可不可能被「有 agent 在跑」挡住、装不装,全在 `installLatestIfNeeded`
|
|
483
|
+
// 里一起判:面板那颗按钮与外壳的自动更新必须走同一套判定,否则会出现「手动说有新版、
|
|
484
|
+
// 自动说没有」这种最难查的偏差。
|
|
484
485
|
let result
|
|
485
486
|
try {
|
|
486
|
-
result = await
|
|
487
|
+
result = await installLatestIfNeeded({
|
|
488
|
+
profileNodeModules,
|
|
489
|
+
force,
|
|
490
|
+
// 宿主发布的包管理器调用方式(`{command, args, env?}`)。**必须优先用它**:PATH 上
|
|
491
|
+
// 那个 pnpm 的版本可能和 profile 的 store 对不上(实测 10.x 装 11.x 的 profile 直接
|
|
492
|
+
// `ERR_PNPM_UNEXPECTED_STORE`)。市场也是这么取的(`profileContext.packageManager`)。
|
|
493
|
+
packageManager: hostService('profileContext')?.packageManager ?? null,
|
|
494
|
+
// 「有对话在跑就先别换插件文件」那道闸的判据。force 由用户点按钮时显式带上。
|
|
495
|
+
agents: hostService('agents'),
|
|
496
|
+
})
|
|
487
497
|
} catch (error) {
|
|
488
498
|
result = { installed: false, installedVersion: beforeVersion, command: null, output: '', failure: { code: 'install_failed', message: String(error?.message || error) } }
|
|
489
499
|
}
|
|
@@ -491,8 +501,12 @@ function registerControlPlaneRoutes(deps) {
|
|
|
491
501
|
outcome.failure = result.failure ?? null
|
|
492
502
|
outcome.command = result.command ?? null
|
|
493
503
|
outcome.output = result.output ?? ''
|
|
504
|
+
outcome.retryable = result.retryable === true
|
|
505
|
+
// `skipped` 也要如实带出去:已经是最新那一条**不是失败**,面板照着它说「没有改动任何文件」。
|
|
506
|
+
outcome.skipped = result.skipped === true
|
|
507
|
+
outcome.state = result.installed || result.skipped ? 'succeeded' : 'failed'
|
|
494
508
|
if (!result.installed) {
|
|
495
|
-
return sendJson(res, 200, { ok:
|
|
509
|
+
return sendJson(res, 200, { ok: result.skipped === true, data: { ...outcome, check: result.check ?? null, status: runtime.status() } })
|
|
496
510
|
}
|
|
497
511
|
|
|
498
512
|
// 装上了才换代:换代码是外壳的事,失败要如实报(回滚由外壳负责,见 shell/runtime.js)。
|
package/lib/shell/updater.js
CHANGED
|
@@ -227,14 +227,22 @@ const TERMINAL = new Set(['succeeded', 'failed', 'cancelled', 'rolled-back'])
|
|
|
227
227
|
* 装完了但盘上版本没变(市场报成功、文件没落),这里如实返回 `installed: false`,不让调用方
|
|
228
228
|
* 以为换过代码 —— 「状态说成功、代码没换」是这个仓库吃过最贵的一次亏。
|
|
229
229
|
*/
|
|
230
|
-
export async function installIfNeeded(base, { name = PACKAGE_NAME, force = false, onEvent = () => {}, now = Date.now } = {}) {
|
|
230
|
+
export async function installIfNeeded(base, { name = PACKAGE_NAME, force = false, onEvent = () => {}, packageManager = () => null, agents = () => null, now = Date.now } = {}) {
|
|
231
231
|
const before = installedVersion()
|
|
232
|
-
// 装归实现自己(2026-10-11 起):盘上那一代里有 `update-install.cjs`
|
|
233
|
-
//
|
|
234
|
-
//
|
|
232
|
+
// 装归实现自己(2026-10-11 起):盘上那一代里有 `update-install.cjs` 就走它。判定(有没有
|
|
233
|
+
// 新版、`link:` 来源不碰、有 agent 在跑就先别换文件)也一并交给它,别在这里抄第二份 ——
|
|
234
|
+
// 外壳的自动更新与面板那颗按钮必须是同一套判定,否则会出现「手动说有新版、自动说没有」。
|
|
235
235
|
const selfInstall = await installedInstaller()
|
|
236
236
|
if (selfInstall) {
|
|
237
|
-
const outcome = await selfInstall({
|
|
237
|
+
const outcome = await selfInstall({
|
|
238
|
+
profileNodeModules: profileNodeModules(),
|
|
239
|
+
force,
|
|
240
|
+
onEvent,
|
|
241
|
+
// 宿主发布的包管理器(优先用它装)与 agents 服务(那道闸的判据)都从外壳这一层透下去:
|
|
242
|
+
// 实现在代里,它没有宿主 ctx,只有外壳有。
|
|
243
|
+
packageManager: packageManager(),
|
|
244
|
+
agents: agents(),
|
|
245
|
+
})
|
|
238
246
|
return {
|
|
239
247
|
installed: outcome.installed === true,
|
|
240
248
|
skipped: outcome.skipped === true,
|
|
@@ -245,7 +253,8 @@ export async function installIfNeeded(base, { name = PACKAGE_NAME, force = false
|
|
|
245
253
|
restartRequired: false,
|
|
246
254
|
refreshRequired: false,
|
|
247
255
|
failure: outcome.failure ?? null,
|
|
248
|
-
|
|
256
|
+
// 「有 agent 在跑」这类是「现在不行」,下个 tick 再试,不是坏了。
|
|
257
|
+
retryable: outcome.retryable === true,
|
|
249
258
|
command: outcome.command ?? null,
|
|
250
259
|
check: outcome.check ?? null,
|
|
251
260
|
}
|
package/lib/update-install.cjs
CHANGED
|
@@ -20,16 +20,34 @@
|
|
|
20
20
|
* 镜像/超时/Windows shim 这些边角;这里刻意**只做我们自己这条路上必须的那两三个开关**,
|
|
21
21
|
* 剩下的交给 profile 自己的配置。
|
|
22
22
|
*
|
|
23
|
+
* ## 用哪个包管理器:**宿主给的那个优先**
|
|
24
|
+
*
|
|
25
|
+
* 2026-10-11 真机踩到:这台机器 PATH 上的 pnpm 是 10.x(store v10),而 profile 是**宿主
|
|
26
|
+
* 自带的** pnpm 11.x(store v11)装的,拿 PATH 那个去装直接 `ERR_PNPM_UNEXPECTED_STORE`。
|
|
27
|
+
* 所以顺序是「宿主发布的调用方式 → PATH 上的 pnpm → npm」,见 `normalizeInvocation`。
|
|
28
|
+
*
|
|
23
29
|
* ## 三个开关,每个都有理由
|
|
24
30
|
*
|
|
25
31
|
* · `--config.auto-install-peers=false`:本插件的 peer 里有一串 `@deepseek-ai/*`
|
|
26
32
|
* (`cordis`、`dsh-tools`、`dsh-client-runtime`…),它们是 **DSH 运行时注入的、npm 上
|
|
27
33
|
* 根本没有**。pnpm 8 起 `auto-install-peers` 默认开,会去 npm 上拉这些 peer 然后 404。
|
|
28
34
|
* profile 里通常已经关掉了,但那是市场帮它写的 —— 换台机器不一定有,所以这里显式带上。
|
|
29
|
-
* · `--config.minimum-release-age=0
|
|
30
|
-
*
|
|
35
|
+
* · `--config.minimum-release-age=0`:**撞上 pnpm 的「新发布先晾一会儿」策略时才带**。
|
|
36
|
+
* 强制更新(`force`)一开始就带;平时不带,但**失败信息确实是这类策略时会带开关重试一次**
|
|
37
|
+
* —— 那道策略查的是**整份 lockfile**,别的插件刚发不久也会把整份锁文件判成「含未过期
|
|
38
|
+
* 条目」,于是我们自己的更新整整一天都做不了,而原因跟我们这个包毫无关系(2026-10-11
|
|
39
|
+
* 真机撞上的就是 `billion-context@0.1.192`)。市场那边对这种拒绝同样是带开关重试一次。
|
|
31
40
|
* · `CI=1`:没有终端可交互。包管理器在无人值守的子进程里问问题是没法回答的。
|
|
32
41
|
*
|
|
42
|
+
* ## 装之前先看有没有 agent 在跑(但「有没有新版」排在它前面)
|
|
43
|
+
*
|
|
44
|
+
* 装是**就地替换插件文件**,正在干活的对话可能读到半个包。市场那条路有一道这样的闸
|
|
45
|
+
* (`AGENTS_RUNNING`),这里也补上了 —— 判到有人跑就**不动盘**,如实报成「可再试」。
|
|
46
|
+
* `force`(面板上的「强制更新」)是用户明说「我知道,装吧」的那一挡,会跳过它。
|
|
47
|
+
*
|
|
48
|
+
* 顺序上**先答「有没有新版」**:没东西可装时拿一句「有对话在跑」挡在前面是误导,用户会以为
|
|
49
|
+
* 是那个原因,而真正的答案是「已经是最新」。
|
|
50
|
+
*
|
|
33
51
|
* ## 边界(照实说)
|
|
34
52
|
*
|
|
35
53
|
* · **只动 profile 的依赖树**(`pnpm add` 会顺手把 profile 的 `package.json` 里的依赖声明
|
|
@@ -138,7 +156,56 @@ function declaredSource(profileDir, name = DEFAULT_NAME) {
|
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
|
|
141
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* 宿主**发布出来**的包管理器调用方式(DSH 桌面版把它挂在 `profileContext.packageManager`
|
|
161
|
+
* 上,形状 `{command, args, env?}`)。
|
|
162
|
+
*
|
|
163
|
+
* 为什么非得用它、而不是 PATH 上那个 pnpm:桌面版由 GUI 拉起时 PATH 很干净,而且**版本
|
|
164
|
+
* 可能对不上** —— 实测这台机器上 PATH 的 pnpm 是 10.x(store v10),而 profile 是宿主自带的
|
|
165
|
+
* pnpm 11.x(store v11)装的,用它装会直接 `ERR_PNPM_UNEXPECTED_STORE`。市场那边也是
|
|
166
|
+
* 「宿主给的那个优先,找不到才退回 PATH 和 corepack」(`dsh-cli.js` 的 `probePnpm`)。
|
|
167
|
+
*
|
|
168
|
+
* 形状有一格不对就整个丢掉(半份调用等于仍然跑不起来的工具);`env` 只收字符串值。
|
|
169
|
+
*/
|
|
170
|
+
function normalizeInvocation(published) {
|
|
171
|
+
if (published === null || typeof published !== 'object' || Array.isArray(published)) return null;
|
|
172
|
+
const { command, args, env } = published;
|
|
173
|
+
if (typeof command !== 'string' || command.trim() === '') return null;
|
|
174
|
+
if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return null;
|
|
175
|
+
const merged = {};
|
|
176
|
+
for (const [key, value] of Object.entries(env ?? {})) {
|
|
177
|
+
if (typeof value === 'string') merged[key] = value;
|
|
178
|
+
}
|
|
179
|
+
return { command: command.trim(), args: [...args], env: merged };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 此刻正在跑的 agent(`status === 'running'` 的那些)。
|
|
184
|
+
*
|
|
185
|
+
* 判据与市场那份(`dshmarket` 的 `agents.js`)逐字一致:`agents.list()` 里 `status` 正好是
|
|
186
|
+
* `'running'` 的才算,列表读不到或抛错一律当「没有」—— 守卫宁可放过,也不能因为读不到状态
|
|
187
|
+
* 就把更新整个锁死。**不排除调用者自己**:这正是「有对话在跑就别换插件文件」的意思。
|
|
188
|
+
*/
|
|
189
|
+
function runningAgentIds(agents) {
|
|
190
|
+
if (agents === null || agents === undefined) return [];
|
|
191
|
+
let listed;
|
|
192
|
+
try {
|
|
193
|
+
listed = typeof agents.list === 'function' ? agents.list() : null;
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(listed)) return [];
|
|
198
|
+
const ids = [];
|
|
199
|
+
for (const agent of listed) {
|
|
200
|
+
if (agent === null || typeof agent !== 'object' || agent.status !== 'running') continue;
|
|
201
|
+
const id = typeof agent.id === 'string' && agent.id !== '' ? agent.id : 'agent';
|
|
202
|
+
if (!ids.includes(id)) ids.push(id);
|
|
203
|
+
}
|
|
204
|
+
return ids;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 跑一条命令,收 stdout+stderr 的尾巴。超时或非零退出码都如实带出来。 */
|
|
208
|
+
function run(command, args, { cwd, env, timeoutMs, spawnImpl = spawn }) {
|
|
142
209
|
return new Promise(resolve => {
|
|
143
210
|
let child;
|
|
144
211
|
try {
|
|
@@ -191,15 +258,16 @@ async function installLatest({
|
|
|
191
258
|
platform = process.platform,
|
|
192
259
|
// 去哪儿找包管理器。默认那组见 `toolSearchDirs`;注入只为测试。
|
|
193
260
|
dirs = null,
|
|
261
|
+
// 宿主发布的调用方式(`profileContext.packageManager`)。**优先用它**,理由见
|
|
262
|
+
// `normalizeInvocation` 的文件注释(版本对不上会把 profile 的 store 认错)。
|
|
263
|
+
packageManager = null,
|
|
194
264
|
onEvent = () => {},
|
|
195
265
|
} = {}) {
|
|
196
266
|
const beforeVersion = installedVersion(profileNodeModules, name);
|
|
197
267
|
const profileDir = path.dirname(profileNodeModules);
|
|
198
268
|
const searchDirs = dirs ?? toolSearchDirs({ platform, env });
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const manager = pnpm ? 'pnpm' : npm ? 'npm' : null;
|
|
202
|
-
const binary = pnpm || npm;
|
|
269
|
+
const hostInvocation = normalizeInvocation(packageManager);
|
|
270
|
+
const childEnv = spawnEnv(searchDirs, env);
|
|
203
271
|
|
|
204
272
|
const fail = (code, message) => ({
|
|
205
273
|
installed: false,
|
|
@@ -212,11 +280,36 @@ async function installLatest({
|
|
|
212
280
|
failure: { code, message },
|
|
213
281
|
});
|
|
214
282
|
|
|
215
|
-
|
|
283
|
+
/**
|
|
284
|
+
* 挑一个能用的包管理器:**宿主给的那个先试**(探一次 `--version`,跑不起来就当没有),
|
|
285
|
+
* 之后才是 PATH 上的 pnpm / npm。返回 `{command, prefix, manager, kind}`。
|
|
286
|
+
*/
|
|
287
|
+
async function pickManager() {
|
|
288
|
+
if (hostInvocation) {
|
|
289
|
+
const probe = await run(hostInvocation.command, [...hostInvocation.args, '--version'], {
|
|
290
|
+
cwd: profileDir,
|
|
291
|
+
env: { ...childEnv, ...hostInvocation.env },
|
|
292
|
+
timeoutMs: 20000,
|
|
293
|
+
});
|
|
294
|
+
if (probe.ok) {
|
|
295
|
+
return { command: hostInvocation.command, prefix: hostInvocation.args, manager: 'pnpm', kind: 'host', env: hostInvocation.env };
|
|
296
|
+
}
|
|
297
|
+
onEvent({ phase: 'host-manager-unusable', output: probe.output });
|
|
298
|
+
}
|
|
299
|
+
const pnpm = resolveExecutable('pnpm', searchDirs, { platform, env });
|
|
300
|
+
if (pnpm) return { command: pnpm, prefix: [], manager: 'pnpm', kind: 'path', env: {} };
|
|
301
|
+
const npm = resolveExecutable('npm', searchDirs, { platform, env });
|
|
302
|
+
if (npm) return { command: npm, prefix: [], manager: 'npm', kind: 'path', env: {} };
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const picked = await pickManager();
|
|
307
|
+
if (!picked) {
|
|
216
308
|
return fail(
|
|
217
309
|
'PACKAGE_MANAGER_MISSING',
|
|
218
|
-
'
|
|
219
|
-
'
|
|
310
|
+
'这台机器上找不到能用的包管理器(宿主没发布、PATH 上也没有 pnpm / npm),装不了新版。' +
|
|
311
|
+
'装一个 pnpm(或 npm)之后重试;插件不会自己去下载解包 —— 那等于把包管理器的活抄一遍,' +
|
|
312
|
+
'抄漏一处就是装出半个包。',
|
|
220
313
|
);
|
|
221
314
|
}
|
|
222
315
|
|
|
@@ -230,13 +323,39 @@ async function installLatest({
|
|
|
230
323
|
}
|
|
231
324
|
|
|
232
325
|
const spec = `${name}@${version}`;
|
|
233
|
-
const
|
|
234
|
-
|
|
326
|
+
const releaseAgeFlag = '--config.minimum-release-age=0';
|
|
327
|
+
const args = picked.manager === 'pnpm'
|
|
328
|
+
? ['add', spec, '--config.auto-install-peers=false', ...(force ? [releaseAgeFlag] : [])]
|
|
235
329
|
: ['install', spec, '--no-audit', '--no-fund'];
|
|
236
|
-
|
|
237
|
-
onEvent({ phase: 'install-started', command });
|
|
330
|
+
let command = [picked.manager, ...args].join(' ');
|
|
331
|
+
onEvent({ phase: 'install-started', command, manager: picked.kind });
|
|
238
332
|
|
|
239
|
-
const
|
|
333
|
+
const spawnAdd = extra => run(picked.command, [...picked.prefix, ...args, ...extra], {
|
|
334
|
+
cwd: profileDir,
|
|
335
|
+
env: { ...childEnv, ...picked.env },
|
|
336
|
+
timeoutMs,
|
|
337
|
+
});
|
|
338
|
+
let result = await spawnAdd([]);
|
|
339
|
+
let output = result.output;
|
|
340
|
+
/**
|
|
341
|
+
* 撞上 pnpm 的「新发布先晾一会儿」策略:**带上开关重试一次**。
|
|
342
|
+
*
|
|
343
|
+
* 为什么非重试不可:那道策略查的是**整份 lockfile**,不是我们要装的这个包。这台机器上
|
|
344
|
+
* 实测过一次 —— 另一个插件(`billion-context@0.1.192`)刚发不久,整份 lockfile 就被判
|
|
345
|
+
* 「含未过期的条目」而拒绝,于是我们自己的更新整整一天都做不了,而原因跟我们这个包毫无
|
|
346
|
+
* 关系。市场那边对这种拒绝也是带开关重试一次(`RELEASE_AGE_OVERRIDE`)。
|
|
347
|
+
*
|
|
348
|
+
* 只在**第一次没带开关**、且失败信息确实是这类策略时才重试:别的失败(网络、权限、盘满)
|
|
349
|
+
* 重试一次只会把同一条错误再说一遍,还多花一次等待。
|
|
350
|
+
*/
|
|
351
|
+
const releaseAgeLike = /MINIMUM_RELEASE_AGE|minimumReleaseAge|minimum-release-age|release age/i;
|
|
352
|
+
const retried = !result.ok && picked.manager === 'pnpm' && !force && releaseAgeLike.test(output);
|
|
353
|
+
if (retried) {
|
|
354
|
+
onEvent({ phase: 'release-age-retry' });
|
|
355
|
+
result = await spawnAdd([releaseAgeFlag]);
|
|
356
|
+
command = [picked.manager, ...args, releaseAgeFlag].join(' ');
|
|
357
|
+
output = `${output}\n--- 撞上 pnpm 的发布时长策略,带上 ${releaseAgeFlag} 重试一次 ---\n${result.output}`;
|
|
358
|
+
}
|
|
240
359
|
const after = installedVersion(profileNodeModules, name);
|
|
241
360
|
if (!result.ok) {
|
|
242
361
|
return {
|
|
@@ -246,7 +365,9 @@ async function installLatest({
|
|
|
246
365
|
installedVersion: after,
|
|
247
366
|
source: declaredSource(profileDir, name),
|
|
248
367
|
command,
|
|
249
|
-
|
|
368
|
+
manager: picked.kind,
|
|
369
|
+
retried,
|
|
370
|
+
output,
|
|
250
371
|
// 超时与「真的失败了」分开说:前者可以再试,后者多半要看输出。
|
|
251
372
|
failure: {
|
|
252
373
|
code: result.code === null ? 'INSTALL_ABORTED' : 'INSTALL_FAILED',
|
|
@@ -263,7 +384,9 @@ async function installLatest({
|
|
|
263
384
|
installedVersion: after,
|
|
264
385
|
source: declaredSource(profileDir, name),
|
|
265
386
|
command,
|
|
266
|
-
|
|
387
|
+
manager: picked.kind,
|
|
388
|
+
retried,
|
|
389
|
+
output,
|
|
267
390
|
failure: changed
|
|
268
391
|
? null
|
|
269
392
|
: { code: 'VERSION_UNCHANGED', message: `${command} 跑完了,但盘上还是 ${after ?? '读不到版本'},这一次不算数` },
|
|
@@ -271,16 +394,27 @@ async function installLatest({
|
|
|
271
394
|
}
|
|
272
395
|
|
|
273
396
|
/**
|
|
274
|
-
*
|
|
397
|
+
* 「让我变成最新」:先看有没有人在干活,再查一次,最后决定装不装。
|
|
275
398
|
*
|
|
276
399
|
* 面板那颗按钮**与外壳的自动更新**都打这个入口 —— 两处走同一套判定,免得出现「手动说有
|
|
277
400
|
* 新版、自动说没有」这种最难查的偏差。已经是最新(`force` 为假)就什么都不做、如实报
|
|
278
401
|
* `skipped`。
|
|
402
|
+
*
|
|
403
|
+
* ## 为什么装之前先看有没有 agent 在跑
|
|
404
|
+
*
|
|
405
|
+
* 装会**就地换掉插件文件**(profile 里那一份),而正在干活的对话可能正好在装配/读取插件
|
|
406
|
+
* 目录 —— 换到一半就是「读到半个包」。市场那条路也有同一道闸(`AGENTS_RUNNING`),这条是
|
|
407
|
+
* 把它搬到插件自己这条路上来。判到有人跑就**不动盘**,如实报成「可再试」,下个 tick 再来。
|
|
408
|
+
*
|
|
409
|
+
* `force` 是**用户明说「我知道,装吧」**的那一挡(面板上那颗「强制更新」):它同时跳过
|
|
410
|
+
* 这道闸和「新发布先晾一会儿」的门槛。自动更新永远不带 force。
|
|
279
411
|
*/
|
|
280
412
|
async function installLatestIfNeeded(options = {}) {
|
|
281
|
-
const { profileNodeModules, force = false, name = DEFAULT_NAME } = options;
|
|
413
|
+
const { profileNodeModules, force = false, name = DEFAULT_NAME, agents = null } = options;
|
|
282
414
|
const beforeVersion = installedVersion(profileNodeModules, name);
|
|
283
415
|
const check = await checkForUpdate({ installedVersion: beforeVersion, source: declaredSource(path.dirname(profileNodeModules), name) });
|
|
416
|
+
// 「有没有新版」排在「有没有人在跑」**前面**:没东西可装的时候,拿一句「有对话在跑」挡住
|
|
417
|
+
// 真正的答案(已经是最新)是误导 —— 2026-10-11 真机上就是这么被绊了一下。
|
|
284
418
|
if (!force && !check.updateAvailable) {
|
|
285
419
|
return {
|
|
286
420
|
installed: false,
|
|
@@ -294,6 +428,30 @@ async function installLatestIfNeeded(options = {}) {
|
|
|
294
428
|
check,
|
|
295
429
|
};
|
|
296
430
|
}
|
|
431
|
+
if (!force) {
|
|
432
|
+
const running = runningAgentIds(agents);
|
|
433
|
+
if (running.length > 0) {
|
|
434
|
+
return {
|
|
435
|
+
installed: false,
|
|
436
|
+
skipped: false,
|
|
437
|
+
beforeVersion,
|
|
438
|
+
installedVersion: beforeVersion,
|
|
439
|
+
source: check.source,
|
|
440
|
+
command: null,
|
|
441
|
+
output: '',
|
|
442
|
+
runningAgents: running,
|
|
443
|
+
retryable: true,
|
|
444
|
+
check,
|
|
445
|
+
failure: {
|
|
446
|
+
code: 'AGENTS_RUNNING',
|
|
447
|
+
message:
|
|
448
|
+
`有 ${running.length} 个 agent 正在运行(${running.slice(0, 4).join(', ')})。` +
|
|
449
|
+
'装新版会就地替换插件文件,正在干活的对话可能读到半个包;等它完成(或先取消)再更新。' +
|
|
450
|
+
'确定要现在装的话,点面板上的「强制更新」。',
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
297
455
|
const result = await installLatest({ ...options, version: check.latestVersion });
|
|
298
456
|
return { ...result, check };
|
|
299
457
|
}
|
|
@@ -305,6 +463,8 @@ module.exports = {
|
|
|
305
463
|
declaredSource,
|
|
306
464
|
toolSearchDirs,
|
|
307
465
|
resolveExecutable,
|
|
466
|
+
normalizeInvocation,
|
|
467
|
+
runningAgentIds,
|
|
308
468
|
DEFAULT_NAME,
|
|
309
469
|
INSTALL_TIMEOUT_MS,
|
|
310
470
|
OUTPUT_TAIL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (0.2.0-rc.2 compatible, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|