dsh-mobile-plugin 0.4.1 → 0.4.4

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.
Files changed (41) hide show
  1. package/README.md +71 -74
  2. package/dsh-mobile-credentials/lib/credentials.js +39 -16
  3. package/lib/impl.js +0 -4
  4. package/lib/index.js +1 -1
  5. package/lib/mobile/allowed-zone.js +100 -0
  6. package/lib/mobile/archive-clean.js +4 -3
  7. package/lib/mobile/attachments.js +5 -1
  8. package/lib/mobile/file-read.js +168 -0
  9. package/lib/mobile/history-derive.js +67 -98
  10. package/lib/mobile/local-images.js +38 -7
  11. package/lib/mobile/model-vendors.js +105 -0
  12. package/lib/mobile/plugin-market.js +475 -0
  13. package/lib/mobile/plugin-self.js +1 -1
  14. package/lib/mobile/plugin-update.js +1 -1
  15. package/lib/mobile/relay-control.js +131 -7
  16. package/lib/mobile/session-cwd.js +28 -0
  17. package/lib/mobile/session-file.js +1 -12
  18. package/lib/mobile/stats-collect.cjs +146 -15
  19. package/lib/mobile/stats-ledger.cjs +249 -59
  20. package/lib/mobile/stats-scheduler.cjs +41 -0
  21. package/lib/mobile/stream.js +30 -110
  22. package/lib/mobile/uploads.js +68 -5
  23. package/lib/mobile/vendor-logos.cjs +30 -0
  24. package/lib/mobile/wire-cut.js +212 -0
  25. package/lib/panel/index.html +2 -2
  26. package/lib/relay/apply-ledger.cjs +52 -0
  27. package/lib/relay/core.js +14 -3
  28. package/lib/relay/migrate.js +90 -0
  29. package/lib/relay/protocol.js +4 -0
  30. package/lib/relay/verify.js +52 -0
  31. package/lib/routes/control-plane.js +12 -3
  32. package/lib/routes/mobile-api.js +28 -2
  33. package/lib/session-cleanup.cjs +10 -0
  34. package/lib/shared/device-headers.js +12 -4
  35. package/lib/shell/updater.js +2 -2
  36. package/lib/sidecar-facade-worker.cjs +55 -8
  37. package/lib/sidecar-facade.cjs +11 -1
  38. package/lib/sidecar-lifecycle.cjs +14 -1
  39. package/lib/update-install.cjs +13 -6
  40. package/package.json +1 -1
  41. package/lib/auto-archive.cjs +0 -98
@@ -13,9 +13,10 @@
13
13
  * 别的插件刚发也会连累我们)。
14
14
  *
15
15
  * 包管理器优先用宿主发布的那个(profile 是宿主自带 pnpm 装的,PATH 上版本不同会
16
- * ERR_PNPM_UNEXPECTED_STORE),找不到再退回 PATH。装之前先看有没有 agent 在跑;force 跳过
17
- * 这道闸并跳过发布晾置期,auto 只跳过这道闸。顺序上先答「有没有新版」,没东西可装时不该拿
18
- * 「有对话在跑」挡在前面。
16
+ * ERR_PNPM_UNEXPECTED_STORE),找不到再退回 PATH。装之前先看有没有 agent 在跑;force 与 auto
17
+ * 都跳过这道闸、也都不等发布晾置期(2026-10-26 用户要求自动更新一检测到新版就立刻装,与面板
18
+ * 上那颗「强制更新」同一个口径),两者的区别只剩「没新版时装不装」。顺序上先答「有没有新版」,
19
+ * 没东西可装时不该拿「有对话在跑」挡在前面。
19
20
  *
20
21
  * 只动 profile 的依赖树;找不到包管理器就如实失败;装完以盘上版本号为准,退出码 0 不算装上。
21
22
  */
@@ -243,6 +244,7 @@ function run(command, args, { cwd, env, timeoutMs, spawnImpl = spawn }) {
243
244
  * @param {string} options.profileNodeModules profile 的 `node_modules` 目录
244
245
  * @param {string} options.version 要装的版本(注册表给的)
245
246
  * @param {boolean} [options.force] true = 跳过「新发布先晾一会儿」的门槛
247
+ * @param {boolean} [options.auto] 自动更新那一轮:同样跳过发布晾置期(见文件头)
246
248
  * @param {() => any} [options.beforeInstall] 真正跑包管理器**之前**调一次(外壳用它把正在跑的
247
249
  * 那一代复制进回退目录)。包管理器一跑就把旧的 store 目录清掉了,晚一步没得复制。
248
250
  * @returns {Promise<{installed, skipped, beforeVersion, installedVersion, source, command, output, failure}>}
@@ -252,6 +254,7 @@ async function installLatest({
252
254
  version,
253
255
  name = DEFAULT_NAME,
254
256
  force = false,
257
+ auto = false,
255
258
  timeoutMs = INSTALL_TIMEOUT_MS,
256
259
  env = process.env,
257
260
  platform = process.platform,
@@ -331,8 +334,11 @@ async function installLatest({
331
334
 
332
335
  const spec = `${name}@${version}`;
333
336
  const releaseAgeFlag = '--config.minimum-release-age=0';
337
+ // `auto` 与 `force` 一样一开始就带上开关:自动更新要「一检测到就装」,不该先被那道策略挡回来
338
+ // 再重试一遍(多等一轮、日志里多一条失败)。
339
+ const skipReleaseAge = force || auto;
334
340
  const args = picked.manager === 'pnpm'
335
- ? ['add', spec, '--config.auto-install-peers=false', ...(force ? [releaseAgeFlag] : [])]
341
+ ? ['add', spec, '--config.auto-install-peers=false', ...(skipReleaseAge ? [releaseAgeFlag] : [])]
336
342
  : ['install', spec, '--no-audit', '--no-fund'];
337
343
  let command = [picked.manager, ...args].join(' ');
338
344
  onEvent({ phase: 'install-started', command, manager: picked.kind });
@@ -356,7 +362,7 @@ async function installLatest({
356
362
  * 重试一次只会把同一条错误再说一遍,还多花一次等待。
357
363
  */
358
364
  const releaseAgeLike = /MINIMUM_RELEASE_AGE|minimumReleaseAge|minimum-release-age|release age/i;
359
- const retried = !result.ok && picked.manager === 'pnpm' && !force && releaseAgeLike.test(output);
365
+ const retried = !result.ok && picked.manager === 'pnpm' && !skipReleaseAge && releaseAgeLike.test(output);
360
366
  if (retried) {
361
367
  onEvent({ phase: 'release-age-retry' });
362
368
  result = await spawnAdd([releaseAgeFlag]);
@@ -422,7 +428,8 @@ async function installLatest({
422
428
  *
423
429
  * `force` 是**用户明说「我知道,装吧」**的那一挡(面板上那颗「强制更新」):它同时跳过
424
430
  * 这道闸和「新发布先晾一会儿」的门槛,而且不管有没有新版都装一遍。`auto`(宿主那一轮自动
425
- * 更新)只跳过这道闸,其余照旧 —— 见文件头。
431
+ * 更新)跳过同样的两道闸,只是仍然「没新版就不动盘」—— 2026-10-26 用户要求自动更新一检测到
432
+ * 新版就立刻装,所以它不再等发布晾置期,见文件头。
426
433
  */
427
434
  async function installLatestIfNeeded(options = {}) {
428
435
  const { profileNodeModules, force = false, auto = false, name = DEFAULT_NAME, agents = null } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -1,98 +0,0 @@
1
- 'use strict'
2
-
3
- const { unwrap } = require('./mobile/envelope.js')
4
-
5
- const ARCHIVE_AFTER_MS = 24 * 60 * 60 * 1000
6
- const SCAN_INTERVAL_MS = 12 * 60 * 60 * 1000
7
-
8
- function projectCounts(items) {
9
- const counts = new Map()
10
- for (const item of items) {
11
- const cwd = typeof item?.cwd === 'string' ? item.cwd.trim() : ''
12
- if (cwd) counts.set(cwd, (counts.get(cwd) || 0) + 1)
13
- }
14
- return counts
15
- }
16
-
17
- function archiveCandidates(items, now = Date.now()) {
18
- const counts = projectCounts(items)
19
- const cutoff = now - ARCHIVE_AFTER_MS
20
- return items.filter(item => {
21
- const cwd = typeof item?.cwd === 'string' ? item.cwd.trim() : ''
22
- if (!cwd || (counts.get(cwd) || 0) <= 10) return false
23
- if (item?.blank || item?.running || item?.sessionId === undefined) return false
24
- return Number.isFinite(item.updatedAt) && item.updatedAt < cutoff
25
- })
26
- }
27
-
28
- function loggerOf(ctx) {
29
- return ctx?.logger && typeof ctx.logger === 'object' ? ctx.logger : undefined
30
- }
31
-
32
- function logFailure(ctx, sessionId, error) {
33
- const logger = loggerOf(ctx)
34
- const message = `automatic session archive failed for ${sessionId}: ${String(error?.message || error)}`
35
- if (typeof logger?.warn === 'function') logger.warn(message)
36
- else if (typeof logger?.error === 'function') logger.error(message)
37
- }
38
-
39
- async function scanAndArchive(ctx, now = Date.now()) {
40
- const api = ctx?.apiProxy
41
- const list = api?.sessions?.list
42
- const archive = api?.workspace?.archiveSession
43
- if (typeof list !== 'function' || typeof archive !== 'function') return { scanned: 0, archived: 0, failed: 0 }
44
-
45
- const response = await list({ rpcId: 'dsh-mobile-plugin-auto-archive', method: 'session.list', payload: {} })
46
- // 回执是 { result: { ok, value } } 信封:早先这里直接读 response.value,
47
- // 而 list() 的回执根本不在那一层——这个定时任务从来没真正扫到过会话。
48
- const listed = unwrap(response)
49
- const items = (listed.value && listed.value.items) || []
50
- const candidates = archiveCandidates(items, now)
51
- let archived = 0
52
- let failed = 0
53
- for (const item of candidates) {
54
- try {
55
- const result = await archive({
56
- rpcId: 'dsh-mobile-plugin-auto-archive',
57
- method: 'workspace.archiveSession',
58
- payload: { sessionId: item.sessionId },
59
- })
60
- if (result?.ok === false) throw new Error(result.error?.message || 'archive rejected')
61
- archived += 1
62
- } catch (error) {
63
- failed += 1
64
- logFailure(ctx, item.sessionId, error)
65
- }
66
- }
67
- return { scanned: items.length, archived, failed }
68
- }
69
-
70
- function installAutoArchive(ctx) {
71
- let disposed = false
72
- let inFlight
73
- const scan = () => {
74
- if (disposed || inFlight) return inFlight
75
- inFlight = Promise.resolve().then(() => scanAndArchive(ctx)).catch(error => {
76
- logFailure(ctx, 'scan', error)
77
- return { scanned: 0, archived: 0, failed: 1 }
78
- }).finally(() => { inFlight = undefined })
79
- return inFlight
80
- }
81
-
82
- void scan()
83
- const timer = setInterval(() => { void scan() }, SCAN_INTERVAL_MS)
84
- return () => {
85
- if (disposed) return
86
- disposed = true
87
- clearInterval(timer)
88
- }
89
- }
90
-
91
- module.exports = {
92
- ARCHIVE_AFTER_MS,
93
- SCAN_INTERVAL_MS,
94
- archiveCandidates,
95
- installAutoArchive,
96
- projectCounts,
97
- scanAndArchive,
98
- }