dsh-mobile-plugin 0.1.69 → 0.1.70
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 +15 -3
- package/host.js +44 -5
- package/lib/mobile/host-bridge/event-synthesis.js +30 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ Electron 运行时**,`node host.js` 即可独立运行。
|
|
|
14
14
|
|
|
15
15
|
本仓库是插件的**公开发布仓库**,仓库根就是包根:别人从这里读代码,也从这里核对这份代码的说法。
|
|
16
16
|
|
|
17
|
+
隐私政策(App Store / Google Play 上架用的那份,描述手机 App 会经手哪些数据):[PRIVACY.md](PRIVACY.md)。
|
|
18
|
+
技术支持 / 常见问题(上架时填的「技术支持网址」):[SUPPORT.md](SUPPORT.md)。
|
|
19
|
+
|
|
17
20
|
开发和 App 客户端、中转服务器放在一起进行(一个私有 monorepo),所以这里的提交历史从拆分那天开始,
|
|
18
21
|
不是项目历史 —— 每次发版,把同一次改动一起推到这里。
|
|
19
22
|
|
|
@@ -83,9 +86,18 @@ DSH_HOME=/opt/dsh-mobile/.dsh DSH_MOBILE_PROFILE=mobile node host.js
|
|
|
83
86
|
|
|
84
87
|
本包有两条完全不同的安装路径,别混用。
|
|
85
88
|
|
|
86
|
-
**服务器 /
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
**服务器 / 无头部署:启动器 rsync + 插件本体走 npm。**(2026-10-11 改)那一台是**两半**,别混着看:
|
|
90
|
+
|
|
91
|
+
- **启动器**:`host.js`、两层 patch、`config/`、内嵌的凭据包。把仓库目录 rsync 到 `/opt/dsh-mobile/`,
|
|
92
|
+
然后 `systemctl restart dsh-mobile-host` —— 它只负责把宿主那棵树拼起来。
|
|
93
|
+
- **插件本体**:装在 profile 里,用 pnpm 从 npm 装:
|
|
94
|
+
`cd <DSH_HOME>/profiles/<profile> && pnpm add dsh-mobile-plugin@<版本>`。宿主按**绝对路径**挂那份的
|
|
95
|
+
`lib/index.js`(**稳定外壳**),所以这台面板里也有「检查 / 立即更新」,换代**不用重启宿主**。
|
|
96
|
+
profile 的 `autoInstallPeers: false` 是刻意的:peer 依赖(`@deepseek-ai/*`)由宿主自己的依赖树供,
|
|
97
|
+
两边必须解析到**同一份 realpath**,否则会出现两份 cordis(见 `lib/shell/manager.js` 的文件头)。
|
|
98
|
+
- 挂哪一份由 `host.js` 现场判断(`profile` 里有没有这个包,不看环境变量):装了挂那份外壳;
|
|
99
|
+
没装就退回**源码树直挂**(没有外壳,面板里更新不了,只能同步源码 + 重启宿主)。两种现场都会在
|
|
100
|
+
启动日志里打一行「挂载方式 = …」,`npm run test:plugin:server` 会核对它。
|
|
89
101
|
|
|
90
102
|
**桌面端:从 npm 装,更新交给 DSH 的插件市场。**(2026-10 改的:此前是本包自带的 OTA +
|
|
91
103
|
「稳定外壳 + 可热替换的代」,那套已经删掉。)
|
package/host.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* package's `cordis.patch.yml`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { writeFileSync } from 'node:fs'
|
|
17
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { dirname, join } from 'node:path'
|
|
19
19
|
import { fileURLToPath } from 'node:url'
|
|
20
20
|
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
|
@@ -59,6 +59,32 @@ try {
|
|
|
59
59
|
initProfile(profileDir, PROFILE_TEMPLATES.web)
|
|
60
60
|
const profile = loadProfile(BIN_NAME, PROFILE_NAME, INSTALL_ANCHOR, home, { userLayer: true })
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 本插件是**装在这个 profile 里**的,还是「源码树直接跑」的?
|
|
64
|
+
*
|
|
65
|
+
* 这两种现场的挂载方式不一样,而差别全在「挂哪一份入口」上(2026-10-11 起两条都留着):
|
|
66
|
+
*
|
|
67
|
+
* - **装在 profile 里**(桌面端;服务器那台改成 `pnpm 装进 profile` 之后也是)→ 挂
|
|
68
|
+
* `<profile>/node_modules/dsh-mobile-plugin/lib/index.js`,也就是**稳定外壳**:
|
|
69
|
+
* 查更新、装新版、就地换代那一套全在它手里,所以面板上才有「检查 / 立即更新」。
|
|
70
|
+
* - **源码树直接跑**(老的无头部署、开发时直接 `node host.js`)→ 插件**就是** Loader 自己
|
|
71
|
+
* 正跑的那个包,没有任何人把它放进 `node_modules`,于是这一支挂随包发布的
|
|
72
|
+
* `lib/impl.js`(真正的实现)。代价是**没有外壳** → 那台的面板里更新不了,只能同步
|
|
73
|
+
* 源码 + 重启宿主。
|
|
74
|
+
*
|
|
75
|
+
* **两处都走「绝对路径」而不是包名**:`cordis.patch.yml` 里那行按包名挂载是给桌面端用的,
|
|
76
|
+
* 无头宿主这边解析不到 —— loader 是从**它自己所在的位置**解析包名的(实测报错原文:
|
|
77
|
+
* `Cannot find package 'dsh-mobile-plugin' imported from <…>/@deepseek-ai/cordis-plugin-loader/lib/index.js`),
|
|
78
|
+
* 而它在宿主自己的依赖树里,profile 里装的那份它根本看不见。而 loader 对解析失败不是留一条
|
|
79
|
+
* 警告,是**整棵插件树加载失败**(服务器那台当年就是这么完全起不来的)。所以那行在无头宿主
|
|
80
|
+
* 里一律摘掉,由这里按绝对路径插一行 —— 装没装 profile 决定的是**挂哪一份**,不是挂不挂。
|
|
81
|
+
*
|
|
82
|
+
* 判据取「profile 里有没有这个包」,不看环境变量、不看目录名:装没装是事实,改不了口径。
|
|
83
|
+
*/
|
|
84
|
+
const installedDir = join(profileDir, 'node_modules', BIN_NAME)
|
|
85
|
+
const selfInstalled = existsSync(join(installedDir, 'package.json'))
|
|
86
|
+
const selfPatches = loadOverlayPatches(BIN_NAME, join(PACKAGE_DIR, 'cordis.patch.yml'))
|
|
87
|
+
|
|
62
88
|
// An empty entry list is the composition root: upstream fills the tree from
|
|
63
89
|
// the profile's bundle layers; every plugin arrives via a patch layer.
|
|
64
90
|
const rootConfig = join(profileDir, 'cordis.root.yml')
|
|
@@ -67,15 +93,24 @@ try {
|
|
|
67
93
|
const patches = [
|
|
68
94
|
...profile.layers.flatMap(layer => layer.patches),
|
|
69
95
|
...profile.patches,
|
|
70
|
-
...dropBareNameSelfMounts(
|
|
96
|
+
...dropBareNameSelfMounts(selfPatches, BIN_NAME),
|
|
71
97
|
// 宿主专属的层(不开浏览器、目录选择器钉成 browse)。刻意不放进
|
|
72
98
|
// cordis.patch.yml:那一层桌面 GUI 也会加载,而桌面操作者就坐在机器前,
|
|
73
99
|
// 不需要把原生目录选择器换成远程用的 browse 交互。
|
|
74
100
|
...loadOverlayPatches(BIN_NAME, join(PACKAGE_DIR, 'cordis.mobile-host.yml')),
|
|
75
101
|
// The mobile API is this very package. The Loader imports an entry by bare
|
|
76
|
-
// name from its own location, and `dsh-mobile-plugin
|
|
77
|
-
//
|
|
78
|
-
|
|
102
|
+
// name from its own location, and that location has no `dsh-mobile-plugin`,
|
|
103
|
+
// so the row names the entry module by absolute path — profile 里装了就挂
|
|
104
|
+
// 那份的外壳,没装就挂源码树自己的实现(见上)。
|
|
105
|
+
{
|
|
106
|
+
insert: [{
|
|
107
|
+
id: 'mobile-api',
|
|
108
|
+
name: selfInstalled
|
|
109
|
+
? join(installedDir, 'lib', 'index.js')
|
|
110
|
+
: join(PACKAGE_DIR, 'lib/impl.js'),
|
|
111
|
+
config: { enabled: true },
|
|
112
|
+
}],
|
|
113
|
+
},
|
|
79
114
|
// The DSH Web bundle disables every agent-plane row (shell, files, search,
|
|
80
115
|
// skills, goals, planning, compaction, delegation) and lets each session
|
|
81
116
|
// mount a preset instead — but it leaves the roster's shipped root to
|
|
@@ -93,6 +128,10 @@ try {
|
|
|
93
128
|
},
|
|
94
129
|
]
|
|
95
130
|
|
|
131
|
+
process.stderr.write(
|
|
132
|
+
`${BIN_NAME}: 挂载方式 = ${selfInstalled ? `profile 里装的那份(${installedDir},带稳定外壳,面板里能自更新)` : '源码树直挂(无外壳,更新靠同步源码 + 重启宿主)'}\n`,
|
|
133
|
+
)
|
|
134
|
+
|
|
96
135
|
const ctx = await boot(BIN_NAME, rootConfig, patches, (hostCtx) => {
|
|
97
136
|
// A headless mobile host has no command line: nothing passes flags and
|
|
98
137
|
// nobody is watching a help screen. The web startup plugin still waits on
|
|
@@ -10,7 +10,35 @@
|
|
|
10
10
|
* 按 frame.payload 里的 type 分发,形状对上了它们一行都不用改。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const { openStream, warn } = require('./transport.js');
|
|
13
|
+
const { gatewayOf, openStream, warn } = require('./transport.js');
|
|
14
|
+
|
|
15
|
+
const warnedUnavailableContexts = new WeakSet();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Keep the compatibility SSE alive when a host has no streaming gateway. The
|
|
19
|
+
* device stream also carries plugin-owned signals and heartbeats, so ending it
|
|
20
|
+
* here would cause reconnect churn and disable that independent data path.
|
|
21
|
+
*/
|
|
22
|
+
async function* unavailableHostStream(ctx, signal) {
|
|
23
|
+
const gateway = gatewayOf(ctx);
|
|
24
|
+
if (gateway && typeof gateway.stream === 'function') return false;
|
|
25
|
+
|
|
26
|
+
if (ctx && typeof ctx === 'object' && !warnedUnavailableContexts.has(ctx)) {
|
|
27
|
+
warnedUnavailableContexts.add(ctx);
|
|
28
|
+
warn(ctx, '宿主未提供 typertGateway.stream,跳过宿主事件并保持设备流空闲');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!signal || signal.aborted) return true;
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
const finish = () => {
|
|
34
|
+
signal.removeEventListener('abort', finish);
|
|
35
|
+
resolve();
|
|
36
|
+
};
|
|
37
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
38
|
+
if (signal.aborted) finish();
|
|
39
|
+
});
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
14
42
|
|
|
15
43
|
// ── 事件合成 ──────────────────────────────────────────────────────────────
|
|
16
44
|
|
|
@@ -168,6 +196,7 @@ async function* deviceFrames(ctx, signal) {
|
|
|
168
196
|
function muxFrames(ctx, request, signal) {
|
|
169
197
|
const sessionId = sessionHint(request);
|
|
170
198
|
return (async function* run() {
|
|
199
|
+
if (yield* unavailableHostStream(ctx, signal)) return;
|
|
171
200
|
if (!sessionId) {
|
|
172
201
|
yield* deviceFrames(ctx, signal);
|
|
173
202
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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",
|