dsh-multi-chat 0.4.3 → 0.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-multi-chat",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "DSH 多对话:在官方 DeepSeek Harness Web 界面里并排运行和监控多个对话实例(多窗口墙),并内置带口令认证的手机访问网关。npx dsh-multi-chat install|start|stop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,7 +6,7 @@ Multi-window wall plugin, browser half + node half: a grid of every running DSH
6
6
 
7
7
  The wall's business state is a single store (`dsh.multi-wall`): the discovered port list and the grid column count, persisted across view switches and reloads. Discovery, liveness, create, and stop all flow through the node half's read-only JSON routes — `/multi/api/ports` (auto-discovery, excluding nothing so the serving instance is also watchable), `/multi/api/status` (liveness of a specific port list), `/multi/api/stop` (terminate a chosen instance), `/multi/api/create` (start a fresh instance, surfacing the child's stderr on failure), and `/multi/api/link` (phone access).
8
8
 
9
- Phone/remote access: the official CLI forbids `--host 0.0.0.0` (it would expose remote code execution), so `/multi/api/link` lazily starts an **inline authenticated gateway** (raw `node:net` reverse proxy with an HMAC-signed session-cookie login, targets `127.0.0.1:<self-port>`, rewrites Host/Origin so the official `/api` browser-trust fence sees a local request, and passes WebSocket upgrades through). The route returns the LAN URLs plus the login token; the gateway falls back to an OS-assigned port when its intended port hits a Windows excluded range or is already bound.
9
+ Phone/remote access: the official CLI forbids `--host 0.0.0.0` (it would expose remote code execution), so `/multi/api/link` lazily starts an **inline authenticated gateway** (raw `node:net` reverse proxy with an HMAC-signed session-cookie login, targets `127.0.0.1:<self-port>`, rewrites Host/Origin so the official `/api` browser-trust fence sees a local request, and passes WebSocket upgrades through). The route returns the LAN URLs plus the login token. The returned addresses drop virtual NICs (VMware/VirtualBox/WSL/Docker/Hyper-V/VPN — unreachable from a phone) and put physical NICs (Wi-Fi/Ethernet) first; the gateway falls back to an OS-assigned port when its intended port hits a Windows excluded range or is already bound.
10
10
 
11
11
  The `/client` exports the plugin body (`apply`/`inject`), the `WallView`/`WallToggle` components, the wall store factory, and the injected probe-face types.
12
12
 
@@ -14,8 +14,8 @@ window.__ModuleLoader__.load({
14
14
  const zh = {
15
15
  "toggle": "多窗口",
16
16
  "toggle.aria": "打开或关闭多窗口",
17
- "view.multiWall": "多窗口墙",
18
- "overlay.title": "多窗口墙",
17
+ "view.multiWall": "多窗口",
18
+ "overlay.title": "多窗口",
19
19
  "overlay.close": "关闭",
20
20
  "scan": "发现实例",
21
21
  "scan.from": "起始端口",
@@ -25,7 +25,7 @@ window.__ModuleLoader__.load({
25
25
  "create.failed": "创建失败:{error}",
26
26
  "create.unknown": "未知原因",
27
27
  "exit": "退出",
28
- "exit.aria": "退出多窗口墙,返回对话",
28
+ "exit.aria": "退出多窗口,返回对话",
29
29
  "link": "手机访问",
30
30
  "link.aria": "获取手机访问链接",
31
31
  "link.fetching": "正在获取链接…",
@@ -60,8 +60,8 @@ window.__ModuleLoader__.load({
60
60
  const en = {
61
61
  "toggle": "Multi-Window",
62
62
  "toggle.aria": "Toggle multi-window",
63
- "view.multiWall": "Multi-Window Wall",
64
- "overlay.title": "Multi-Window Wall",
63
+ "view.multiWall": "Multi-Window",
64
+ "overlay.title": "Multi-Window",
65
65
  "overlay.close": "Close",
66
66
  "scan": "Discover",
67
67
  "scan.from": "Start port",
@@ -71,7 +71,7 @@ window.__ModuleLoader__.load({
71
71
  "create.failed": "Create failed: {error}",
72
72
  "create.unknown": "unknown reason",
73
73
  "exit": "Exit",
74
- "exit.aria": "Exit the wall and return to chat",
74
+ "exit.aria": "Exit multi-window and return to chat",
75
75
  "link": "Phone access",
76
76
  "link.aria": "Get phone access link",
77
77
  "link.fetching": "Fetching link…",
@@ -242,33 +242,33 @@ window.__ModuleLoader__.load({
242
242
  document.head.appendChild(tag);
243
243
  }
244
244
  var WallView_module_css_default = {
245
- "paneTitle": "ouhzTG_paneTitle",
246
- "hint": "ouhzTG_hint",
247
- "toolbar": "ouhzTG_toolbar",
245
+ "danger": "ouhzTG_danger",
246
+ "action": "ouhzTG_action",
248
247
  "confirm": "ouhzTG_confirm",
249
- "paneActions": "ouhzTG_paneActions",
250
- "linkBar": "ouhzTG_linkBar",
251
- "grid": "ouhzTG_grid",
252
- "paneHead": "ouhzTG_paneHead",
253
- "empty": "ouhzTG_empty",
254
- "title": "ouhzTG_title",
255
248
  "field": "ouhzTG_field",
256
- "controls": "ouhzTG_controls",
257
249
  "wall": "ouhzTG_wall",
258
- "zoomed": "ouhzTG_zoomed",
250
+ "grid": "ouhzTG_grid",
251
+ "hint": "ouhzTG_hint",
259
252
  "status": "ouhzTG_status",
260
- "pane": "ouhzTG_pane",
261
- "danger": "ouhzTG_danger",
262
- "paneBody": "ouhzTG_paneBody",
253
+ "paneTitle": "ouhzTG_paneTitle",
254
+ "controls": "ouhzTG_controls",
263
255
  "linkText": "ouhzTG_linkText",
256
+ "paneHead": "ouhzTG_paneHead",
257
+ "paneBody": "ouhzTG_paneBody",
258
+ "empty": "ouhzTG_empty",
264
259
  "dot": "ouhzTG_dot",
265
- "action": "ouhzTG_action"
260
+ "toolbar": "ouhzTG_toolbar",
261
+ "linkBar": "ouhzTG_linkBar",
262
+ "title": "ouhzTG_title",
263
+ "pane": "ouhzTG_pane",
264
+ "paneActions": "ouhzTG_paneActions",
265
+ "zoomed": "ouhzTG_zoomed"
266
266
  };
267
267
  //#endregion
268
268
  //#region src/client/WallView.tsx
269
269
  /**
270
270
  * WallView: the multi-window wall as a `conversation.view` ring entry. When
271
- * the header's "多窗口墙" tab is selected, the right panel swaps from the
271
+ * the header's "多窗口" tab is selected, the right panel swaps from the
272
272
  * chat to this view — a toolbar plus a grid of iframes, one pane per running
273
273
  * DSH instance (127.0.0.1:<port>). Pure additive UI; no existing slot is
274
274
  * replaced.
@@ -469,7 +469,10 @@ window.__ModuleLoader__.load({
469
469
  const copyFirstLink = async () => {
470
470
  if (linkInfo === null || linkInfo.lan.length === 0) return;
471
471
  try {
472
- await navigator.clipboard.writeText(linkInfo.lan[0] ?? "");
472
+ const base = linkInfo.lan[0] ?? "";
473
+ const token = linkInfo.token ?? "";
474
+ const url = token !== "" ? `${base}?token=${encodeURIComponent(token)}` : base;
475
+ await navigator.clipboard.writeText(url);
473
476
  setLinkCopied(true);
474
477
  } catch {
475
478
  setLinkCopied(false);
@@ -657,15 +660,15 @@ window.__ModuleLoader__.load({
657
660
  document.head.appendChild(tag);
658
661
  }
659
662
  var WallToggle_module_css_default = {
660
- "row": "bmjS6q_row",
661
- "label": "bmjS6q_label"
663
+ "label": "bmjS6q_label",
664
+ "row": "bmjS6q_row"
662
665
  };
663
666
  //#endregion
664
667
  //#region src/client/WallToggle.tsx
665
668
  /**
666
669
  * WallToggle: the sidebar-foot shortcut. Clicking it opens the wall view —
667
670
  * the `conversation.view` ring switches to the 'multi-wall' entry, which the
668
- * header renders as the "多窗口墙" tab. The click is a plain user-equivalent
671
+ * header renders as the "多窗口" tab. The click is a plain user-equivalent
669
672
  * activation: it finds the header's view-ring tab for this plugin's label and
670
673
  * clicks it, so the official view-ring state machine (the chat store's active
671
674
  * view field) performs the switch. No store is declared: the ring decides
@@ -607,13 +607,67 @@ async function startInstance(launcher, port, timeoutMs = 2e4) {
607
607
  }
608
608
  }
609
609
  /**
610
+ * Interface-name patterns that mark a *virtual* NIC (VM bridge, WSL,
611
+ * Docker, Hyper-V, VPN adapters, Loopback Pseudo-Instance, etc.). These
612
+ * interfaces are never reachable from a phone on the same LAN, so they are
613
+ * dropped from the link list entirely.
614
+ */
615
+ const VIRTUAL_IFACE_PATTERNS = [
616
+ /vEthernet/i,
617
+ /vmware/i,
618
+ /virtualbox/i,
619
+ /^vbox/i,
620
+ /wsl/i,
621
+ /docker/i,
622
+ /hyper-v/i,
623
+ /tap-windows/i,
624
+ /^tap/i,
625
+ /^tun/i,
626
+ /ppp/i,
627
+ /loopback/i,
628
+ /apipa/i,
629
+ /^utun/i,
630
+ /^awdl/i,
631
+ /^llw/i,
632
+ /^bridge/i,
633
+ /bluetooth/i
634
+ ];
635
+ /**
636
+ * Interface-name patterns that mark a *physical* NIC (Wi-Fi / Ethernet).
637
+ * Matches kept addresses are ordered before any unknown-but-surviving
638
+ * address so the phone-first address is the machine's real NIC.
639
+ */
640
+ const PHYSICAL_IFACE_PATTERNS = [
641
+ /^(wi-?fi|wlan|wireless)/i,
642
+ /^(eth(ernet)?|以太网|以太)/i,
643
+ /^(en|wan)[0-9]/i,
644
+ /^本地连接/i,
645
+ /^e[0-9]+$/i,
646
+ /^w[0-9]+$/i
647
+ ];
648
+ /**
610
649
  * The non-loopback IPv4 addresses of this machine (the LAN reachable URLs).
650
+ * Virtual NICs (VM/WSL/Docker/VPN/loopback pseudo) are filtered out; the
651
+ * remaining addresses are ordered with physical NICs (Wi-Fi/Ethernet) first
652
+ * so the phone shows the actually-reachable LAN address at the top.
611
653
  * @returns the address list (possibly empty).
612
654
  */
613
655
  function lanAddresses() {
614
- const out = [];
615
- for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) out.push(iface.address);
616
- return out;
656
+ const candidates = [];
657
+ for (const [name, ifaces] of Object.entries(networkInterfaces())) {
658
+ const virtual = VIRTUAL_IFACE_PATTERNS.some((re) => re.test(name));
659
+ const physical = !virtual && PHYSICAL_IFACE_PATTERNS.some((re) => re.test(name));
660
+ for (const iface of ifaces ?? []) {
661
+ if (iface.family !== "IPv4" || iface.internal) continue;
662
+ if (virtual) continue;
663
+ candidates.push({
664
+ address: iface.address,
665
+ physical
666
+ });
667
+ }
668
+ }
669
+ candidates.sort((a, b) => Number(b.physical) - Number(a.physical));
670
+ return candidates.map((c) => c.address);
617
671
  }
618
672
  /**
619
673
  * Register the probe routes. Everything lives under `/multi/api` so the