dsh-multi-chat 0.5.2 → 0.6.1
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/README.md +26 -5
- package/assets/01-windows-dual-chat.png +0 -0
- package/assets/02-ipad-dual-chat.png +0 -0
- package/assets/03-windows-triple-chat.png +0 -0
- package/package.json +5 -2
- package/plugin/dsh-client-ui-multi-wall/lib/client.js +40 -19
- package/plugin/dsh-client-ui-multi-wall/lib/index.js +29 -8
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# 💬 dsh-multi-chat —— 多对话,一屏驾驭
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/dsh-multi-chat"><img src="https://img.shields.io/npm/v/dsh-multi-chat" alt="npm version"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/dsh-multi-chat"><img src="https://img.shields.io/npm/dm/dsh-multi-chat" alt="npm downloads"></a>
|
|
6
|
+
<a href="https://github.com/daetz-coder/dsh-multi-chat/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="license"></a>
|
|
7
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/dsh-plugin-✓-brightgreen" alt="dsh-plugin"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
> **在 DeepSeek Harness 里同时开 N 个对话,并排盯住每一个 Agent 的实时进度,还能用手机/平板躺着看。** 一个浏览器,从「一次一个对话」升级成「全景多对话驾驶舱」。
|
|
4
11
|
|
|
5
12
|
给 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 官方 Web 界面装上一面**多窗口墙**:在一张网格里同时显示 N 个正在运行的 DSH 对话实例(每个实例独立跑一个任务),所有 Agent 的实时进度、对话、输出**一眼尽收**,不用在无数标签页/窗口之间切来切去。
|
|
6
13
|
|
|
@@ -8,7 +15,7 @@
|
|
|
8
15
|
|
|
9
16
|
| 能力 | 说明 |
|
|
10
17
|
|------|------|
|
|
11
|
-
| 📺
|
|
18
|
+
| 📺 **多窗口** | 侧边栏一键进入,右侧对话区原位变成窗口网格,一个端口一格,并排看全部任务 |
|
|
12
19
|
| 🔍 **自动发现** | 扫描端口区间自动发现正在运行的 DSH 实例,也可手动管理 |
|
|
13
20
|
| ➕ **一键新建窗口** | 墙内直接启动全新 DSH 实例,凑成你的多对话矩阵 |
|
|
14
21
|
| 📱 **手机访问** | 点「手机访问」自动起一个**内置带口令认证的局域网网关**,手机打开 URL、输入口令即可看进度 |
|
|
@@ -16,6 +23,20 @@
|
|
|
16
23
|
|
|
17
24
|
> **多对话 = 多端口。** 启动 N 个 `dsh web --port <n>`,每个实例独立跑一个对话/任务;在任意一个实例里打开多窗口墙,即可并排看到全部。
|
|
18
25
|
|
|
26
|
+
## 📸 运行效果
|
|
27
|
+
|
|
28
|
+
**🖥️ Windows · 双对话并排** —— 两个正在运行的 DSH 实例并排列出,每格都是完整的官方对话界面,带实时在线状态点与单窗控制(放大 / 刷新 / 新标签页 / 移除):
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
**📱 iPad · 双对话移动端** —— 同一局域网内,iPad 打开带口令认证的网关地址,即可在平板上一屏并排盯住两个 Agent 的实时进度:
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
**🖥️ Windows · 三对话全景** —— 3 列网格并排显示 3 个正在运行的实例,一屏尽收全部 Agent,把「一次一个对话」升级成「全景多对话驾驶舱」:
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
19
40
|
## 🚀 30 秒上手
|
|
20
41
|
|
|
21
42
|
```bash
|
|
@@ -25,7 +46,7 @@ npx dsh-multi-chat install
|
|
|
25
46
|
# 2. 启动几个实例
|
|
26
47
|
npx dsh-multi-chat start --ports 3080,3081,3082
|
|
27
48
|
|
|
28
|
-
# 3.
|
|
49
|
+
# 3. 打开任意实例,点侧边栏底部「多窗口」→ 完成 🎉
|
|
29
50
|
```
|
|
30
51
|
|
|
31
52
|
## 为什么这样做
|
|
@@ -60,7 +81,7 @@ harness-src/ # 官方 deepseek-harness 源码(开发/构
|
|
|
60
81
|
|
|
61
82
|
# 2) 重启 dsh web,打开任意实例
|
|
62
83
|
dsh web --port 3084
|
|
63
|
-
# 浏览器打开 http://127.0.0.1:3084
|
|
84
|
+
# 浏览器打开 http://127.0.0.1:3084 ,侧边栏底部出现「多窗口」按钮
|
|
64
85
|
```
|
|
65
86
|
|
|
66
87
|
或手动:
|
|
@@ -74,7 +95,7 @@ dsh plugin --profile web add <tarball> # 装进 profile
|
|
|
74
95
|
## 使用
|
|
75
96
|
|
|
76
97
|
1. 先启动若干实例:`.\scripts\start-multi.ps1 -Ports "3080,3081,3082,3084"`(或手动 `dsh web --port <n>`)。
|
|
77
|
-
2.
|
|
98
|
+
2. 打开任意实例,点侧边栏底部的「多窗口」快捷入口(或点对话区头部的「多窗口」标签页)。
|
|
78
99
|
3. 墙视图内:自动发现实例(自动排除自身端口)、列数切换(自动/1/2/3/4/6,默认横向铺满)、点标题放大、⟳ 单独刷新、↗ 新标签页打开、✕ 从视图移除、全部刷新、实时在线状态点。布局保存在 localStorage。
|
|
79
100
|
4. 退出墙:点工具栏**右上角的「退出」按钮**,一键切回对话视图。
|
|
80
101
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-multi-chat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 多对话:在官方 DeepSeek Harness Web
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "DSH 多对话:在官方 DeepSeek Harness Web 界面里以多窗口网格并排运行、监控多个对话实例,一屏盯住所有 Agent 的实时进度;内置带口令认证的局域网网关,手机/平板也能看。npx dsh-multi-chat install|start|stop|gateway",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"deepseek",
|
|
22
22
|
"multi-chat",
|
|
23
23
|
"multi-conversation",
|
|
24
|
+
"multi-window",
|
|
25
|
+
"multi-agent",
|
|
24
26
|
"client-plugin",
|
|
25
27
|
"gateway",
|
|
26
28
|
"phone-access"
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"bin/",
|
|
30
32
|
"scripts/",
|
|
31
33
|
"patches/",
|
|
34
|
+
"assets/",
|
|
32
35
|
"plugin/dsh-client-ui-multi-wall/package.json",
|
|
33
36
|
"plugin/dsh-client-ui-multi-wall/README.md",
|
|
34
37
|
"plugin/dsh-client-ui-multi-wall/lib/index.js",
|
|
@@ -242,27 +242,27 @@ window.__ModuleLoader__.load({
|
|
|
242
242
|
document.head.appendChild(tag);
|
|
243
243
|
}
|
|
244
244
|
var WallView_module_css_default = {
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"confirm": "ouhzTG_confirm",
|
|
248
|
-
"field": "ouhzTG_field",
|
|
245
|
+
"paneActions": "ouhzTG_paneActions",
|
|
246
|
+
"linkBar": "ouhzTG_linkBar",
|
|
249
247
|
"wall": "ouhzTG_wall",
|
|
250
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
265
|
-
"
|
|
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(
|
|
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:
|
|
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
|
-
"
|
|
664
|
-
"
|
|
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
|
|
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(
|
|
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,
|
|
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:${
|
|
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;
|