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.
- package/README.md +159 -2
- package/config/agent-presets/standard/agent.cordis.yml +170 -0
- package/config/agent-presets/standard/preset.yml +3 -0
- package/cordis.mobile-host.yml +48 -0
- package/cordis.patch.yml +21 -0
- package/dsh-mobile-credentials/lib/credentials.js +135 -0
- package/dsh-mobile-credentials/package.json +13 -0
- package/host.js +128 -0
- package/lib/auto-archive.cjs +98 -0
- package/lib/client.js +690 -0
- package/lib/desktop-half-stats.cjs +101 -0
- package/lib/desktop-view.cjs +109 -0
- package/lib/host-guard.cjs +230 -0
- package/lib/host-patches.cjs +46 -0
- package/lib/http/package.json +3 -0
- package/lib/http/routing.js +84 -0
- package/lib/impl.js +406 -0
- package/lib/mirror/pusher.cjs +706 -0
- package/lib/mirror-snapshot.cjs +39 -0
- package/lib/mobile/agent-presets.js +182 -0
- package/lib/mobile/attachments.js +116 -0
- package/lib/mobile/auth.js +78 -0
- package/lib/mobile/completion.js +114 -0
- package/lib/mobile/credentials.js +150 -0
- package/lib/mobile/directory-browse.js +295 -0
- package/lib/mobile/envelope.js +51 -0
- package/lib/mobile/events.js +293 -0
- package/lib/mobile/history-derive.js +608 -0
- package/lib/mobile/host-bridge/endpoints.js +189 -0
- package/lib/mobile/host-bridge/event-synthesis.js +232 -0
- package/lib/mobile/host-bridge/shims.js +218 -0
- package/lib/mobile/host-bridge/transport.js +168 -0
- package/lib/mobile/host-bridge.js +71 -0
- package/lib/mobile/host-settings.js +282 -0
- package/lib/mobile/idempotency.js +78 -0
- package/lib/mobile/jobs.js +97 -0
- package/lib/mobile/local-images.js +153 -0
- package/lib/mobile/package.json +3 -0
- package/lib/mobile/plugins.js +149 -0
- package/lib/mobile/questions.js +71 -0
- package/lib/mobile/references.js +209 -0
- package/lib/mobile/rpc.js +76 -0
- package/lib/mobile/session-cache.js +263 -0
- package/lib/mobile/session-goal.js +44 -0
- package/lib/mobile/session-log.js +94 -0
- package/lib/mobile/session-mirror.js +177 -0
- package/lib/mobile/session-models.js +153 -0
- package/lib/mobile/session-presets.js +52 -0
- package/lib/mobile/session-search.js +65 -0
- package/lib/mobile/session-send.js +278 -0
- package/lib/mobile/session-snapshot.js +142 -0
- package/lib/mobile/sessions.js +462 -0
- package/lib/mobile/skills.js +80 -0
- package/lib/mobile/stream.js +268 -0
- package/lib/mobile/subagents.js +265 -0
- package/lib/mobile/workspaces.js +214 -0
- package/lib/panel/index.html +33 -0
- package/lib/read-receipt.cjs +124 -0
- package/lib/reconnect-scheduler.cjs +37 -0
- package/lib/relay/core.js +77 -0
- package/lib/relay/lan-network.cjs +85 -0
- package/lib/relay/lan-proxy.js +593 -0
- package/lib/relay/package.json +3 -0
- package/lib/relay/pairing-policy.js +45 -0
- package/lib/relay/port-allocator.cjs +60 -0
- package/lib/relay/preview.js +374 -0
- package/lib/relay/protocol.js +177 -0
- package/lib/relay/status.js +145 -0
- package/lib/relay/tunnel.js +721 -0
- package/lib/routes/control-plane.js +377 -0
- package/lib/routes/mobile-api.js +208 -0
- package/lib/routes/package.json +3 -0
- package/lib/session-cleanup.cjs +153 -0
- package/lib/shared/credentials-entry.js +48 -0
- package/lib/shared/dshtoken.js +62 -0
- package/lib/shared/fresh-request.js +35 -0
- package/lib/shared/http-util.js +71 -0
- package/lib/shared/list-revision.js +51 -0
- package/lib/shared/list-signal.js +87 -0
- package/lib/shared/package.json +3 -0
- package/lib/shared/paths.js +27 -0
- package/lib/shared/session-signal.js +110 -0
- package/lib/shared/workspace-guard.js +127 -0
- package/lib/sidecar-facade-worker.cjs +353 -0
- package/lib/sidecar-facade.cjs +141 -0
- package/lib/sidecar-lifecycle.cjs +246 -0
- 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
|
+
}
|