dsh-mobile-plugin 0.0.0-stage → 0.1.58

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 (87) hide show
  1. package/README.md +159 -2
  2. package/config/agent-presets/standard/agent.cordis.yml +170 -0
  3. package/config/agent-presets/standard/preset.yml +3 -0
  4. package/cordis.mobile-host.yml +48 -0
  5. package/cordis.patch.yml +21 -0
  6. package/dsh-mobile-credentials/lib/credentials.js +135 -0
  7. package/dsh-mobile-credentials/package.json +13 -0
  8. package/host.js +128 -0
  9. package/lib/auto-archive.cjs +98 -0
  10. package/lib/client.js +690 -0
  11. package/lib/desktop-half-stats.cjs +101 -0
  12. package/lib/desktop-view.cjs +109 -0
  13. package/lib/host-guard.cjs +230 -0
  14. package/lib/host-patches.cjs +46 -0
  15. package/lib/http/package.json +3 -0
  16. package/lib/http/routing.js +84 -0
  17. package/lib/impl.js +406 -0
  18. package/lib/mirror/pusher.cjs +706 -0
  19. package/lib/mirror-snapshot.cjs +39 -0
  20. package/lib/mobile/agent-presets.js +182 -0
  21. package/lib/mobile/attachments.js +116 -0
  22. package/lib/mobile/auth.js +78 -0
  23. package/lib/mobile/completion.js +114 -0
  24. package/lib/mobile/credentials.js +150 -0
  25. package/lib/mobile/directory-browse.js +295 -0
  26. package/lib/mobile/envelope.js +51 -0
  27. package/lib/mobile/events.js +293 -0
  28. package/lib/mobile/history-derive.js +608 -0
  29. package/lib/mobile/host-bridge/endpoints.js +189 -0
  30. package/lib/mobile/host-bridge/event-synthesis.js +232 -0
  31. package/lib/mobile/host-bridge/shims.js +218 -0
  32. package/lib/mobile/host-bridge/transport.js +168 -0
  33. package/lib/mobile/host-bridge.js +71 -0
  34. package/lib/mobile/host-settings.js +282 -0
  35. package/lib/mobile/idempotency.js +78 -0
  36. package/lib/mobile/jobs.js +97 -0
  37. package/lib/mobile/local-images.js +153 -0
  38. package/lib/mobile/package.json +3 -0
  39. package/lib/mobile/plugins.js +149 -0
  40. package/lib/mobile/questions.js +71 -0
  41. package/lib/mobile/references.js +209 -0
  42. package/lib/mobile/rpc.js +76 -0
  43. package/lib/mobile/session-cache.js +263 -0
  44. package/lib/mobile/session-goal.js +44 -0
  45. package/lib/mobile/session-log.js +94 -0
  46. package/lib/mobile/session-mirror.js +177 -0
  47. package/lib/mobile/session-models.js +153 -0
  48. package/lib/mobile/session-presets.js +52 -0
  49. package/lib/mobile/session-search.js +65 -0
  50. package/lib/mobile/session-send.js +278 -0
  51. package/lib/mobile/session-snapshot.js +142 -0
  52. package/lib/mobile/sessions.js +462 -0
  53. package/lib/mobile/skills.js +80 -0
  54. package/lib/mobile/stream.js +268 -0
  55. package/lib/mobile/subagents.js +265 -0
  56. package/lib/mobile/workspaces.js +214 -0
  57. package/lib/panel/index.html +33 -0
  58. package/lib/read-receipt.cjs +124 -0
  59. package/lib/reconnect-scheduler.cjs +37 -0
  60. package/lib/relay/core.js +77 -0
  61. package/lib/relay/lan-network.cjs +85 -0
  62. package/lib/relay/lan-proxy.js +593 -0
  63. package/lib/relay/package.json +3 -0
  64. package/lib/relay/pairing-policy.js +45 -0
  65. package/lib/relay/port-allocator.cjs +60 -0
  66. package/lib/relay/preview.js +374 -0
  67. package/lib/relay/protocol.js +177 -0
  68. package/lib/relay/status.js +145 -0
  69. package/lib/relay/tunnel.js +721 -0
  70. package/lib/routes/control-plane.js +377 -0
  71. package/lib/routes/mobile-api.js +208 -0
  72. package/lib/routes/package.json +3 -0
  73. package/lib/session-cleanup.cjs +153 -0
  74. package/lib/shared/credentials-entry.js +48 -0
  75. package/lib/shared/dshtoken.js +62 -0
  76. package/lib/shared/fresh-request.js +35 -0
  77. package/lib/shared/http-util.js +71 -0
  78. package/lib/shared/list-revision.js +51 -0
  79. package/lib/shared/list-signal.js +87 -0
  80. package/lib/shared/package.json +3 -0
  81. package/lib/shared/paths.js +27 -0
  82. package/lib/shared/session-signal.js +110 -0
  83. package/lib/shared/workspace-guard.js +127 -0
  84. package/lib/sidecar-facade-worker.cjs +353 -0
  85. package/lib/sidecar-facade.cjs +141 -0
  86. package/lib/sidecar-lifecycle.cjs +246 -0
  87. package/package.json +74 -4
package/host.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Headless Cordis Host for the DSH mobile app.
4
+ *
5
+ * The repository used to ship an Electron desktop shell that owned the Host
6
+ * process; the DSH mobile app never used that surface. This entry replaces it:
7
+ * it boots the same upstream Cordis Host, composes the shipped DSH Web
8
+ * bundle, and adds one plugin — the mobile API at `/api/mobile/v1`.
9
+ *
10
+ * The Host itself is upstream. `@deepseek-ai/dsh-app-boot#boot` mounts a
11
+ * `cordis.yml` entry list, so the root list stays empty and every plugin is
12
+ * contributed by a patch layer: the profile's bundle layers (dsh-base then
13
+ * dsh-web-app), the profile's own `cordis.patch.yml`, and finally this
14
+ * package's `cordis.patch.yml`.
15
+ */
16
+
17
+ import { writeFileSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
21
+ import { installProcessGuard } from './lib/host-guard.cjs'
22
+ import { dropBareNameSelfMounts } from './lib/host-patches.cjs'
23
+ import {
24
+ assertEntriesActivated,
25
+ boot,
26
+ initProfile,
27
+ installFailLoud,
28
+ loadLayeredEnv,
29
+ loadOverlayPatches,
30
+ loadProfile,
31
+ PROFILE_TEMPLATES,
32
+ resolveProfileDir,
33
+ } from '@deepseek-ai/dsh-app-boot'
34
+
35
+ const BIN_NAME = 'dsh-mobile-plugin'
36
+ const PROFILE_NAME = process.env.DSH_MOBILE_PROFILE || 'mobile'
37
+ const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url))
38
+ // The bundle layer fallback resolves plugins out of the installing package's
39
+ // dependency closure, so the anchor must be this package's own manifest.
40
+ const INSTALL_ANCHOR = join(PACKAGE_DIR, 'package.json')
41
+
42
+ // The process-level error boundary goes up **before** anything else can throw or
43
+ // reject: Node >= 15 turns an unhandled rejection into a throw, so a single
44
+ // detached promise during boot (or later, from any background task) would
45
+ // otherwise take the whole DSH down with it. The host is long-lived and the
46
+ // desktop shell has no plugin isolation, so "one plugin bug" and "DSH crashed"
47
+ // are the same event unless something catches it here. The guard records each
48
+ // fault, keeps the process alive, and only exits on a genuine crash loop so
49
+ // `Restart=on-failure` can pull up a clean one.
50
+ const { guard: hostGuard } = installProcessGuard()
51
+
52
+ loadLayeredEnv(BIN_NAME)
53
+ const releaseFailLoud = installFailLoud(BIN_NAME)
54
+
55
+ try {
56
+ const home = process.env.DSH_HOME
57
+ const profileDir = resolveProfileDir(PROFILE_NAME, home)
58
+ // Idempotent: an already-initialized profile keeps its own patch layer.
59
+ initProfile(profileDir, PROFILE_TEMPLATES.web)
60
+ const profile = loadProfile(BIN_NAME, PROFILE_NAME, INSTALL_ANCHOR, home, { userLayer: true })
61
+
62
+ // An empty entry list is the composition root: upstream fills the tree from
63
+ // the profile's bundle layers; every plugin arrives via a patch layer.
64
+ const rootConfig = join(profileDir, 'cordis.root.yml')
65
+ writeFileSync(rootConfig, '[]\n')
66
+
67
+ const patches = [
68
+ ...profile.layers.flatMap(layer => layer.patches),
69
+ ...profile.patches,
70
+ ...dropBareNameSelfMounts(loadOverlayPatches(BIN_NAME, join(PACKAGE_DIR, 'cordis.patch.yml')), BIN_NAME),
71
+ // 宿主专属的层(不开浏览器、目录选择器钉成 browse)。刻意不放进
72
+ // cordis.patch.yml:那一层桌面 GUI 也会加载,而桌面操作者就坐在机器前,
73
+ // 不需要把原生目录选择器换成远程用的 browse 交互。
74
+ ...loadOverlayPatches(BIN_NAME, join(PACKAGE_DIR, 'cordis.mobile-host.yml')),
75
+ // The mobile API is this very package. The Loader imports an entry by bare
76
+ // name from its own location, and `dsh-mobile-plugin` is not installed
77
+ // there, so the row names the plugin's entry module by absolute path.
78
+ { insert: [{ id: 'mobile-api', name: join(PACKAGE_DIR, 'lib/impl.js'), config: { enabled: true } }] },
79
+ // The DSH Web bundle disables every agent-plane row (shell, files, search,
80
+ // skills, goals, planning, compaction, delegation) and lets each session
81
+ // mount a preset instead — but it leaves the roster's shipped root to
82
+ // `apps/cli`'s `composeProfile`, which a headless host never runs. Point
83
+ // the roster at this package's own `config/agent-presets` so `standard`
84
+ // resolves; without this the roster is composed but empty and every
85
+ // `session/new` fails with `agent-preset-not-found`, while disabling the
86
+ // roster instead would leave the agent with no tools at all.
87
+ {
88
+ id: 'agent-presets',
89
+ config: {
90
+ default: 'standard',
91
+ roots: [{ path: join(PACKAGE_DIR, 'config', 'agent-presets'), trust: 'system' }],
92
+ },
93
+ },
94
+ ]
95
+
96
+ const ctx = await boot(BIN_NAME, rootConfig, patches, (hostCtx) => {
97
+ // A headless mobile host has no command line: nothing passes flags and
98
+ // nobody is watching a help screen. The web startup plugin still waits on
99
+ // `cmdlineArgs` before it will provide `webStartup`, so the service must
100
+ // exist with an empty argument list or the tree deadlocks.
101
+ provideCmdline(hostCtx, {
102
+ args: [],
103
+ exit: (code) => {
104
+ process.exitCode = code
105
+ },
106
+ })
107
+ })
108
+ await assertEntriesActivated(ctx, BIN_NAME)
109
+ // 装载期结束,撤掉 installFailLoud 的兜底。
110
+ //
111
+ // 它自己的语义是对的:一个插件**装载**失败不该让宿主半死不活地上线,所以
112
+ // 任何 unhandledRejection 都直接 exit(1)。但它注册在 process 上之后**一辈子
113
+ // 不摘**(releaseFailLoud 在成功路径上从来没被调用过),而宿主是长驻进程——
114
+ // 于是运行期任意一次游离 reject 都会走到它那里把进程带走。上一版提交
115
+ // (ca60417)在真机上被这条打脸:账本记下了「未处理的 Promise 拒绝(已忽略)」,
116
+ // 紧接着一行 `fatal load failure` 就把 PID 换掉了。
117
+ //
118
+ // 这里在 assertEntriesActivated 通过之后才撤:那一刻树已经确认全部激活
119
+ //(激活失败的 entry 会在它内部 throw,进上面的 catch 走 boot failed),
120
+ // 此后再出现的 reject 就不再是「装载失败」而是运行期故障,
121
+ // 交给进程级错误边界记账 + 熔断。
122
+ releaseFailLoud()
123
+ process.stderr.write(`${BIN_NAME}: mobile host ready (profile ${PROFILE_NAME})\n`)
124
+ } catch (error) {
125
+ process.stderr.write(`${BIN_NAME}: boot failed: ${error instanceof Error ? error.stack : String(error)}\n`)
126
+ await releaseFailLoud()
127
+ process.exitCode = 1
128
+ }
@@ -0,0 +1,98 @@
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
+ }