dsh-jiuling-remote-mobile 1.0.23 → 1.0.24

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 (2) hide show
  1. package/lib/client.js +305 -13
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -305,6 +305,78 @@ window.__ModuleLoader__.load({
305
305
  */
306
306
  var mirrorSlugs = {};
307
307
 
308
+ /**
309
+ * 「用户刚刚在新建对话界面选中的那个工作区」的 cwd —— 只在**会话行还没带上
310
+ * cwd** 的那一小段时间里当兜底判据用。
311
+ *
312
+ * ⚠️⚠️ **为什么需要它(用户报障,2026-10-17 真机复现)**:
313
+ *
314
+ * 在**新建对话界面**(还没有任何消息)切到电脑工作区时,`sessions.list`
315
+ * 里那一行**还没有 cwd**(会话刚物化,宿主没回填),于是
316
+ * `isRemoteSession()` 只能退回 `remoteSessionIds[sessionId]` —— 而新建的
317
+ * 本地壳会话**不在里面**,结论是「本地」。后果:
318
+ *
319
+ * - 模型菜单只显示手机的模型(电脑的组被 `pickWorkspaceGroups` 筛掉);
320
+ * - 预设列表同理,仍是手机那份。
321
+ *
322
+ * 用户原话:「我在待选的工作区里面选择了默认工作区,也就是我手机的工作区,
323
+ * 这个预设它没有改回来,模型列表也没有改回来」——反过来切到电脑时也一样。
324
+ *
325
+ * 兜底只影响**这一小段窗口**:一旦列表行带回 cwd,`cwdOf()` 优先,这个值
326
+ * 自动失效(见 `isRemoteSession` 的判据顺序),不会长期歪掉。
327
+ */
328
+ var pendingWorkspaceCwd = null;
329
+
330
+ /**
331
+ * `pendingWorkspaceCwd` 的**过期时刻**(毫秒时间戳)。
332
+ *
333
+ * ⚠️ 为什么必须有这个:官方工作区下拉菜单走的是
334
+ * `workspaceNavigation.openWorkspace()`(`ConversationContent` 的
335
+ * `selectWorkspace`),**完全不经过本插件** —— 所以「用户切回默认工作区」
336
+ * 这个动作我这边收不到通知,没法在那个时刻清掉 `pendingWorkspaceCwd`。
337
+ *
338
+ * 不设过期的话,一旦某次在电脑工作区设过它、而之后那个会话行的 cwd
339
+ * 又一直读不到,兜底值就会**永久**把「本地会话」判成电脑会话,用户看到
340
+ * 的正是「切回默认工作区,模型和预设都没改回来」。
341
+ *
342
+ * 取 30 秒:足够覆盖「建会话 → 列表行回填 cwd」这一段(真机是几百毫秒),
343
+ * 又不至于让一个陈旧值活太久。
344
+ */
345
+ var pendingWorkspaceExpiresAt = 0;
346
+
347
+ /** `pendingWorkspaceCwd` 的有效期(毫秒)。 */
348
+ var PENDING_WORKSPACE_TTL = 30000;
349
+
350
+ /**
351
+ * 记下「刚选中的工作区」并开始计时。
352
+ *
353
+ * @param cwd - 工作区路径;传 `null`/空串表示**立刻作废**。
354
+ */
355
+ function setPendingWorkspace(cwd) {
356
+ if (typeof cwd !== 'string' || cwd === '') {
357
+ pendingWorkspaceCwd = null;
358
+ pendingWorkspaceExpiresAt = 0;
359
+ return;
360
+ }
361
+ pendingWorkspaceCwd = cwd;
362
+ pendingWorkspaceExpiresAt = Date.now() + PENDING_WORKSPACE_TTL;
363
+ }
364
+
365
+ /**
366
+ * 取当前有效的兜底 cwd(过期就当没有)。
367
+ *
368
+ * @returns 有效的 cwd 或 `null`。
369
+ */
370
+ function currentPendingWorkspace() {
371
+ if (pendingWorkspaceCwd === null) return null;
372
+ if (Date.now() > pendingWorkspaceExpiresAt) {
373
+ pendingWorkspaceCwd = null;
374
+ pendingWorkspaceExpiresAt = 0;
375
+ return null;
376
+ }
377
+ return pendingWorkspaceCwd;
378
+ }
379
+
308
380
  /**
309
381
  * 从 cwd 取出镜像工作区的 slug(与宿主 `desktopPathOfMirror` 同构)。
310
382
  *
@@ -331,7 +403,17 @@ window.__ModuleLoader__.load({
331
403
  * 工作区就是隔离条件本身。解绑后 `mirrorSlugs` 清空 ⇒ 全部回到本地,
332
404
  * 不需要再去清每个会话的旧标记。
333
405
  *
334
- * 只有列表行还没带 cwd 的那一两帧(会话刚建好)才退回 `remoteSessionIds`。
406
+ * 只有列表行还没带 cwd 的那一两帧(会话刚建好)才退回
407
+ * `pendingWorkspaceCwd` / `remoteSessionIds`。
408
+ *
409
+ * ⚠️ **判据顺序不能换**:
410
+ * 1. `cwdOf(sessionId)` —— 会话自己的 cwd,最准;
411
+ * 2. `pendingWorkspaceCwd` —— 用户在新建界面刚选的工作区(见它的注释,
412
+ * 这是修「新建界面模型/预设不跟随工作区」的那一条);
413
+ * 3. `remoteSessionIds` —— 宿主显式标记过的会话。
414
+ *
415
+ * 第 2 条只在第 1 条读不到时才生效,所以一旦会话行带回 cwd,
416
+ * 它就自动让位,不会跟真实 cwd 打架。
335
417
  *
336
418
  * @param sessionId - 会话 id。
337
419
  * @param cwdOf - `sessionId → cwd` 的同步读取函数。
@@ -343,6 +425,12 @@ window.__ModuleLoader__.load({
343
425
  var slug = mirrorSlugOf(cwd);
344
426
  return slug !== undefined && mirrorSlugs[slug] === true;
345
427
  }
428
+ // 会话自己的 cwd 还没进列表行 —— 用「刚选中的工作区」兜底(会过期)。
429
+ var pending = currentPendingWorkspace();
430
+ if (pending !== null) {
431
+ var pendingSlug = mirrorSlugOf(pending);
432
+ return pendingSlug !== undefined && mirrorSlugs[pendingSlug] === true;
433
+ }
346
434
  return remoteSessionIds[sessionId] === true;
347
435
  }
348
436
 
@@ -717,22 +805,215 @@ window.__ModuleLoader__.load({
717
805
  function makeCwdOf(ctx) {
718
806
  return function (sessionId) {
719
807
  if (typeof sessionId !== 'string' || sessionId === '') return undefined;
808
+ var row;
720
809
  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];
810
+ if (sessions !== undefined && sessions !== null) {
811
+ var list = sessions.list;
812
+ if (list !== undefined && list !== null && typeof list.getSnapshot === 'function') {
813
+ var snapshot = list.getSnapshot();
814
+ if (snapshot !== undefined && snapshot !== null) {
815
+ var byId = snapshot.byId;
816
+ if (byId !== undefined && byId !== null) row = byId[sessionId];
817
+ }
818
+ }
819
+ }
820
+ if (row !== undefined && row !== null) {
821
+ var cwd = row.cwd;
822
+ if (typeof cwd === 'string' && cwd !== '') return cwd;
823
+ }
824
+ // ⚠️ **会话行没有 cwd(甚至整行都还没出现)时,退回「它所属工作区的路径」。**
825
+ //
826
+ // 新建对话界面(还没有消息)那一行常常只有 `workspaceId`、没有 `cwd`:
827
+ // `cwd` 要等宿主把会话真正物化后才回填。这一小段窗口里如果直接
828
+ // 返回 undefined,`isRemoteSession()` 就把「电脑工作区的新会话」
829
+ // 判成本地 ⇒ 模型菜单只显示手机模型、预设也不换。
830
+ //
831
+ // 用户报障原话:「在待选的工作区里面选择了默认工作区…这个预设它没有
832
+ // 改回来,模型列表也没有改回来」——两个方向都是这个原因。
833
+ //
834
+ // 三条退路,先精确后宽松:
835
+ // 1. `pendingWorkspaceCwd` —— 用户刚在新建界面选中的工作区(见其注释);
836
+ // 2. `workspacePathOfSession` —— 按工作区的 `sessionIds` 反查
837
+ // (不依赖会话行本身有没有字段,所以 `row` 缺失也照样能用);
838
+ // 3. `workspacePathOf` —— 按 `row.workspaceId` 直查。
839
+ //
840
+ // 工作区快照是**同步**可读的,所以仍然满足「模型菜单每帧同步读」。
841
+ if (typeof pendingWorkspaceCwd === 'string' && pendingWorkspaceCwd !== '') {
842
+ var pendingNow = currentPendingWorkspace();
843
+ if (pendingNow !== null) return pendingNow;
844
+ }
845
+ var bySession = workspacePathOfSession(ctx, sessionId);
846
+ if (bySession !== undefined) return bySession;
730
847
  if (row === undefined || row === null) return undefined;
731
- var cwd = row.cwd;
732
- return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
848
+ return workspacePathOf(ctx, row.workspaceId);
733
849
  };
734
850
  }
735
851
 
852
+ /**
853
+ * 收集所有可能挂着工作区快照的宿主对象。
854
+ *
855
+ * ⚠️ 用 `ctx.get('workspaces')` 而不是只靠 `ctx.workspaces`:cordis 的
856
+ * `get()` 不受 `inject` 列表约束(`inject` 只影响属性访问的代理),
857
+ * 所以即使将来 `inject` 改了,这条路仍然能拿到服务。
858
+ *
859
+ * @param ctx - client 根上下文。
860
+ * @returns 候选宿主数组(可能为空)。
861
+ */
862
+ function workspaceHostsOf(ctx) {
863
+ var hosts = [];
864
+ var push = function (value) {
865
+ if (value === undefined || value === null) return;
866
+ for (var at = 0; at < hosts.length; at += 1) {
867
+ if (hosts[at] === value) return;
868
+ }
869
+ hosts.push(value);
870
+ };
871
+ try {
872
+ if (typeof ctx.get === 'function') push(ctx.get('workspaces'));
873
+ } catch (error) {
874
+ /* 服务没起就跳过 */
875
+ }
876
+ push(ctx.workspaces);
877
+ push(ctx.sessions);
878
+ push(ctx.uiWorkspace);
879
+ return hosts;
880
+ }
881
+
882
+ /**
883
+ * 按 `workspaceId` 查工作区的目录路径 —— **同步**,读不到就 `undefined`。
884
+ *
885
+ * 官方 `workspaces` 服务的快照长这样(`dsh-api-workspace-controller` 的
886
+ * `buildSnapshot()` → `{items, archivedSessionIds, pinnedSessionIds, …}`),
887
+ * 每个 item 由 `workspaceView()` 投影出来:
888
+ *
889
+ * {workspaceId, path, title, sessionIds, createdAt, updatedAt}
890
+ *
891
+ * ⚠️ 主键叫 **`workspaceId`**,不是 `id` —— 按 `id` 找永远找不到。
892
+ * ⚠️ 路径字段是 **`path`**(`workspace.path`)。
893
+ *
894
+ * @param ctx - client 根上下文。
895
+ * @param workspaceId - 工作区 id。
896
+ * @returns 工作区路径;查不到时 `undefined`。
897
+ */
898
+ function workspacePathOf(ctx, workspaceId) {
899
+ if (typeof workspaceId !== 'string' || workspaceId === '') return undefined;
900
+ var hosts = workspaceHostsOf(ctx);
901
+ for (var ht = 0; ht < hosts.length; ht += 1) {
902
+ var host = hosts[ht];
903
+ var stores = [host.list, host.workspaces, host];
904
+ for (var st = 0; st < stores.length; st += 1) {
905
+ var value = pathFromWorkspaceStore(stores[st], workspaceId);
906
+ if (value !== undefined) return value;
907
+ }
908
+ }
909
+ return undefined;
910
+ }
911
+
912
+ /**
913
+ * 从一个「工作区 store / 列表 / 数组」里取指定 id 的路径。
914
+ *
915
+ * 抽出来是为了让 `workspacePathOf` 能把多个来源串起来试,而不用把
916
+ * 「快照怎么取」的细节重复四遍。
917
+ *
918
+ * 兼容三种主键写法(`workspaceId` / `id`)与四种路径字段写法,
919
+ * 免得官方换个字段名就静默失效。
920
+ *
921
+ * @param store - 可能是 store、列表、数组或映射。
922
+ * @param workspaceId - 工作区 id。
923
+ * @returns 路径;没有就 `undefined`。
924
+ */
925
+ function pathFromWorkspaceStore(store, workspaceId) {
926
+ var items = workspaceItemsOf(store);
927
+ if (items === undefined) return undefined;
928
+ var found;
929
+ if (Array.isArray(items)) {
930
+ for (var at = 0; at < items.length; at += 1) {
931
+ var row = items[at];
932
+ if (!row) continue;
933
+ if (row.workspaceId === workspaceId || row.id === workspaceId) { found = row; break; }
934
+ }
935
+ } else {
936
+ found = items[workspaceId];
937
+ }
938
+ return found === undefined || found === null ? undefined : pathOfWorkspaceRow(found);
939
+ }
940
+
941
+ /**
942
+ * 从会话 id 反查它所属工作区的路径 —— **同步**,读不到就 `undefined`。
943
+ *
944
+ * 比「先拿 `row.workspaceId` 再查工作区」更直接:新建对话那一行**连
945
+ * `workspaceId` 都可能还没填**,但它一定已经出现在某个工作区的
946
+ * `sessionIds` 里(官方建会话时先挂到工作区,再物化 cwd)。
947
+ *
948
+ * ⚠️ 一个会话理论上只属于一个工作区,所以命中即返回,不用遍历完。
949
+ *
950
+ * @param ctx - client 根上下文。
951
+ * @param sessionId - 会话 id。
952
+ * @returns 所属工作区路径;查不到时 `undefined`。
953
+ */
954
+ function workspacePathOfSession(ctx, sessionId) {
955
+ if (typeof sessionId !== 'string' || sessionId === '') return undefined;
956
+ var hosts = workspaceHostsOf(ctx);
957
+ for (var ht = 0; ht < hosts.length; ht += 1) {
958
+ var host = hosts[ht];
959
+ var stores = [host.list, host.workspaces, host];
960
+ for (var st = 0; st < stores.length; st += 1) {
961
+ var items = workspaceItemsOf(stores[st]);
962
+ if (items === undefined || !Array.isArray(items)) continue;
963
+ for (var at = 0; at < items.length; at += 1) {
964
+ var row = items[at];
965
+ if (!row || !Array.isArray(row.sessionIds)) continue;
966
+ if (row.sessionIds.indexOf(sessionId) === -1) continue;
967
+ var value = pathOfWorkspaceRow(row);
968
+ if (value !== undefined) return value;
969
+ }
970
+ }
971
+ }
972
+ return undefined;
973
+ }
974
+
975
+ /**
976
+ * 从一个「工作区 store / 列表 / 数组」里取出 item 集合。
977
+ *
978
+ * @param store - 可能是 store、列表、数组或映射。
979
+ * @returns 数组或 `{id: row}` 映射;取不到就 `undefined`。
980
+ */
981
+ function workspaceItemsOf(store) {
982
+ if (store === undefined || store === null) return undefined;
983
+ var list;
984
+ if (typeof store.getSnapshot === 'function') {
985
+ try { list = store.getSnapshot(); } catch (error) { return undefined; }
986
+ } else if (typeof store.list === 'function') {
987
+ try { list = store.list(); } catch (error) { return undefined; }
988
+ } else if (Array.isArray(store)) {
989
+ list = store;
990
+ } else {
991
+ return undefined;
992
+ }
993
+ if (list === undefined || list === null) return undefined;
994
+ // 官方快照是 `{items, …}`;也兼容直接给数组或 `{byId}`。
995
+ if (Array.isArray(list)) return list;
996
+ if (Array.isArray(list.items)) return list.items;
997
+ if (list.byId !== undefined && list.byId !== null) return list.byId;
998
+ return undefined;
999
+ }
1000
+
1001
+ /**
1002
+ * 取一行工作区的路径(按几个候选字段名试)。
1003
+ *
1004
+ * @param row - 工作区行。
1005
+ * @returns 路径;没有就 `undefined`。
1006
+ */
1007
+ function pathOfWorkspaceRow(row) {
1008
+ if (row === undefined || row === null) return undefined;
1009
+ var keys = ['path', 'cwd', 'root', 'directory'];
1010
+ for (var kt = 0; kt < keys.length; kt += 1) {
1011
+ var value = row[keys[kt]];
1012
+ if (typeof value === 'string' && value !== '') return value;
1013
+ }
1014
+ return undefined;
1015
+ }
1016
+
736
1017
  /**
737
1018
  * 调宿主桥的一条 RPC(令牌只在引擎进程里,浏览器侧不持有)。
738
1019
  *
@@ -1021,6 +1302,10 @@ window.__ModuleLoader__.load({
1021
1302
  /** 「手机」页签:采纳为本地工作区。 */
1022
1303
  var pickLocal = function () {
1023
1304
  if (loc.path === '') return;
1305
+ // ⚠️ **必须清掉电脑那条兜底**(见 `pendingWorkspaceCwd`):用户从
1306
+ // 电脑工作区切回手机默认工作区时,若不清,模型/预设会继续按
1307
+ // 「电脑」筛 —— 正是用户报的「切回默认工作区,预设和模型都没改回来」。
1308
+ setPendingWorkspace(null);
1024
1309
  onPicked(loc.path);
1025
1310
  };
1026
1311
 
@@ -1308,6 +1593,13 @@ window.__ModuleLoader__.load({
1308
1593
  // ⚠️ **先登记、再打开**,顺序反了就会静默失败(见 `registerLocal`)。
1309
1594
  return settleLocal(result).then(function () {
1310
1595
  if (!aliveRef.current) return;
1596
+ // ⚠️ 会话行还没带上 cwd 之前,模型/预设菜单靠这个值判断
1597
+ // 「这是电脑工作区」(见 `pendingWorkspaceCwd`)。不设的话,
1598
+ // 新建出来的电脑会话在那一小段窗口里会被判成本地,
1599
+ // 模型菜单显示手机模型 —— 用户报障就是这个。
1600
+ if (result && typeof result.cwd === 'string' && result.cwd !== '') {
1601
+ setPendingWorkspace(result.cwd);
1602
+ }
1311
1603
  patch({ creating: false });
1312
1604
  onCancel();
1313
1605
  if (openSession !== undefined && result && result.sessionId) {
@@ -1749,7 +2041,7 @@ window.__ModuleLoader__.load({
1749
2041
  //#endregion
1750
2042
 
1751
2043
  /** 需要宿主提供的服务:slot 注册表、工作区浏览与导航、会话数据。 */
1752
- const inject = ['slots', 'uiWorkspace', 'sessions'];
2044
+ const inject = ['slots', 'uiWorkspace', 'sessions', 'workspaces'];
1753
2045
 
1754
2046
  /**
1755
2047
  * Client 插件主体:注册「手机 / 电脑」双页签的目录选择对话框 + 订阅流式帧。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-jiuling-remote-mobile",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "DSH 远程目录(手机端):在选文件夹流程里多出「电脑」页签,进入电脑目录、在电脑上发起对话,并把电脑的实时消息流同步到手机本地界面(只传文本,不映射桌面)。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",