dsh-multi-chat 0.5.2 → 0.6.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.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "DSH 多对话:在官方 DeepSeek Harness Web 界面里并排运行和监控多个对话实例(多窗口墙),并内置带口令认证的手机访问网关。npx dsh-multi-chat install|start|stop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -242,27 +242,27 @@ window.__ModuleLoader__.load({
242
242
  document.head.appendChild(tag);
243
243
  }
244
244
  var WallView_module_css_default = {
245
- "danger": "ouhzTG_danger",
246
- "action": "ouhzTG_action",
247
- "confirm": "ouhzTG_confirm",
248
- "field": "ouhzTG_field",
245
+ "paneActions": "ouhzTG_paneActions",
246
+ "linkBar": "ouhzTG_linkBar",
249
247
  "wall": "ouhzTG_wall",
250
- "grid": "ouhzTG_grid",
248
+ "action": "ouhzTG_action",
249
+ "danger": "ouhzTG_danger",
250
+ "paneBody": "ouhzTG_paneBody",
251
+ "title": "ouhzTG_title",
251
252
  "hint": "ouhzTG_hint",
253
+ "empty": "ouhzTG_empty",
254
+ "zoomed": "ouhzTG_zoomed",
255
+ "field": "ouhzTG_field",
256
+ "paneHead": "ouhzTG_paneHead",
252
257
  "status": "ouhzTG_status",
253
258
  "paneTitle": "ouhzTG_paneTitle",
254
- "controls": "ouhzTG_controls",
255
- "linkText": "ouhzTG_linkText",
256
- "paneHead": "ouhzTG_paneHead",
257
- "paneBody": "ouhzTG_paneBody",
258
- "empty": "ouhzTG_empty",
259
+ "grid": "ouhzTG_grid",
259
260
  "dot": "ouhzTG_dot",
260
- "toolbar": "ouhzTG_toolbar",
261
- "linkBar": "ouhzTG_linkBar",
262
- "title": "ouhzTG_title",
263
261
  "pane": "ouhzTG_pane",
264
- "paneActions": "ouhzTG_paneActions",
265
- "zoomed": "ouhzTG_zoomed"
262
+ "confirm": "ouhzTG_confirm",
263
+ "toolbar": "ouhzTG_toolbar",
264
+ "controls": "ouhzTG_controls",
265
+ "linkText": "ouhzTG_linkText"
266
266
  };
267
267
  //#endregion
268
268
  //#region src/client/WallView.tsx
@@ -294,6 +294,27 @@ window.__ModuleLoader__.load({
294
294
  /** Embed flag appended to every pane URL; such pages register no wall UI. */
295
295
  const EMBED_FLAG = "multi-wall=embed";
296
296
  /**
297
+ * Whether this wall is being viewed through the phone gateway (a non-loopback
298
+ * host) rather than on the machine running DSH. When remote, pane iframes must
299
+ * load through the gateway's `/gw/<port>` route — a phone's `127.0.0.1` points
300
+ * at the phone itself, not the host.
301
+ */
302
+ function isRemoteViewer() {
303
+ const host = typeof window !== "undefined" ? window.location.hostname : "";
304
+ return host !== "localhost" && host !== "127.0.0.1" && host !== "::1";
305
+ }
306
+ /**
307
+ * The iframe URL for a pane. Local viewers embed the loopback instance
308
+ * directly; remote (phone) viewers route through the gateway that is already
309
+ * serving this page, appending the port so the gateway proxies to it.
310
+ * @param port - target DSH instance port.
311
+ * @returns the pane URL.
312
+ */
313
+ function paneUrl(port) {
314
+ if (isRemoteViewer()) return `${window.location.origin}/gw/${port}/?${EMBED_FLAG}`;
315
+ return `http://127.0.0.1:${port}/?${EMBED_FLAG}`;
316
+ }
317
+ /**
297
318
  * One pane: header (port, liveness dot, zoom/refresh/open/stop/remove) plus
298
319
  * the embedded original DSH UI.
299
320
  */
@@ -338,7 +359,7 @@ window.__ModuleLoader__.load({
338
359
  className: WallView_module_css_default.action,
339
360
  title: t("openTab"),
340
361
  onClick: () => {
341
- window.open(`http://127.0.0.1:${port}/`, "_blank");
362
+ window.open(paneUrl(port), "_blank");
342
363
  },
343
364
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, { size: 14 })
344
365
  }),
@@ -363,7 +384,7 @@ window.__ModuleLoader__.load({
363
384
  className: WallView_module_css_default.paneBody,
364
385
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
365
386
  title: `DSH :${port}`,
366
- src: `http://127.0.0.1:${port}/?${EMBED_FLAG}`,
387
+ src: paneUrl(port),
367
388
  loading: "lazy"
368
389
  })
369
390
  })]
@@ -660,8 +681,8 @@ window.__ModuleLoader__.load({
660
681
  document.head.appendChild(tag);
661
682
  }
662
683
  var WallToggle_module_css_default = {
663
- "label": "bmjS6q_label",
664
- "row": "bmjS6q_row"
684
+ "row": "bmjS6q_row",
685
+ "label": "bmjS6q_label"
665
686
  };
666
687
  //#endregion
667
688
  //#region src/client/WallToggle.tsx
@@ -170,11 +170,13 @@ function injectPolyfill(body) {
170
170
  * `127.0.0.1:<target-port>` so the DSH `/api` browser-trust fence sees a local
171
171
  * request. Non-upgrade requests get `connection: close` (fresh connection per
172
172
  * request keeps keep-alive from smuggling unrewritten Host headers); upgrades
173
- * (WebSocket) keep `connection: Upgrade`.
173
+ * (WebSocket) keep `connection: Upgrade`. When `pathOverride` is set (a
174
+ * `/gw/<port>` route), the request line's path becomes the de-prefixed path.
174
175
  */
175
- function rewriteHead(parsed, targetPort) {
176
+ function rewriteHead(parsed, targetPort, pathOverride) {
176
177
  const upgrade = parsed.map.upgrade !== void 0;
177
- const lines = [`${parsed.method} ${parsed.target} ${parsed.version}`];
178
+ const requestTarget = pathOverride !== void 0 ? pathOverride : parsed.target;
179
+ const lines = [`${parsed.method} ${requestTarget} ${parsed.version}`];
178
180
  for (const [name, value] of parsed.headers) if (name === "host") lines.push(`host: 127.0.0.1:${targetPort}`);
179
181
  else if (name === "origin") lines.push(`origin: http://127.0.0.1:${targetPort}`);
180
182
  else if (name === "connection" || name === "proxy-connection") {} else lines.push(`${name}: ${value}`);
@@ -187,7 +189,8 @@ function rewriteHead(parsed, targetPort) {
187
189
  * @returns a handle with the assigned port and token, and a close().
188
190
  */
189
191
  function startGateway(options) {
190
- const { targetPort, port: requestedPort, token, name, maxAgeHours = 12, log = () => {} } = options;
192
+ const { targetPort, port: requestedPort, token, name, maxAgeHours = 12, routedPorts, log = () => {} } = options;
193
+ const allowedRouted = new Set(routedPorts ?? []);
191
194
  const hmac = (data) => createHmac("sha256", token).update(data).digest("hex");
192
195
  const signSession = (exp) => {
193
196
  const payload = Buffer.from(JSON.stringify({ exp })).toString("base64url");
@@ -412,6 +415,19 @@ connection: close\r
412
415
  handleLogin(socket, parsed, buffer.subarray(parsed.headBytes));
413
416
  return;
414
417
  }
418
+ let routePort = null;
419
+ let routePath;
420
+ const routeMatch = /^\/gw\/(\d+)(\/.*)?$/.exec(path);
421
+ if (routeMatch !== null) {
422
+ const candidate = Number(routeMatch[1]);
423
+ if (!Number.isInteger(candidate) || candidate <= 0 || !allowedRouted.has(candidate)) {
424
+ respondPlain(socket, "403", "port not routable");
425
+ return;
426
+ }
427
+ routePort = candidate;
428
+ routePath = routeMatch[2] && routeMatch[2] !== "" ? routeMatch[2] : "/";
429
+ }
430
+ const effectivePort = routePort ?? targetPort;
415
431
  const kind = authKind(parsed);
416
432
  if (kind === "none") {
417
433
  respondLogin(socket, name, path === "/" ? "/" : path);
@@ -421,19 +437,19 @@ connection: close\r
421
437
  respondSessionRedirect(socket, parsed, `dsh_gw_session=${signSession(Date.now() + maxAgeHours * 36e5)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAgeHours * 3600}`);
422
438
  return;
423
439
  }
424
- const target = connect(targetPort, "127.0.0.1");
440
+ const target = connect(effectivePort, "127.0.0.1");
425
441
  const rest = buffer.subarray(parsed.headBytes);
426
442
  let connected = false;
427
443
  target.on("connect", () => {
428
444
  connected = true;
429
- target.write(rewriteHead(parsed, targetPort));
445
+ target.write(rewriteHead(parsed, effectivePort, routePath));
430
446
  if (rest.length > 0) target.write(rest);
431
447
  proxying = true;
432
448
  socket.pipe(target);
433
449
  relayTarget(socket, target);
434
450
  });
435
451
  target.on("error", (error) => {
436
- if (!connected) respondPlain(socket, "502", `cannot reach target 127.0.0.1:${targetPort}: ${error.message}`);
452
+ if (!connected) respondPlain(socket, "502", `cannot reach target 127.0.0.1:${effectivePort}: ${error.message}`);
437
453
  socket.destroy();
438
454
  });
439
455
  socket.on("error", () => target.destroy());
@@ -817,7 +833,8 @@ function lanAddresses() {
817
833
  if (virtual) continue;
818
834
  candidates.push({
819
835
  address: iface.address,
820
- physical
836
+ physical,
837
+ virtual: false
821
838
  });
822
839
  }
823
840
  }
@@ -960,11 +977,15 @@ function apply(ctx, config = {}) {
960
977
  const token = config.gatewayToken && config.gatewayToken !== "" ? config.gatewayToken : randomBytes(6).toString("hex");
961
978
  const gatewayPort = config.gatewayPort && config.gatewayPort !== 0 ? config.gatewayPort : port + 5e3;
962
979
  gatewayTargetPort = port;
980
+ const routed = [];
981
+ for (let p = scanFrom; p <= scanTo; p++) routed.push(p);
982
+ for (const p of fixedPorts) if (!routed.includes(p)) routed.push(p);
963
983
  return startGateway({
964
984
  targetPort: port,
965
985
  port: gatewayPort,
966
986
  token,
967
987
  name: "DSH",
988
+ routedPorts: routed,
968
989
  log: (msg) => ctx.logger.info(`multi-wall gateway: ${msg}`)
969
990
  }).then((handle) => {
970
991
  gateway = handle;