dsh-jiuling-remote-mobile 1.0.16 → 1.0.18

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
@@ -230,6 +230,37 @@ window.__ModuleLoader__.load({
230
230
  document.head.appendChild(styleTag);
231
231
  }
232
232
 
233
+ /**
234
+ * 电脑目录的**访问缓存**:`电脑路径 → listing`(模块级,跨对话框开关存活)。
235
+ *
236
+ * ## 为什么必须有(用户报障原文)
237
+ *
238
+ * 「为什么我点击添加工作区的时候,每次点击电脑都要重新加载一遍目录,
239
+ * 这就算了,我加载好之后,随便点击一个文件夹,文件夹里面的内容还要重新加载
240
+ * 一遍」—— 同一个电脑目录,每次进「电脑」页签、每次点进同一个文件夹,
241
+ * 都要重走一趟「手机 → Cloudflare 隧道 → 电脑」,一个来回 1~3 秒。
242
+ * 用户要的是「加载一次,后续直接从本地读」。
243
+ *
244
+ * ## 为什么放模块级而不是组件内 ref
245
+ *
246
+ * 对话框每次打开都是**新挂载的组件**,组件内的 ref 一关就没了 —— 那样
247
+ * 「关掉再打开又转一遍圈」,正是用户抱怨的第一个现象。放模块级则一次加载
248
+ * 整条会话内都命中。
249
+ *
250
+ * ⚠️ 键是**电脑路径**原样(`C:\Users\...`),`''` 表示根目录 —— 与
251
+ * `bridge('listDesktop')` 的入参完全一致,不做任何改写。
252
+ *
253
+ * ⚠️ **失效只有两条路**:用户点「刷新」(见 `refreshAll`),
254
+ * 或者那一次加载失败(见下面的 catch)。不做 TTL 轮询 —— 用户明确要的是
255
+ * 「加载一次就直接从本地读」。
256
+ */
257
+ var desktopDirCache = {};
258
+
259
+ /** 清掉访问缓存(配对 / 解绑 / 切页签时调用,避免跨电脑串味)。 */
260
+ function clearDesktopDirCache() {
261
+ desktopDirCache = {};
262
+ }
263
+
233
264
  /**
234
265
  * 调宿主桥的一条 RPC(令牌只在引擎进程里,浏览器侧不持有)。
235
266
  *
@@ -384,6 +415,38 @@ window.__ModuleLoader__.load({
384
415
  };
385
416
  }, []);
386
417
 
418
+ /**
419
+ * 让官方前端的模型目录**失效重载**。
420
+ *
421
+ * ## ⚠️ 这一步为什么不能省
422
+ *
423
+ * 官方 `ModelCatalogDirectory` 在页面加载时就 `load()` 一次并进入
424
+ * `status === 'ready'`,此后**点菜单一个请求都不发**
425
+ * (`dsh-client-ui-model-selection/lib/client.js:100`:ready 时直接
426
+ * 返回旧值)。所以「配对之后模型列表变成电脑的」这件事,只把宿主那边的
427
+ * 缓存换掉是**看不见的** —— 前端那一层永远捧着配对前拉到的手机列表。
428
+ *
429
+ * 这正是用户报的「预设列表正确地加载了电脑预设,但模型列表还是没同步」:
430
+ * 预设走 `remoteExportList`,**没有缓存层**、每次重读,所以它是对的。
431
+ *
432
+ * `modelDirectories` 是官方那个服务实例(`super(ctx, "modelDirectories")`),
433
+ * `.catalog.refresh()` 就是官方在 `llm/adapters-updated` 时走的同一条路 ——
434
+ * 走公开方法,不碰前端 bundle 内部类。
435
+ *
436
+ * `ctx.get` 拿不到服务时静默跳过(见 `injected()` 里 `modelDirectories`
437
+ * 的注释):最坏是回到「要点一下刷新按钮」的旧行为,不会报错。
438
+ */
439
+ var reloadModelCatalog = function () {
440
+ try {
441
+ var models = props.modelDirectories;
442
+ if (models && models.catalog && typeof models.catalog.refresh === 'function') {
443
+ models.catalog.refresh();
444
+ }
445
+ } catch (error) {
446
+ /* 服务结构变了就只更新宿主缓存,不打扰用户 */
447
+ }
448
+ };
449
+
387
450
  // 每次打开都回到「手机」页签,避免上次停在电脑目录造成困惑。
388
451
  react.useEffect(
389
452
  function () {
@@ -424,6 +487,16 @@ window.__ModuleLoader__.load({
424
487
  patch({ loading: false, listing: null, error: null });
425
488
  return undefined;
426
489
  }
490
+ // ⚠️ **电脑目录命中缓存就直接铺回去,不打桥**(用户报障:来回点文件夹
491
+ // 每次都重新加载)。缓存键用 `''` 表示根,与 `listDesktop` 的入参一致。
492
+ if (tab === 'desktop') {
493
+ var key = path === null ? '' : path;
494
+ var hit = desktopDirCache[key];
495
+ if (hit !== undefined) {
496
+ patch({ loading: false, listing: hit, error: null });
497
+ return undefined;
498
+ }
499
+ }
427
500
  patch({ loading: true, error: null });
428
501
  var load =
429
502
  tab === 'desktop'
@@ -432,10 +505,16 @@ window.__ModuleLoader__.load({
432
505
  load.then(
433
506
  function (listing) {
434
507
  if (!aliveRef.current) return;
508
+ if (tab === 'desktop') {
509
+ desktopDirCache[path === null ? '' : path] = listing;
510
+ }
435
511
  patch({ loading: false, listing: listing });
436
512
  },
437
513
  function (reason) {
438
514
  if (!aliveRef.current) return;
515
+ // ⚠️ 失败**要清掉这一格缓存**:留着的话用户再点进来会看到
516
+ // 上一次的旧内容,而以为自己看的是电脑现状。
517
+ if (tab === 'desktop') delete desktopDirCache[path === null ? '' : path];
439
518
  patch({
440
519
  loading: false,
441
520
  listing: null,
@@ -488,7 +567,14 @@ window.__ModuleLoader__.load({
488
567
  code: '',
489
568
  message: { kind: 'ok', text: '配对成功,正在连接电脑…' },
490
569
  });
570
+ // ⚠️ 换了一台电脑,旧缓存里的路径完全无效(甚至同名不同机)——
571
+ // 必须作废,否则用户会看到上一台电脑的目录。
572
+ clearDesktopDirCache();
491
573
  patch({ path: null, listing: null });
574
+ // ⚠️ 前端那份模型目录是**永久缓存**的(见 `reloadModelCatalog`),
575
+ // 不作废的话用户接下来看到的仍然是配对前那份手机模型列表 ——
576
+ // 就是用户报的「模型列表还是没同步」。
577
+ reloadModelCatalog();
492
578
  },
493
579
  function (reason) {
494
580
  if (!aliveRef.current) return;
@@ -520,6 +606,8 @@ window.__ModuleLoader__.load({
520
606
  code: '',
521
607
  message: { kind: 'ok', text: '已解绑,请到电脑上生成新的配对码' },
522
608
  });
609
+ // 解绑后旧凭据作废,缓存里的目录不该再露出来。
610
+ clearDesktopDirCache();
523
611
  patch({ path: null, listing: null });
524
612
  },
525
613
  function (reason) {
@@ -611,11 +699,16 @@ window.__ModuleLoader__.load({
611
699
  if (pairState.refreshing) return;
612
700
  patchPair({ refreshing: true, refreshNote: null });
613
701
  var target = state.tab === 'desktop' ? loc.path : '';
702
+ // ⚠️ **「刷新」必须清掉访问缓存**,否则新列表铺回 `listing` 之后,
703
+ // 用户一离开再回来又命中旧的那一格,看起来就是「刷新没生效」。
704
+ // 连同整棵已缓存的树一起作废:电脑上的目录可能已经变了。
705
+ clearDesktopDirCache();
614
706
  bridge('refreshAll', target === '' ? {} : { path: target }).then(
615
707
  function (result) {
616
708
  if (!aliveRef.current) return;
617
- // 目录树:直接把新列表铺回去(正在看的这一层)。
709
+ // 目录树:直接把新列表铺回去(正在看的这一层),并写回缓存。
618
710
  if (result && result.directory && result.directory.ok) {
711
+ desktopDirCache[target === '' ? '' : target] = result.directory.value;
619
712
  patch({ listing: result.directory.value });
620
713
  }
621
714
  // 模型:让官方目录失效重载,下一次打开菜单就是电脑的。
package/lib/index.js CHANGED
@@ -678,7 +678,7 @@ export function apply(ctx) {
678
678
  const watchers = new Set();
679
679
 
680
680
  /**
681
- * 电脑侧的静默缓存:`{ catalog, presets, at }`。
681
+ * 电脑侧的静默缓存:`{ catalog, presets, directory, at }`。
682
682
  *
683
683
  * ## 为什么要缓存(用户需求原文)
684
684
  *
@@ -686,10 +686,17 @@ export function apply(ctx) {
686
686
  * 以及目录树这样体验会好很多……因为一般项目目录是不会变的,可以提前加载」。
687
687
  *
688
688
  * 配对后立刻拉一次,之后前端一打开模型菜单就有内容,不用等一轮往返。
689
- * 目录树**不进这个缓存** —— 它按路径变化,缓存了反而会显示过期内容,
690
- * 由前端每次进目录时实时拉。
689
+ *
690
+ * ⚠️ **`directory` 只缓存「根」那一层**(`listDirectory` 不给 path 时返回的
691
+ * home),因为「点开电脑页签」是每次都逃不掉的第一步,而它现在必然要过一轮
692
+ * 隧道往返 —— 用户看到的就是那个转圈(原话:「还是有一个加载过程,
693
+ * 不能预加载吗」)。
694
+ *
695
+ * **整棵目录树不进缓存**:电脑上几万个目录,全拉一遍既慢又没意义,而且
696
+ * 会大面积显示过期内容。点进子目录仍然实时拉(前端另有 `desktopDirCache`
697
+ * 兜住「同一层来回点」)。根目录的过期窗口由「刷新」按钮关闭。
691
698
  */
692
- const desktopCache = { catalog: null, presets: null, at: 0 };
699
+ const desktopCache = { catalog: null, presets: null, directory: null, at: 0 };
693
700
 
694
701
  /**
695
702
  * 静默预拉电脑的模型目录与预设列表。
@@ -703,7 +710,7 @@ export function apply(ctx) {
703
710
  */
704
711
  const prefetchDesktop = async (why) => {
705
712
  if (config.token === EMPTY_TOKEN) return;
706
- const [catalog, presets] = await Promise.all([
713
+ const [catalog, presets, directory] = await Promise.all([
707
714
  callRemote(config, 'modelCatalog', {}).catch((error) => {
708
715
  ctx.logger?.warn?.(`[dsh-remote-mobile] 预拉模型列表失败(${why}): ${String(error?.message ?? error)}`);
709
716
  return null;
@@ -712,16 +719,49 @@ export function apply(ctx) {
712
719
  ctx.logger?.warn?.(`[dsh-remote-mobile] 预拉预设列表失败(${why}): ${String(error?.message ?? error)}`);
713
720
  return null;
714
721
  }),
722
+ // 根目录那一层:不带 path ⇒ 电脑返回它的 home,正是用户点开
723
+ // 「电脑」页签第一眼看到的东西。见 `desktopCache.directory` 的注释。
724
+ callRemote(config, 'listDirectory', {}).catch((error) => {
725
+ ctx.logger?.warn?.(`[dsh-remote-mobile] 预拉目录树失败(${why}): ${String(error?.message ?? error)}`);
726
+ return null;
727
+ }),
715
728
  ]);
716
729
  if (catalog !== null) desktopCache.catalog = catalog;
717
730
  if (presets !== null) desktopCache.presets = presets;
731
+ if (directory !== null) desktopCache.directory = directory;
718
732
  desktopCache.at = Date.now();
719
733
  const models = catalog?.groups?.reduce((n, g) => n + (g.models?.length ?? 0), 0) ?? 0;
734
+ const dirs = directory?.entries?.length ?? directory?.items?.length ?? 0;
720
735
  ctx.logger?.info?.(
721
- `[dsh-remote-mobile] 已预拉电脑数据(${why}):${models} 个模型、${presets?.presets?.length ?? 0} 个预设`,
736
+ `[dsh-remote-mobile] 已预拉电脑数据(${why}):${models} 个模型、${presets?.presets?.length ?? 0} 个预设、根目录 ${dirs} 项`,
722
737
  );
723
738
  };
724
739
 
740
+ /**
741
+ * 取电脑的模型目录,缓存为空就**当场拉一次**。
742
+ *
743
+ * ⚠️ **不能只依赖配对时的预拉**。那是异步的后台动作,而前端页面一加载
744
+ * 就会调一次 `modelCatalog`(`ModelCatalogDirectory` 构造后立刻 `load()`,
745
+ * 见 `dsh-client-ui-model-selection/lib/client.js:354`)—— 谁先到谁决定
746
+ * 用户看到什么。拿不到电脑那份就返回手机本地列表,症状正是用户报的
747
+ * 「预设列表正确地加载了电脑预设,但模型列表还是没同步」:预设走
748
+ * `remoteExportList`,**没有缓存层**、每次重读,所以它是对的。
749
+ *
750
+ * @returns 电脑的模型目录;拉不到时 `null`。
751
+ */
752
+ const desktopCatalog = async () => {
753
+ if (desktopCache.catalog !== null) return desktopCache.catalog;
754
+ try {
755
+ desktopCache.catalog = await callRemote(config, 'modelCatalog', {});
756
+ return desktopCache.catalog;
757
+ } catch (error) {
758
+ ctx.logger?.warn?.(
759
+ `[dsh-remote-mobile] 取电脑模型列表失败: ${String(error?.message ?? error)}`,
760
+ );
761
+ return null;
762
+ }
763
+ };
764
+
725
765
  /**
726
766
  * 把电脑的模型目录并进手机本地的目录。
727
767
  *
@@ -1152,8 +1192,10 @@ export function apply(ctx) {
1152
1192
  const originalCatalog = controller.modelCatalog.bind(controller);
1153
1193
  controller.modelCatalog = async (...args) => {
1154
1194
  const local = await originalCatalog(...args);
1155
- if (!paired() || desktopCache.catalog === null) return local;
1156
- return mergeCatalog(local, desktopCache.catalog);
1195
+ if (!paired()) return local;
1196
+ const desktop = await desktopCatalog();
1197
+ if (desktop === null) return local;
1198
+ return mergeCatalog(local, desktop);
1157
1199
  };
1158
1200
  undo.push(() => {
1159
1201
  controller.modelCatalog = originalCatalog;
@@ -1379,6 +1421,10 @@ export function apply(ctx) {
1379
1421
  // 解绑要能停掉全部旧连接、清掉全部搬运状态(见 `unpair`)。
1380
1422
  follows,
1381
1423
  mirrorStates,
1424
+ // ⚠️ **必须显式传进来**:`handleBridge` 是模块级函数,作用域里
1425
+ // 没有 `apply()` 的闭包变量;漏传就是 `workspaceMap is not defined`,
1426
+ // 用户点「在此新建对话」时必然失败(1.0.16 的真实报障)。
1427
+ workspaceMap,
1382
1428
  persist: () => saveRemoteSessions(remoteSessions, ctx.logger),
1383
1429
  // 模型/预设的静默缓存:配对后预拉、刷新按钮回写(见 desktopCache)。
1384
1430
  desktopCache,
@@ -1556,7 +1602,7 @@ function desktopPathOfMirror(workspaceMap, dir) {
1556
1602
  *
1557
1603
  * @param ctx - Cordis 上下文。
1558
1604
  * @param config - 远程配置。
1559
- * @param io - `{ remoteSessions, follow, follows, mirrorStates, persist, body }`。
1605
+ * @param io - `{ remoteSessions, follow, follows, mirrorStates, persist, workspaceMap, body }`。
1560
1606
  */
1561
1607
  async function handleBridge(ctx, config, io) {
1562
1608
  const params = io.body?.params ?? {};
@@ -1565,9 +1611,24 @@ async function handleBridge(ctx, config, io) {
1565
1611
  case 'ping':
1566
1612
  return callRemote(config, 'ping', {});
1567
1613
 
1568
- /** 列电脑上的目录。 */
1569
- case 'listDesktop':
1570
- return callRemote(config, 'listDirectory', { path: params.path });
1614
+ /**
1615
+ * 列电脑上的目录。
1616
+ *
1617
+ * ⚠️ **根那层命中预拉缓存就直接回,不打隧道**:用户点开「电脑」页签时
1618
+ * 看的正是这一层,而它每一轮往返都要穿过 Cloudflare 隧道 ——
1619
+ * 那就是用户抱怨的那个「还是有一个加载过程」(原话:「目录树都提前存档
1620
+ * 手机本地了,为什么还是还加载呢,不能预加载吗」)。
1621
+ *
1622
+ * 只在**没给 path**(= 要 home)且预拉成功过时才走缓存。子目录照旧实时拉,
1623
+ * 前端 `desktopDirCache` 负责「同一层来回点」,两边分工见各自的注释。
1624
+ */
1625
+ case 'listDesktop': {
1626
+ const wantRoot = params.path === undefined || params.path === null || params.path === '';
1627
+ if (wantRoot && io.desktopCache?.directory != null) return io.desktopCache.directory;
1628
+ const listing = await callRemote(config, 'listDirectory', { path: params.path });
1629
+ if (wantRoot && io.desktopCache !== undefined) io.desktopCache.directory = listing;
1630
+ return listing;
1631
+ }
1571
1632
 
1572
1633
  /**
1573
1634
  * 电脑上的模型目录。
@@ -1611,9 +1672,23 @@ async function handleBridge(ctx, config, io) {
1611
1672
  // ⚠️ 刷新到的模型/预设要**写回缓存**,否则「刷新按钮」只更新了它自己
1612
1673
  // 那份响应,前端模型菜单读的是宿主拦截器里的缓存 —— 用户会看到
1613
1674
  // 「点了刷新,模型列表没变」。
1675
+ //
1676
+ // 根目录那一层同理:刷完不回写的话,用户接下来点开「电脑」页签
1677
+ // 命中的还是**刷新前**那份预拉缓存,看起来就是「刷新没生效」。
1678
+ //
1679
+ // ⚠️ **判据是「拉回来的这一层就是缓存的那一层」**(同 path),
1680
+ // 不是「有没有带 path」:前端在根层点刷新时传的是当前显示的绝对路径
1681
+ // (`loc.path`,也就是 home),照样是带 path 的一次调用 —— 只看
1682
+ // `params.path` 空不空会把根层刷新漏掉。
1614
1683
  if (io.desktopCache !== undefined) {
1615
1684
  if (result.models?.ok === true) io.desktopCache.catalog = result.models.value;
1616
1685
  if (result.presets?.ok === true) io.desktopCache.presets = result.presets.value;
1686
+ if (
1687
+ result.directory?.ok === true &&
1688
+ io.desktopCache.directory?.path === result.directory.value?.path
1689
+ ) {
1690
+ io.desktopCache.directory = result.directory.value;
1691
+ }
1617
1692
  io.desktopCache.at = Date.now();
1618
1693
  }
1619
1694
  return result;
@@ -1640,7 +1715,7 @@ async function handleBridge(ctx, config, io) {
1640
1715
  // 这里给它一个**电脑目录专属的镜像工作区**(标题 = 电脑目录末段),
1641
1716
  // 而不是手机的「默认工作区」—— 否则所有远程会话都堆在「默认工作区」
1642
1717
  // 底下,看起来就像「建完又回到默认工作去了」(见 `ensureDesktopWorkspace`)。
1643
- const workspace = await ensureDesktopWorkspace(ctx, params.path, workspaceMap);
1718
+ const workspace = await ensureDesktopWorkspace(ctx, params.path, io.workspaceMap);
1644
1719
  const cwd = workspace.path;
1645
1720
  // ⚠️ **必须走 `sessionController.create`,不能走 `ctx.sessions.create`。**
1646
1721
  //
@@ -1770,4 +1845,4 @@ async function handleBridge(ctx, config, io) {
1770
1845
  }
1771
1846
  }
1772
1847
 
1773
- export { callRemote, mirrorEvent, loadConfig, DEFAULT_ENDPOINT, CONFIG_PATH, PAIR_ENDPOINT, claimPairing };
1848
+ export { callRemote, mirrorEvent, loadConfig, DEFAULT_ENDPOINT, CONFIG_PATH, PAIR_ENDPOINT, claimPairing, handleBridge };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-jiuling-remote-mobile",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "DSH 远程目录(手机端):在选文件夹流程里多出「电脑」页签,进入电脑目录、在电脑上发起对话,并把电脑的实时消息流同步到手机本地界面(只传文本,不映射桌面)。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",