dsh-jiuling-remote-mobile 1.0.19 → 1.0.20

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/lib/client.js CHANGED
@@ -284,6 +284,68 @@ window.__ModuleLoader__.load({
284
284
  */
285
285
  var remoteSessionIds = {};
286
286
 
287
+ /**
288
+ * 电脑镜像工作区的 slug 集合 —— 隔离的**真正判据**。
289
+ *
290
+ * ⚠️⚠️ **为什么不用 `remoteSessionIds`(那是第一代,已被用户报障推翻)。**
291
+ *
292
+ * 第一代按 **sessionId** 判定,只在「用户从电脑文件夹对话框里建会话」
293
+ * 或「宿主拦截器绑过」时才知道。而用户在**侧边栏点「新建会话」**时,
294
+ * 官方前端直接 `sessionController.create({workspaceId})`,**不经过本插件**;
295
+ * 宿主的 create 拦截器认得出镜像目录(消息发得对),但前端不知道
296
+ * ⇒ 模型菜单仍按「本地」筛,显示手机模型。
297
+ *
298
+ * 用户原话:「只有我手动添加工作区的时候,才会触发模型和预设的刷新」——
299
+ * 说的就是判据挂在了「走了哪条路」上,而不是「会话的工作区在哪」。
300
+ *
301
+ * 正解:**按 cwd 判定**,与宿主 `desktopPathOfMirror` 同一个轴。
302
+ * 前端从 `sessions.list` 里同步读到会话的 `cwd`,看它是不是
303
+ * `…/workspaces/<slug>` 且 slug 属于电脑镜像。这条路与「谁建的会话」
304
+ * 完全无关,所以**任何入口进来都一致,不需要任何刷新触发**。
305
+ */
306
+ var mirrorSlugs = {};
307
+
308
+ /**
309
+ * 从 cwd 取出镜像工作区的 slug(与宿主 `desktopPathOfMirror` 同构)。
310
+ *
311
+ * ⚠️ 用 `/workspaces/` 定位而不是前缀比对:`workspace.path` 是 realpath,
312
+ * 真机上 `/data/user/0` 与 `/data/data` 会差一层,前缀一比就永远不命中
313
+ * (宿主那边踩过这个坑,注释见 `desktopPathOfMirror`)。
314
+ * 两种分隔符都认:手机是 posix,但在 Windows 上调试时是 `\`。
315
+ *
316
+ * @param cwd - 会话的 cwd。
317
+ * @returns 镜像工作区的 slug;不是镜像目录时 `undefined`。
318
+ */
319
+ function mirrorSlugOf(cwd) {
320
+ if (typeof cwd !== 'string' || cwd === '') return undefined;
321
+ var at = cwd.search(/[\\/]workspaces[\\/]/);
322
+ if (at < 0) return undefined;
323
+ var slug = cwd.slice(at).split(/[\\/]/)[2];
324
+ return slug === undefined || slug === '' ? undefined : slug;
325
+ }
326
+
327
+ /**
328
+ * 这个会话是不是跑在电脑上 —— **同步**判定,任何入口都用同一条。
329
+ *
330
+ * ⚠️ **cwd 读得到时,它以 cwd 为唯一判据**(不看 `remoteSessionIds`):
331
+ * 工作区就是隔离条件本身。解绑后 `mirrorSlugs` 清空 ⇒ 全部回到本地,
332
+ * 不需要再去清每个会话的旧标记。
333
+ *
334
+ * 只有列表行还没带 cwd 的那一两帧(会话刚建好)才退回 `remoteSessionIds`。
335
+ *
336
+ * @param sessionId - 会话 id。
337
+ * @param cwdOf - `sessionId → cwd` 的同步读取函数。
338
+ * @returns 是不是电脑工作区的会话。
339
+ */
340
+ function isRemoteSession(sessionId, cwdOf) {
341
+ var cwd = cwdOf === undefined ? undefined : cwdOf(sessionId);
342
+ if (typeof cwd === 'string' && cwd !== '') {
343
+ var slug = mirrorSlugOf(cwd);
344
+ return slug !== undefined && mirrorSlugs[slug] === true;
345
+ }
346
+ return remoteSessionIds[sessionId] === true;
347
+ }
348
+
287
349
  /** 从宿主拉一次全量远程会话清单(失败就按「全是本地」处理)。 */
288
350
  function loadRemoteSessionIds() {
289
351
  return bridge('remoteSessions', {}).then(
@@ -300,6 +362,28 @@ window.__ModuleLoader__.load({
300
362
  );
301
363
  }
302
364
 
365
+ /**
366
+ * 从宿主拉一次「电脑镜像工作区的 slug 集合」(隔离的真正判据)。
367
+ *
368
+ * 拉不到就保持原样(空集合 = 全部按本地处理):最坏是本地工作区多看到
369
+ * 电脑模型,**不会误发** —— 消息该发给谁由宿主 `prompt` 拦截器按
370
+ * `remoteSessions` 决定,与这里无关。
371
+ */
372
+ function loadMirrorSlugs() {
373
+ return bridge('mirrorSlugs', {}).then(
374
+ function (result) {
375
+ var slugs = result && result.slugs;
376
+ if (!Array.isArray(slugs)) return;
377
+ var next = {};
378
+ for (var at = 0; at < slugs.length; at += 1) next[slugs[at]] = true;
379
+ mirrorSlugs = next;
380
+ },
381
+ function () {
382
+ /* 同上:最坏是多显示几项,不会误发 */
383
+ },
384
+ );
385
+ }
386
+
303
387
  /**
304
388
  * 按「这个会话是不是电脑的」筛出该显示的组与失败项。
305
389
  *
@@ -459,13 +543,17 @@ window.__ModuleLoader__.load({
459
543
  *
460
544
  * @param directory - 官方的 `ModelDirectory` 实例。
461
545
  * @param sessionId - 它归属的会话。
546
+ * @param cwdOf - `sessionId → cwd` 的**同步**读取函数(隔离判据要用它)。
462
547
  * @returns 读路径被筛过的同构对象。
463
548
  */
464
- function scopeDirectoryToWorkspace(directory, sessionId) {
549
+ function scopeDirectoryToWorkspace(directory, sessionId, cwdOf) {
465
550
  if (!directory || !directory.store || typeof directory.store.getSnapshot !== 'function') {
466
551
  return directory;
467
552
  }
468
- const isRemote = function () { return remoteSessionIds[sessionId] === true; };
553
+ // ⚠️ 判据是**每次读时现算**的(不是建包装时算一次):会话的 cwd 可能
554
+ // 在列表行到达后才出现,而且解绑/换工作区都会改结论。
555
+ // `isRemoteSession` 见其注释 —— 它看的 cwd,跟宿主 `desktopPathOfMirror` 同一个轴。
556
+ const isRemote = function () { return isRemoteSession(sessionId, cwdOf); };
469
557
  const wrapper = Object.create(Object.getPrototypeOf(directory));
470
558
  Object.assign(wrapper, directory);
471
559
 
@@ -551,9 +639,10 @@ window.__ModuleLoader__.load({
551
639
  * 旧行为,不会报错)。返回一个撤销函数。
552
640
  *
553
641
  * @param ctx - client 根上下文。
642
+ * @param cwdOf - `sessionId → cwd` 的**同步**读取函数(隔离判据要用它)。
554
643
  * @returns 撤销函数。
555
644
  */
556
- function patchModelDirectories(ctx) {
645
+ function patchModelDirectories(ctx, cwdOf) {
557
646
  var models = ctx.get === undefined ? undefined : ctx.get('modelDirectories');
558
647
  if (!models || typeof models.directoryFor !== 'function') return function () {};
559
648
 
@@ -595,7 +684,7 @@ window.__ModuleLoader__.load({
595
684
  raw.directoryFor = function (sessionId) {
596
685
  var directory = call(sessionId);
597
686
  if (directory.__dshRemoteScoped !== undefined) return directory.__dshRemoteScoped;
598
- var scoped = scopeDirectoryToWorkspace(directory, sessionId);
687
+ var scoped = scopeDirectoryToWorkspace(directory, sessionId, cwdOf);
599
688
  try {
600
689
  directory.__dshRemoteScoped = scoped;
601
690
  } catch (error) {
@@ -609,6 +698,41 @@ window.__ModuleLoader__.load({
609
698
  };
610
699
  }
611
700
 
701
+ /**
702
+ * 造一个 `sessionId → cwd` 的**同步**读取函数。
703
+ *
704
+ * 会话的 cwd 在客户端是**同步可读**的:`sessions` 服务的 `list` 是
705
+ * `createSnapshotStore({ids, byId, …})`,`projectList()` 给每一行算出了
706
+ * `cwd`(`dsh-api-session-controller/lib/client.js` 的 `byId[id] = {id,
707
+ * displayTitle, running, …, cwd?, …}`)。
708
+ *
709
+ * ⚠️ 之所以要「同步」:模型菜单每渲染一帧都会读一次目录快照
710
+ * (`getSnapshot`),那里**不能**是异步的。
711
+ *
712
+ * 全用显式 `if` 判空 —— chrome73 语法约束,不能用 `?.`。
713
+ *
714
+ * @param ctx - client 根上下文。
715
+ * @returns 读取函数;读不到时返回 `undefined`。
716
+ */
717
+ function makeCwdOf(ctx) {
718
+ return function (sessionId) {
719
+ if (typeof sessionId !== 'string' || sessionId === '') return undefined;
720
+ var sessions = ctx.sessions;
721
+ if (sessions === undefined || sessions === null) return undefined;
722
+ var list = sessions.list;
723
+ if (list === undefined || list === null) return undefined;
724
+ if (typeof list.getSnapshot !== 'function') return undefined;
725
+ var snapshot = list.getSnapshot();
726
+ if (snapshot === undefined || snapshot === null) return undefined;
727
+ var byId = snapshot.byId;
728
+ if (byId === undefined || byId === null) return undefined;
729
+ var row = byId[sessionId];
730
+ if (row === undefined || row === null) return undefined;
731
+ var cwd = row.cwd;
732
+ return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
733
+ };
734
+ }
735
+
612
736
  /**
613
737
  * 调宿主桥的一条 RPC(令牌只在引擎进程里,浏览器侧不持有)。
614
738
  *
@@ -707,8 +831,10 @@ window.__ModuleLoader__.load({
707
831
  var registerLocal = props.registerLocal;
708
832
  /** 兜底:拉一次全量会话列表(登记失败时用)。 */
709
833
  var refreshSessions = props.refreshSessions;
710
- /** 把会话标记成「电脑的」,让模型菜单按工作区隔离(见 `remoteSessionIds`)。 */
834
+ /** 把会话标记成「电脑的」(cwd 还没进列表行时兜底,见 `isRemoteSession`)。 */
711
835
  var markRemote = props.markRemote;
836
+ /** 重拉一次镜像工作区 slug 集合(新建镜像工作区之后必须调)。 */
837
+ var refreshMirrorSlugs = props.refreshMirrorSlugs;
712
838
  /** 解绑后把所有会话退回「本地」。 */
713
839
  var clearRemote = props.clearRemote;
714
840
 
@@ -927,6 +1053,10 @@ window.__ModuleLoader__.load({
927
1053
  // 不作废的话用户接下来看到的仍然是配对前那份手机模型列表 ——
928
1054
  // 就是用户报的「模型列表还是没同步」。
929
1055
  reloadModelCatalog();
1056
+ // ⚠️ **镜像工作区 slug 集合也要重拉**:换一台电脑后 `workspaceMap`
1057
+ // 是另一份(slug 由电脑路径哈希得来,同名不同机 slug 不同)。
1058
+ // 不重拉的话,新电脑的镜像工作区会被判成「本地」—— 又是那个 bug。
1059
+ if (refreshMirrorSlugs !== undefined) refreshMirrorSlugs();
930
1060
  },
931
1061
  function (reason) {
932
1062
  if (!aliveRef.current) return;
@@ -1229,9 +1359,13 @@ window.__ModuleLoader__.load({
1229
1359
  */
1230
1360
  var settleLocal = function (result) {
1231
1361
  if (!result || typeof result.sessionId !== 'string') return Promise.resolve();
1232
- // ⚠️ **先标记「这是电脑的」再登记**:模型菜单的隔离判据就靠这个标记,
1233
- // 而用户在会话刚建好、还没发第一条消息时就会去看模型列表。
1362
+ // ⚠️ **先标记「这是电脑的」再登记**:列表行的 `cwd` 要等宿主那一行
1363
+ // 回来才带上,之前那几帧判据读不到 cwd,靠这个标记兜住。
1234
1364
  if (markRemote !== undefined) markRemote(result.sessionId);
1365
+ // ⚠️ **重拉镜像工作区 slug 集合**:宿主刚为这个电脑目录建好(或复用)
1366
+ // 镜像工作区,`workspaceMap` 可能**刚多出一条**。前端那份集合是快照,
1367
+ // 不重拉的话这个工作区在侧边栏里点进去仍被判成「本地」。
1368
+ if (refreshMirrorSlugs !== undefined) refreshMirrorSlugs();
1235
1369
  var local = registerLocal === undefined
1236
1370
  ? Promise.resolve()
1237
1371
  : Promise.resolve(registerLocal(result.sessionId, result.cwd)).then(null, function () {
@@ -1674,17 +1808,32 @@ window.__ModuleLoader__.load({
1674
1808
  markRemote: (sessionId) => {
1675
1809
  if (typeof sessionId === 'string' && sessionId !== '') remoteSessionIds[sessionId] = true;
1676
1810
  },
1811
+ /**
1812
+ * 重拉一次「电脑镜像工作区 slug 集合」。
1813
+ *
1814
+ * ⚠️ **必须在「刚给某个电脑目录建好工作区」之后调**:宿主
1815
+ * `ensureDesktopWorkspace()` 每碰到一个新的电脑目录就往 `workspaceMap`
1816
+ * 里加一条 slug,而前端这份集合是**快照**。不重拉的话,新加的镜像工作区
1817
+ * 在侧边栏里点进去仍会被判成「本地」—— 正是用户报的那个 bug。
1818
+ */
1819
+ refreshMirrorSlugs: () => loadMirrorSlugs(),
1677
1820
  /** 解绑后把所有会话退回「本地」(电脑已经不在了)。 */
1678
1821
  clearRemote: () => {
1679
1822
  remoteSessionIds = {};
1823
+ mirrorSlugs = {};
1680
1824
  },
1681
1825
  });
1682
1826
 
1683
1827
  // 拉一次全量远程会话清单(重启 App 后已有会话也要能正确识别)。
1684
1828
  loadRemoteSessionIds();
1829
+ // 拉一次镜像工作区 slug 集合(隔离的**真正判据**,见 `mirrorSlugs`)。
1830
+ loadMirrorSlugs();
1685
1831
 
1686
1832
  // 把「按工作区筛模型目录」接到官方服务上。
1687
- ctx.effect(() => patchModelDirectories(ctx), 'dsh-remote-mobile: workspace-scoped model catalog');
1833
+ ctx.effect(
1834
+ () => patchModelDirectories(ctx, makeCwdOf(ctx)),
1835
+ 'dsh-remote-mobile: workspace-scoped model catalog',
1836
+ );
1688
1837
 
1689
1838
  // 负 priority:single slot 取 entries[0],entries 按 priority 升序 ⇒ 最先渲染。
1690
1839
  const register = (name) =>
package/lib/index.js CHANGED
@@ -1918,6 +1918,31 @@ async function handleBridge(ctx, config, io) {
1918
1918
  case 'remoteSessions':
1919
1919
  return { ids: [...io.remoteSessions] };
1920
1920
 
1921
+ /**
1922
+ * 电脑镜像工作区的 slug 集合(`workspaceMap` 的 keys)。
1923
+ *
1924
+ * ⚠️⚠️ **这是「模型列表按工作区隔离」的第二代判据,用户 1.0.19 报障后的正解。**
1925
+ *
1926
+ * 上一代用 `remoteSessions`(按 **sessionId**)当判据,漏掉了这条真实路径:
1927
+ * 用户在侧边栏点「新建会话」时,官方前端直接
1928
+ * `sessionController.create({workspaceId})` —— **不经过本插件的桥**。
1929
+ * 宿主的 create 拦截器认得出镜像目录(所以消息发得对),但**前端不知道**,
1930
+ * 于是模型菜单仍按「本地」筛,显示手机模型。
1931
+ *
1932
+ * 用户原话:「只有我手动添加工作区的时候,才会触发模型和预设的刷新」——
1933
+ * 一针见血:判据挂在了「走了哪条路」上,而不是「会话的工作区在哪」。
1934
+ *
1935
+ * 正解是**按 cwd 判定**,与宿主 `desktopPathOfMirror` 同一个轴:
1936
+ * 前端从 `sessions.list` 的会话行里**同步**读到 cwd,看它是不是
1937
+ * `<STATE_DIR>/workspaces/<slug>` 且 `slug` 在这里面。这条路
1938
+ * **与「谁建的会话」完全无关**,所以任何入口进来都一致。
1939
+ *
1940
+ * 只回 slug 列表而不是整张 `workspaceMap`:前端不需要电脑路径,
1941
+ * 少传一份数据也少一处可能不一致的地方。
1942
+ */
1943
+ case 'mirrorSlugs':
1944
+ return { slugs: Object.keys(io.workspaceMap ?? {}) };
1945
+
1921
1946
  /**
1922
1947
  * 配对:用电脑上生成的 6 位码换回这台电脑的隧道地址与令牌。
1923
1948
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-jiuling-remote-mobile",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "DSH 远程目录(手机端):在选文件夹流程里多出「电脑」页签,进入电脑目录、在电脑上发起对话,并把电脑的实时消息流同步到手机本地界面(只传文本,不映射桌面)。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",