dsh-mobile-plugin 0.1.78 → 0.1.82

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.
@@ -128,11 +128,41 @@ function dropUnexpectedArgs(args, message) {
128
128
  return removed ? next : null;
129
129
  }
130
130
 
131
+ /**
132
+ * 调用宿主时一并交出「本机操作者」身份。
133
+ *
134
+ * 0.2.1 起网关在派发前先 `assertPeerLive(request.peer)`:Peer 缺了就对 undefined 读
135
+ * `.signal`,抛 `Cannot read properties of undefined (reading 'signal')`,这里把它收敛成
136
+ * `host-call-failed` —— 手机上表现为 /sessions、/workspaces 一律 400,会话镜像永远
137
+ * 「未推送」(只有本机自算的 /status 还活着)。0.2.0 及更早没有这道检查(网关自己回落
138
+ * operatorPeer),也没有 `browserPeer` 这个成员,所以取不到就**不带**,行为与从前一致。
139
+ *
140
+ * 用 `ctx.get` 而不是 inject:旧宿主没这个成员、极简 profile 也可能没有这个服务,
141
+ * 注入一个不存在的服务会让整行永远 pending(见 impl.js 里 apiProxy 那段教训)。
142
+ */
143
+ function peerOf(ctx) {
144
+ try {
145
+ const connection = ctx.get && ctx.get('connection');
146
+ const peer = connection && connection.browserPeer;
147
+ return peer && typeof peer === 'object' ? peer : null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /** 补齐到「当前宿主认得」的请求形状:新旧宿主的差别只在这一个函数里判定。 */
154
+ function hostRequest(ctx, namespace, method, args, signal) {
155
+ const request = { namespace, method, args };
156
+ if (signal) request.signal = signal;
157
+ const peer = peerOf(ctx);
158
+ if (peer) request.peer = peer;
159
+ return request;
160
+ }
161
+
131
162
  async function invoke(ctx, namespace, method, args, signal) {
132
163
  const gateway = gatewayOf(ctx);
133
164
  if (!gateway) return { error: { ...UNAVAILABLE } };
134
- const request = { namespace, method, args };
135
- if (signal) request.signal = signal;
165
+ const request = hostRequest(ctx, namespace, method, args, signal);
136
166
  try {
137
167
  return { value: await gateway.invoke(request) };
138
168
  } catch (error) {
@@ -154,9 +184,7 @@ async function invoke(ctx, namespace, method, args, signal) {
154
184
  async function openStream(ctx, namespace, method, args, signal) {
155
185
  const gateway = gatewayOf(ctx);
156
186
  if (!gateway) throw Object.assign(new Error(UNAVAILABLE.message), { code: UNAVAILABLE.code });
157
- const request = { namespace, method, args };
158
- if (signal) request.signal = signal;
159
- return gateway.stream(request);
187
+ return gateway.stream(hostRequest(ctx, namespace, method, args, signal));
160
188
  }
161
189
 
162
190
  async function invokeEnvelope(ctx, namespace, method, args, signal) {
@@ -8,7 +8,21 @@ import { compareVersion } from './manager.js'
8
8
 
9
9
  const require_ = createRequire(import.meta.url)
10
10
  const zlib = require_('node:zlib')
11
- const ROOT = () => path.join(dshHome(), 'plugin-ota')
11
+ /**
12
+ * OTA 包落在哪。
13
+ *
14
+ * 默认 `<DSH_HOME>/plugin-ota`,但外壳知道 profile 在哪之后会把它挪到**profile 里面**
15
+ * (`<profile>/.dsh-mobile-ota`)。原因不是洁癖:DSH 宿主的 profile-resolution 拦截层
16
+ * 只认 `profiles/` 树内的模块,树外的裸包名走的是原生解析 —— 而宿主提供的那批 peer
17
+ * (`@deepseek-ai/dsh-tools` 之类)压根不在 profile 的 node_modules 里,于是 OTA 代
18
+ * 第一句 `require('@deepseek-ai/dsh-tools')` 就 MODULE_NOT_FOUND(2026-10-17 真机:
19
+ * 0.1.80 装载失败)。落在 profile 里之后,OTA 代与装进来的那一代走**同一条**解析链。
20
+ */
21
+ let storageRoot = ''
22
+ export function configureOtaRoot(dir) {
23
+ if (typeof dir === 'string' && dir.length > 0) storageRoot = dir
24
+ }
25
+ const ROOT = () => storageRoot || path.join(dshHome(), 'plugin-ota')
12
26
  const PACKAGE = 'dsh-mobile-plugin'
13
27
  const RELAY = 'https://dsh-hub.wnluo.com/api/desktop-plugins/ota'
14
28
  const CHANNEL = 'stable'
@@ -32,7 +32,8 @@ import fs from 'node:fs'
32
32
  import { createRequire } from 'node:module'
33
33
  import path from 'node:path'
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url'
35
- import { availableOta } from './device-ota.js'
35
+ import { availableOta, configureOtaRoot } from './device-ota.js'
36
+ import { dshHome } from '../shared/paths.js'
36
37
 
37
38
  /** CJS 模块缓存。实现树里 `lib/relay`、`lib/routes`、`lib/mobile` 都是 CJS。 */
38
39
  const require_ = createRequire(import.meta.url)
@@ -227,6 +228,83 @@ function evictCommonJsTree(roots) {
227
228
  }
228
229
  }
229
230
 
231
+ /**
232
+ * OTA 包落到 `<profile>/.dsh-mobile-ota`(不再放 `<DSH_HOME>/plugin-ota`)。
233
+ *
234
+ * 宿主对模块解析有一层拦截:**`profiles/` 树内**的模块,裸包名会先按 profile 的
235
+ * 本地包解析、再回落到宿主自己那份;树外的模块走原生解析。而宿主提供的那批 peer
236
+ * (`@deepseek-ai/dsh-tools` 等)并不在 profile 的 `node_modules` 里 —— 所以 OTA 包
237
+ * 一旦落在树外,那一代 `require('@deepseek-ai/dsh-tools')` 就是 MODULE_NOT_FOUND
238
+ * (2026-10-17 真机:0.1.80 装载失败)。位置挪进 profile 后,OTA 代与装进来的那一代
239
+ * 走同一条解析链,不必再靠调用点逐个打补丁。
240
+ *
241
+ * 老位置残留的包顺手搬过来:换代失败时它还是磁盘上的回退包,不能在升级路径上丢。
242
+ */
243
+ function moveOtaRootIntoProfile() {
244
+ if (path.basename(PROFILE_NODE_MODULES) !== 'node_modules') return
245
+ const next = path.join(path.dirname(PROFILE_NODE_MODULES), '.dsh-mobile-ota')
246
+ const legacy = path.join(dshHome(), 'plugin-ota')
247
+ configureOtaRoot(next)
248
+ try {
249
+ if (!fs.existsSync(legacy) || fs.lstatSync(legacy).isSymbolicLink()) return
250
+ fs.mkdirSync(next, { recursive: true })
251
+ for (const entry of fs.readdirSync(legacy)) {
252
+ const target = path.join(next, entry)
253
+ if (fs.existsSync(target)) continue
254
+ fs.renameSync(path.join(legacy, entry), target)
255
+ }
256
+ fs.rmSync(legacy, { recursive: true, force: true })
257
+ } catch {
258
+ // 搬不动就用老位置:那是「OTA 仍可下载但可能装不起来」,比让外壳启动失败强
259
+ }
260
+ }
261
+
262
+ /**
263
+ * 给 OTA 代补齐「裸包名怎么解析」。
264
+ *
265
+ * Node 只沿**自己的祖先目录**找 `node_modules`,而 OTA 落在
266
+ * `<DSH_HOME>/plugin-ota/<version>/` —— 这条路径上没有任何一份 `node_modules`。于是
267
+ * `lib/impl.js` 第一句 `import { z } from '@deepseek-ai/schemastery'` 就报
268
+ * `Cannot find package`,一代整个装不起来(2026-10-17 真机:0.1.79 的 OTA 装载失败,
269
+ * 外壳按规矩回滚到旧代,机器上什么都没变)。
270
+ *
271
+ * 修法是补上基础代本来就有、而 OTA 路径上没有的那两层软链:
272
+ * `<version>/node_modules` → profile 的 `node_modules`
273
+ * `<version>/dsh-mobile-plugin/node_modules` → profile 里那份插件的 `node_modules`(pnpm 严格布局才有)
274
+ * 从 `<version>/dsh-mobile-plugin/lib` 往上第一站就命中,解析到的是**同一份** peer 与依赖,
275
+ * 与基础代的 real path 一致 —— 不会出现两份 cordis/schemastery。
276
+ */
277
+ /** 外壳一启动就把 OTA 落盘位置定下来(见 moveOtaRootIntoProfile 的注释)。 */
278
+ moveOtaRootIntoProfile()
279
+
280
+ function ensureGenerationModules(generation) {
281
+ if (!generation || generation.source !== 'ota' || !generation.dir) return
282
+ // 只认真正的 `node_modules`:`findProfileNodeModules()` 找不到时会回落到 profile 根,
283
+ // 把那个当依赖目录链过去等于凭空造一层错的解析路径。
284
+ if (path.basename(PROFILE_NODE_MODULES) !== 'node_modules' || !fs.existsSync(PROFILE_NODE_MODULES)) return
285
+ const versionDir = path.dirname(generation.dir)
286
+ const link = (target, at) => {
287
+ try {
288
+ if (fs.lstatSync(at).isSymbolicLink()) {
289
+ if (fs.readlinkSync(at) === target) return
290
+ fs.unlinkSync(at)
291
+ } else {
292
+ return // 已经是实体目录(有人手工铺过依赖):不动它
293
+ }
294
+ } catch {
295
+ // 还不存在,往下建
296
+ }
297
+ try {
298
+ fs.symlinkSync(target, at, 'dir')
299
+ } catch {
300
+ // 盘上不让写(只读 home / 权限不足)就让解析失败如实报出来,别把它变成静默的空实现
301
+ }
302
+ }
303
+ link(PROFILE_NODE_MODULES, path.join(versionDir, 'node_modules'))
304
+ const nested = path.join(PROFILE_NODE_MODULES, PLUGIN_PKG, 'node_modules')
305
+ if (fs.existsSync(nested)) link(nested, path.join(versionDir, PLUGIN_PKG, 'node_modules'))
306
+ }
307
+
230
308
  /**
231
309
  * 动态 import 一个代。
232
310
  *
@@ -247,6 +325,7 @@ export async function importGeneration(generation) {
247
325
  const cacheKey = generationCacheKey(generation)
248
326
  const cached = imported.get(cacheKey)
249
327
  if (cached) return cached
328
+ ensureGenerationModules(generation)
250
329
  // 先把 CJS 缓存清干净再 import,否则「入口是新的、子模块还是旧的」,等于换了个半成品上去。
251
330
  evictCommonJsTree(evictionRoots(generation))
252
331
  importSeq += 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.1.78",
3
+ "version": "0.1.82",
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",