@wenbin_wb/dsh-bridge 2.10.8 → 2.10.10
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/CHANGELOG.md +27 -0
- package/README.en.md +27 -0
- package/README.md +27 -0
- package/client/client.js +144 -3
- package/client/index.js +120 -0
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/bridge-rpc-constants.js +2 -0
- package/lib/bridge-rpc.js +32 -2
- package/lib/connection-compat.js +115 -0
- package/lib/feishu/index.js +10 -1
- package/lib/index.js +24 -42
- package/lib/platform/commands.js +18 -4
- package/lib/platform/config-restore.js +69 -0
- package/lib/platform/conversation-bridge.js +97 -13
- package/lib/platform/session-catalog.js +25 -3
- package/lib/platform/session-config.js +52 -0
- package/lib/qq/index.js +11 -1
- package/lib/telegram/index.js +10 -1
- package/lib/wechat/index.js +12 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.10.10] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### ✨ 新功能
|
|
10
|
+
|
|
11
|
+
- **设置页新增「高级设置」**(每个 IM 平台卡片内):可直接配置该平台远程会话的 **Agent 预设**(下拉列出本机可用预设,留空使用 DSH 默认预设)、**工作区目录**、**模型提供方 / 模型**,以及进度摘要间隔、审批超时、单条消息上限、分块发送间隔。保存后立即生效,并随宿主重启保留——此前这四个会话级配置没有任何界面入口,只能手改 `config.json`,节奏参数的面板也已在早前重构中丢失。
|
|
12
|
+
|
|
13
|
+
### 🐞 修复
|
|
14
|
+
|
|
15
|
+
- **DSH 0.1.5 系列上远程 IM 会话工具集残缺**(issue #40):DSH 从 `0.1.5` 起把 agent 侧的工具、提示词与技能目录整体挪到了 agent preset 后面(宿主层不再挂载 `tool-fs` / `tool-bash` / `tool-jobs` / `tool-skill` 等),改由每个会话在创建时挂载预设提供。桥此前只把预设名写进会话元数据、从未真正挂载,会话因此落在「空预设层」——表现为远程 Agent 缺少工具(例如没有读文件的工具)。现在新建与恢复会话都会真实挂载预设:未配置时使用 DSH 自身的默认预设;配置的预设在本机已不存在时回退默认预设,并在会话创建提示里明确告知,不再静默降级。
|
|
16
|
+
- **宿主重启后给已有会话发消息报 `session "..." already exists`、随后卡在「当前没有活动会话」**(issue #39):判断会话是否已持久化时沿用了旧的数据结构假设,而 DSH 0.1.5 的会话列表返回的是 `{ header, revision, sizeBytes }` 快照,`entry.id` 恒为空——已存在的历史会话被误判成新会话,走 `agents.create` 建同名会话而失败。现按真实结构取会话头,重启后能正常恢复续聊。
|
|
17
|
+
- **远程会话的工作区与预设配置重启即丢**(issue #40):`cwd`、`agentPreset`、`agentProvider`、`agentModel` 写进配置后,宿主重启即失效(恢复链路只回写了白名单里的少数字段),远程会话会掉到默认目录与默认预设。现已在重启恢复与配置备份导入时一并读回;同时补上 QQ / 飞书此前遗漏的摘要间隔、审批超时、分块延时恢复。配置的工作区目录不可用(被删、换机器、写成相对路径)时回落到已注册工作区并提示,不再把会话建在不存在的目录上。
|
|
18
|
+
- **`/rename` 改名重启即失效**:此前调用的是 `sessionPersistence.update()`——DSH 的持久化服务根本没有这个方法,被可选链静默跳过,改名只改了内存对象。现改用 DSH 原生会话标题服务(追加 `session/title` 事件并落盘),重启与 Web 侧边栏都能看到新标题;会话尚未恢复时会明确提示而不是假报成功。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [v2.10.9] - 2026-09-12
|
|
23
|
+
|
|
24
|
+
### 🐞 修复
|
|
25
|
+
|
|
26
|
+
- **DSH 0.1.5 系列上 `dsh web` 启动即崩**(issue #38):DSH 从 `0.1.5-alpha.1` 起,宿主自带的 `dsh-client-connection` 调整了插件级服务声明却遗漏了一处同步修改,导致凡是注册 RPC 通道的插件都会让**整棵插件树加载失败**、`dsh web` 直接退出(报 `cannot get property "webServer" without inject`)。现已插件侧兼容:正常路径完全不变,仅在命中该宿主缺陷时自动补上解析兜底并完成通道注册,上游修复后自动空转、无副作用。DSH `0.1.5-alpha.1` / `alpha.2` / `rc.1` / `rc.2`(含当前最新版)均已实测通过,`0.1.0`~`0.1.4` 行为不变。
|
|
27
|
+
|
|
28
|
+
### ✨ 优化
|
|
29
|
+
|
|
30
|
+
- **与 DSH 原生鉴权(`0.1.2` 起内置)确认兼容并加固**:反代转发时自动注入 DSH 要求的回环会话 cookie,手机 / 局域网 / 隧道访问无需处理 DSH 自身的 `?token=` 登录流程;同时修正凭据解析——当 DSH 凭据文件里存在多条密钥记录时,确保取用浏览器会话记录对应的密钥,避免远程访问出现 401。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
7
34
|
## [v2.10.8] - 2026-09-08
|
|
8
35
|
|
|
9
36
|
### ✨ 新功能
|
package/README.en.md
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
- [7. 📊 Maintenance Dashboard & Graceful Restart](#7-📊-maintenance-dashboard--graceful-restart)
|
|
49
49
|
- [💬 FAQ](#-faq)
|
|
50
50
|
- [🛠️ Development & Contribution](#️-development--contribution)
|
|
51
|
+
- [⭐ Star History](#-star-history)
|
|
51
52
|
- [📄 License](#-license)
|
|
52
53
|
|
|
53
54
|
---
|
|
@@ -77,6 +78,20 @@ node -v # v22.19+ or v24+
|
|
|
77
78
|
dsh --version
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
### DSH version compatibility
|
|
82
|
+
|
|
83
|
+
This plugin supports **both old and new DSH releases** — there is no need to pick a plugin version to match your harness.
|
|
84
|
+
|
|
85
|
+
| DSH version | Status |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `0.1.0` ~ `0.1.1` | ✅ Supported (loopback-only RPC channel hardening) |
|
|
88
|
+
| `0.1.2` ~ `0.1.4` | ✅ Supported |
|
|
89
|
+
| `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ Supported (since v2.10.9) |
|
|
90
|
+
|
|
91
|
+
> **About the built-in DSH authentication**: since `0.1.2`, `dsh web` ships browser authentication — it prints a URL carrying a one-time token (`http://127.0.0.1:3080/?token=…`), which is exchanged for a session cookie bound to the loopback address. Afterwards `/`, `/api` and every plugin RPC channel require that cookie.
|
|
92
|
+
>
|
|
93
|
+
> This is **complementary to, not a duplicate of**, this plugin's access-password gate: the built-in auth protects "the DSH process on local loopback", while this plugin's gate protects "remote access arriving over LAN or a public tunnel". The plugin's reverse proxy injects a valid loopback session cookie while forwarding, so phone / tunnel access needs **no** manual handling of DSH's `?token=` — just use it as documented on this page.
|
|
94
|
+
|
|
80
95
|
### Installation
|
|
81
96
|
|
|
82
97
|
```bash
|
|
@@ -405,6 +420,18 @@ dsh plugin --profile web add .
|
|
|
405
420
|
|
|
406
421
|
---
|
|
407
422
|
|
|
423
|
+
## ⭐ Star History
|
|
424
|
+
|
|
425
|
+
<a href="https://www.star-history.com/?repos=wenbin-wb%2Fdsh-bridge&type=date&legend=top-left">
|
|
426
|
+
<picture>
|
|
427
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&theme=dark&legend=top-left" />
|
|
428
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
|
|
429
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
|
|
430
|
+
</picture>
|
|
431
|
+
</a>
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
408
435
|
## 📄 License
|
|
409
436
|
|
|
410
437
|
MIT © [wenbin-wb](https://github.com/wenbin-wb)
|
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
- [7. 📊 运维监控看板与一键平滑重启](#7-📊-运维监控看板与一键平滑重启)
|
|
49
49
|
- [💬 常见问题 (FAQ)](#-常见问题-faq)
|
|
50
50
|
- [🛠️ 开发与贡献](#️-开发与贡献)
|
|
51
|
+
- [⭐ Star History](#-star-history)
|
|
51
52
|
- [📄 开源协议](#-开源协议)
|
|
52
53
|
|
|
53
54
|
---
|
|
@@ -79,6 +80,20 @@ node -v # 应显示 v22.19+ 或 v24+
|
|
|
79
80
|
dsh --version
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
### DSH 版本兼容性
|
|
84
|
+
|
|
85
|
+
本插件**同时兼容新旧 DSH**,无需按 DSH 版本挑选插件版本:
|
|
86
|
+
|
|
87
|
+
| DSH 版本 | 状态 |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `0.1.0` ~ `0.1.1` | ✅ 支持(回环专用 RPC 通道加固) |
|
|
90
|
+
| `0.1.2` ~ `0.1.4` | ✅ 支持 |
|
|
91
|
+
| `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ 支持(v2.10.9 起) |
|
|
92
|
+
|
|
93
|
+
> **关于 DSH 原生鉴权**:DSH 从 `0.1.2` 起为 `dsh web` 内置了浏览器鉴权——启动时会打印带一次性 token 的地址(`http://127.0.0.1:3080/?token=…`),换取一枚绑定回环地址的会话 cookie,此后 `/`、`/api` 及各插件 RPC 通道都要求该 cookie。
|
|
94
|
+
>
|
|
95
|
+
> 这与本插件的**访问密码门禁是两层互补的防护**,不冲突也不重复:原生鉴权保护的是"本机回环上的 DSH 进程",本插件的门禁保护的是"经局域网 / 公网隧道进入的远程访问"。本插件的反向代理会在转发时自动注入合法的回环会话 cookie,因此手机 / 隧道访问**无需**手动处理 DSH 的 `?token=`,按本页说明正常使用即可。
|
|
96
|
+
|
|
82
97
|
### 安装插件
|
|
83
98
|
|
|
84
99
|
```bash
|
|
@@ -427,6 +442,18 @@ dsh plugin --profile web add .
|
|
|
427
442
|
|
|
428
443
|
---
|
|
429
444
|
|
|
445
|
+
## ⭐ Star History
|
|
446
|
+
|
|
447
|
+
<a href="https://www.star-history.com/?repos=wenbin-wb%2Fdsh-bridge&type=date&legend=top-left">
|
|
448
|
+
<picture>
|
|
449
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&theme=dark&legend=top-left" />
|
|
450
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
|
|
451
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
|
|
452
|
+
</picture>
|
|
453
|
+
</a>
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
430
457
|
## 📄 开源协议
|
|
431
458
|
|
|
432
459
|
本项目基于 [MIT 许可证](LICENSE) 开源发布。
|
package/client/client.js
CHANGED
|
@@ -1091,6 +1091,8 @@ var BRIDGE_ENDPOINTS = {
|
|
|
1091
1091
|
platformStop: "platformStop",
|
|
1092
1092
|
platformStart: "platformStart",
|
|
1093
1093
|
platformUnbind: "platformUnbind",
|
|
1094
|
+
// 本机可用的 DSH agent preset(设置页下拉用;老版本 DSH 返回 available:false)
|
|
1095
|
+
listAgentPresets: "listAgentPresets",
|
|
1094
1096
|
// 微信 Bot(v1.x 向后兼容别名,deprecated)
|
|
1095
1097
|
wechatGetStatus: "wechatGetStatus",
|
|
1096
1098
|
wechatLogin: "wechatLogin",
|
|
@@ -2652,10 +2654,29 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
2652
2654
|
appSecret: "",
|
|
2653
2655
|
domain: platform.config.domain ?? "feishu",
|
|
2654
2656
|
botToken: "",
|
|
2655
|
-
proxy: platform.config.proxy ?? ""
|
|
2657
|
+
proxy: platform.config.proxy ?? "",
|
|
2658
|
+
// 会话级配置:留空 = 使用 DSH 默认值
|
|
2659
|
+
agentPreset: platform.config.agentPreset ?? "",
|
|
2660
|
+
cwd: platform.config.cwd ?? "",
|
|
2661
|
+
agentProvider: platform.config.agentProvider ?? "",
|
|
2662
|
+
agentModel: platform.config.agentModel ?? ""
|
|
2656
2663
|
});
|
|
2657
2664
|
}
|
|
2658
2665
|
}, [platform?.config, platformId]);
|
|
2666
|
+
const [presetOptions, setPresetOptions] = React.useState(null);
|
|
2667
|
+
React.useEffect(() => {
|
|
2668
|
+
let cancelled = false;
|
|
2669
|
+
(async () => {
|
|
2670
|
+
try {
|
|
2671
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.listAgentPresets, {});
|
|
2672
|
+
if (!cancelled && r?.ok) setPresetOptions(r.value ?? { available: false, presets: [] });
|
|
2673
|
+
} catch {
|
|
2674
|
+
}
|
|
2675
|
+
})();
|
|
2676
|
+
return () => {
|
|
2677
|
+
cancelled = true;
|
|
2678
|
+
};
|
|
2679
|
+
}, [rpcCall]);
|
|
2659
2680
|
const loadInFlightRef = React.useRef(false);
|
|
2660
2681
|
const seqRef = React.useRef(0);
|
|
2661
2682
|
const load = React.useCallback(async (quiet = false) => {
|
|
@@ -2746,7 +2767,12 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
2746
2767
|
digestIntervalSec: num(cfgDraft.digestIntervalSec, platform?.config?.digestIntervalSec ?? 300),
|
|
2747
2768
|
approvalTimeoutSec: num(cfgDraft.approvalTimeoutSec, platform?.config?.approvalTimeoutSec ?? 600),
|
|
2748
2769
|
maxMessageChars: num(cfgDraft.maxMessageChars, platform?.config?.maxMessageChars ?? 2e3),
|
|
2749
|
-
sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500)
|
|
2770
|
+
sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500),
|
|
2771
|
+
// 会话级配置:空串表示清除该设置、回落 DSH 默认值
|
|
2772
|
+
agentPreset: (cfgDraft.agentPreset ?? "").trim(),
|
|
2773
|
+
cwd: (cfgDraft.cwd ?? "").trim(),
|
|
2774
|
+
agentProvider: (cfgDraft.agentProvider ?? "").trim(),
|
|
2775
|
+
agentModel: (cfgDraft.agentModel ?? "").trim()
|
|
2750
2776
|
};
|
|
2751
2777
|
if (platformId === "qq") {
|
|
2752
2778
|
payload.appId = cfgDraft.appId.trim();
|
|
@@ -2761,7 +2787,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
2761
2787
|
}
|
|
2762
2788
|
await act(BRIDGE_ENDPOINTS.platformSetConfig, payload);
|
|
2763
2789
|
}, [act, cfgDraft, platformId]);
|
|
2764
|
-
const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")) || platformId === "feishu" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.appSecret !== (platform.config.appSecret ?? "")) || platformId === "telegram" && (cfgDraft.botToken !== "" || cfgDraft.proxy !== (platform.config.proxy ?? "")));
|
|
2790
|
+
const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs || (cfgDraft.agentPreset ?? "") !== (platform.config.agentPreset ?? "") || (cfgDraft.cwd ?? "") !== (platform.config.cwd ?? "") || (cfgDraft.agentProvider ?? "") !== (platform.config.agentProvider ?? "") || (cfgDraft.agentModel ?? "") !== (platform.config.agentModel ?? "") || platformId === "qq" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.clientSecret !== (platform.config.clientSecret ?? "")) || platformId === "feishu" && (cfgDraft.appId !== (platform.config.appId ?? "") || cfgDraft.appSecret !== (platform.config.appSecret ?? "")) || platformId === "telegram" && (cfgDraft.botToken !== "" || cfgDraft.proxy !== (platform.config.proxy ?? "")));
|
|
2765
2791
|
if (!platform && !err) {
|
|
2766
2792
|
return React.createElement(
|
|
2767
2793
|
"div",
|
|
@@ -3006,6 +3032,121 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
3006
3032
|
}),
|
|
3007
3033
|
React.createElement("span", null, "\u7FA4\u804A\u81EA\u52A8\u6388\u6743\uFF08\u65B0\u7FA4\u9996\u6B21 @\u673A\u5668\u4EBA \u81EA\u52A8\u52A0\u5165\u767D\u540D\u5355\uFF0C\u9ED8\u8BA4\u5173\u95ED\uFF09")
|
|
3008
3034
|
),
|
|
3035
|
+
// 高级设置:会话级配置(工作区 / Agent 预设 / 模型路由)+ 会话节奏参数
|
|
3036
|
+
React.createElement(
|
|
3037
|
+
"div",
|
|
3038
|
+
{ style: { marginTop: 12 } },
|
|
3039
|
+
React.createElement("button", {
|
|
3040
|
+
style: s.btnGhost,
|
|
3041
|
+
onClick: () => setShowAdvanced((v) => !v)
|
|
3042
|
+
}, showAdvanced ? "\u6536\u8D77\u9AD8\u7EA7\u8BBE\u7F6E" : "\u2699\uFE0F \u9AD8\u7EA7\u8BBE\u7F6E"),
|
|
3043
|
+
showAdvanced && React.createElement(
|
|
3044
|
+
"div",
|
|
3045
|
+
{ style: { ...s.block, marginTop: 8, display: "flex", flexDirection: "column", gap: 10 } },
|
|
3046
|
+
React.createElement(
|
|
3047
|
+
"div",
|
|
3048
|
+
{ style: { ...s.muted, fontSize: 12, lineHeight: 1.6 } },
|
|
3049
|
+
"\u4EE5\u4E0B\u8BBE\u7F6E\u6309\u5E73\u53F0\u4FDD\u5B58\uFF0C\u53EA\u5F71\u54CD\u8BE5\u5E73\u53F0\u901A\u8FC7 Bot \u65B0\u5EFA\u7684\u4F1A\u8BDD\uFF08\u7559\u7A7A = \u4F7F\u7528 DSH \u9ED8\u8BA4\u503C\uFF09\u3002"
|
|
3050
|
+
),
|
|
3051
|
+
// Agent 预设:决定远程会话挂载哪些工具/技能
|
|
3052
|
+
React.createElement(
|
|
3053
|
+
"div",
|
|
3054
|
+
null,
|
|
3055
|
+
React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "Agent \u9884\u8BBE \u2014 \u51B3\u5B9A\u8FDC\u7A0B\u4F1A\u8BDD\u53EF\u7528\u7684\u5DE5\u5177\u4E0E\u6280\u80FD\uFF0C\u7559\u7A7A\u7528 DSH \u9ED8\u8BA4\u9884\u8BBE"),
|
|
3056
|
+
React.createElement("input", {
|
|
3057
|
+
style: { ...s.input, width: "100%" },
|
|
3058
|
+
list: "dsh-bridge-agent-presets",
|
|
3059
|
+
placeholder: presetOptions?.default ? `\u7559\u7A7A = DSH \u9ED8\u8BA4\uFF08${presetOptions.default}\uFF09` : "\u7559\u7A7A = DSH \u9ED8\u8BA4\u9884\u8BBE",
|
|
3060
|
+
value: cfgDraft?.agentPreset ?? "",
|
|
3061
|
+
onChange: (e) => setCfgDraft((d) => ({ ...d, agentPreset: e.target.value }))
|
|
3062
|
+
}),
|
|
3063
|
+
React.createElement(
|
|
3064
|
+
"datalist",
|
|
3065
|
+
{ id: "dsh-bridge-agent-presets" },
|
|
3066
|
+
(presetOptions?.presets ?? []).map((p) => React.createElement("option", { key: p.id, value: p.id }, p.name && p.name !== p.id ? p.name : null))
|
|
3067
|
+
),
|
|
3068
|
+
React.createElement(
|
|
3069
|
+
"div",
|
|
3070
|
+
{ style: { ...s.muted, fontSize: 11, marginTop: 4 } },
|
|
3071
|
+
presetOptions == null ? "\u6B63\u5728\u8BFB\u53D6\u672C\u673A\u53EF\u7528\u9884\u8BBE\u2026" : presetOptions.available ? `\u672C\u673A\u53EF\u7528\uFF1A${(presetOptions.presets ?? []).map((p) => p.id).join(" / ") || "\uFF08\u65E0\uFF09"}` : "\u5F53\u524D DSH \u7248\u672C\u672A\u63D0\u4F9B\u9884\u8BBE\u5217\u8868\uFF0C\u8BF7\u624B\u52A8\u586B\u5199\u9884\u8BBE\u540D"
|
|
3072
|
+
)
|
|
3073
|
+
),
|
|
3074
|
+
// 工作区
|
|
3075
|
+
React.createElement(
|
|
3076
|
+
"div",
|
|
3077
|
+
null,
|
|
3078
|
+
React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5DE5\u4F5C\u533A\u76EE\u5F55 \u2014 \u8BE5\u5E73\u53F0 /new \u65B0\u5EFA\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u76EE\u5F55\uFF0C\u7559\u7A7A\u7528\u9996\u4E2A\u5DF2\u6CE8\u518C\u5DE5\u4F5C\u533A"),
|
|
3079
|
+
React.createElement("input", {
|
|
3080
|
+
style: { ...s.input, width: "100%" },
|
|
3081
|
+
placeholder: "\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u4F8B\u5982 /home/me/project \u6216 D:\\project",
|
|
3082
|
+
value: cfgDraft?.cwd ?? "",
|
|
3083
|
+
onChange: (e) => setCfgDraft((d) => ({ ...d, cwd: e.target.value }))
|
|
3084
|
+
})
|
|
3085
|
+
),
|
|
3086
|
+
// 模型路由
|
|
3087
|
+
React.createElement(
|
|
3088
|
+
"div",
|
|
3089
|
+
{ style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 8 } },
|
|
3090
|
+
React.createElement(
|
|
3091
|
+
"div",
|
|
3092
|
+
null,
|
|
3093
|
+
React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u6A21\u578B\u63D0\u4F9B\u65B9\uFF08\u53EF\u9009\uFF09"),
|
|
3094
|
+
React.createElement("input", {
|
|
3095
|
+
style: { ...s.input, width: "100%" },
|
|
3096
|
+
placeholder: "\u7559\u7A7A = DSH \u9ED8\u8BA4",
|
|
3097
|
+
value: cfgDraft?.agentProvider ?? "",
|
|
3098
|
+
onChange: (e) => setCfgDraft((d) => ({ ...d, agentProvider: e.target.value }))
|
|
3099
|
+
})
|
|
3100
|
+
),
|
|
3101
|
+
React.createElement(
|
|
3102
|
+
"div",
|
|
3103
|
+
null,
|
|
3104
|
+
React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u6A21\u578B\uFF08\u53EF\u9009\uFF09"),
|
|
3105
|
+
React.createElement("input", {
|
|
3106
|
+
style: { ...s.input, width: "100%" },
|
|
3107
|
+
placeholder: "\u7559\u7A7A = DSH \u9ED8\u8BA4",
|
|
3108
|
+
value: cfgDraft?.agentModel ?? "",
|
|
3109
|
+
onChange: (e) => setCfgDraft((d) => ({ ...d, agentModel: e.target.value }))
|
|
3110
|
+
})
|
|
3111
|
+
)
|
|
3112
|
+
),
|
|
3113
|
+
// 会话节奏参数
|
|
3114
|
+
React.createElement(
|
|
3115
|
+
"div",
|
|
3116
|
+
{ style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 8 } },
|
|
3117
|
+
[
|
|
3118
|
+
["digestIntervalSec", "\u8FDB\u5EA6\u6458\u8981\u95F4\u9694\uFF08\u79D2\uFF09"],
|
|
3119
|
+
["approvalTimeoutSec", "\u5BA1\u6279\u8D85\u65F6\uFF08\u79D2\uFF09"],
|
|
3120
|
+
["maxMessageChars", "\u5355\u6761\u6D88\u606F\u4E0A\u9650\uFF08\u5B57\u7B26\uFF09"],
|
|
3121
|
+
["sendChunkDelayMs", "\u5206\u5757\u53D1\u9001\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09"]
|
|
3122
|
+
].map(([key, label]) => React.createElement(
|
|
3123
|
+
"div",
|
|
3124
|
+
{ key },
|
|
3125
|
+
React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, label),
|
|
3126
|
+
React.createElement("input", {
|
|
3127
|
+
style: { ...s.input, width: "100%" },
|
|
3128
|
+
inputMode: "numeric",
|
|
3129
|
+
value: cfgDraft?.[key] ?? "",
|
|
3130
|
+
onChange: (e) => setCfgDraft((d) => ({ ...d, [key]: e.target.value }))
|
|
3131
|
+
})
|
|
3132
|
+
))
|
|
3133
|
+
),
|
|
3134
|
+
React.createElement(
|
|
3135
|
+
"div",
|
|
3136
|
+
{ style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
3137
|
+
React.createElement("button", {
|
|
3138
|
+
style: { ...s.btnPri, opacity: busy || !cfgDirty ? 0.5 : 1 },
|
|
3139
|
+
onClick: saveConfig,
|
|
3140
|
+
disabled: busy || !cfgDirty
|
|
3141
|
+
}, busy ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u9AD8\u7EA7\u8BBE\u7F6E"),
|
|
3142
|
+
React.createElement("button", {
|
|
3143
|
+
style: { ...s.btnGhost },
|
|
3144
|
+
onClick: resetDefaults,
|
|
3145
|
+
disabled: busy
|
|
3146
|
+
}, "\u6062\u590D\u63A8\u8350\u8282\u594F\u53C2\u6570")
|
|
3147
|
+
)
|
|
3148
|
+
)
|
|
3149
|
+
),
|
|
3009
3150
|
// 飞书 / Telegram 扫码直达对话引导卡片
|
|
3010
3151
|
(platformId === "feishu" || platformId === "telegram") && platform.botQr && React.createElement(
|
|
3011
3152
|
"div",
|
package/client/index.js
CHANGED
|
@@ -1396,10 +1396,28 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
1396
1396
|
domain: platform.config.domain ?? 'feishu',
|
|
1397
1397
|
botToken: '',
|
|
1398
1398
|
proxy: platform.config.proxy ?? '',
|
|
1399
|
+
// 会话级配置:留空 = 使用 DSH 默认值
|
|
1400
|
+
agentPreset: platform.config.agentPreset ?? '',
|
|
1401
|
+
cwd: platform.config.cwd ?? '',
|
|
1402
|
+
agentProvider: platform.config.agentProvider ?? '',
|
|
1403
|
+
agentModel: platform.config.agentModel ?? '',
|
|
1399
1404
|
});
|
|
1400
1405
|
}
|
|
1401
1406
|
}, [platform?.config, platformId]);
|
|
1402
1407
|
|
|
1408
|
+
// 本机可用的 DSH agent preset(旧版本 DSH 返回 available:false → 退化为手填)
|
|
1409
|
+
const [presetOptions, setPresetOptions] = React.useState(null);
|
|
1410
|
+
React.useEffect(() => {
|
|
1411
|
+
let cancelled = false;
|
|
1412
|
+
(async () => {
|
|
1413
|
+
try {
|
|
1414
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.listAgentPresets, {});
|
|
1415
|
+
if (!cancelled && r?.ok) setPresetOptions(r.value ?? { available: false, presets: [] });
|
|
1416
|
+
} catch { /* 读取失败保持手填 */ }
|
|
1417
|
+
})();
|
|
1418
|
+
return () => { cancelled = true; };
|
|
1419
|
+
}, [rpcCall]);
|
|
1420
|
+
|
|
1403
1421
|
const loadInFlightRef = React.useRef(false);
|
|
1404
1422
|
const seqRef = React.useRef(0);
|
|
1405
1423
|
const load = React.useCallback(async (quiet = false) => {
|
|
@@ -1507,6 +1525,11 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
1507
1525
|
approvalTimeoutSec: num(cfgDraft.approvalTimeoutSec, platform?.config?.approvalTimeoutSec ?? 600),
|
|
1508
1526
|
maxMessageChars: num(cfgDraft.maxMessageChars, platform?.config?.maxMessageChars ?? 2000),
|
|
1509
1527
|
sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500),
|
|
1528
|
+
// 会话级配置:空串表示清除该设置、回落 DSH 默认值
|
|
1529
|
+
agentPreset: (cfgDraft.agentPreset ?? '').trim(),
|
|
1530
|
+
cwd: (cfgDraft.cwd ?? '').trim(),
|
|
1531
|
+
agentProvider: (cfgDraft.agentProvider ?? '').trim(),
|
|
1532
|
+
agentModel: (cfgDraft.agentModel ?? '').trim(),
|
|
1510
1533
|
};
|
|
1511
1534
|
// QQ / 飞书 / Telegram 平台额外携带凭证
|
|
1512
1535
|
if (platformId === 'qq') {
|
|
@@ -1527,6 +1550,10 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
1527
1550
|
Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec ||
|
|
1528
1551
|
Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars ||
|
|
1529
1552
|
Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs ||
|
|
1553
|
+
(cfgDraft.agentPreset ?? '') !== (platform.config.agentPreset ?? '') ||
|
|
1554
|
+
(cfgDraft.cwd ?? '') !== (platform.config.cwd ?? '') ||
|
|
1555
|
+
(cfgDraft.agentProvider ?? '') !== (platform.config.agentProvider ?? '') ||
|
|
1556
|
+
(cfgDraft.agentModel ?? '') !== (platform.config.agentModel ?? '') ||
|
|
1530
1557
|
(platformId === 'qq' && (
|
|
1531
1558
|
cfgDraft.appId !== (platform.config.appId ?? '') ||
|
|
1532
1559
|
cfgDraft.clientSecret !== (platform.config.clientSecret ?? '')
|
|
@@ -1747,6 +1774,99 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
|
|
|
1747
1774
|
}),
|
|
1748
1775
|
React.createElement('span', null, '群聊自动授权(新群首次 @机器人 自动加入白名单,默认关闭)'),
|
|
1749
1776
|
),
|
|
1777
|
+
// 高级设置:会话级配置(工作区 / Agent 预设 / 模型路由)+ 会话节奏参数
|
|
1778
|
+
React.createElement('div', { style: { marginTop: 12 } },
|
|
1779
|
+
React.createElement('button', {
|
|
1780
|
+
style: s.btnGhost,
|
|
1781
|
+
onClick: () => setShowAdvanced(v => !v),
|
|
1782
|
+
}, showAdvanced ? '收起高级设置' : '⚙️ 高级设置'),
|
|
1783
|
+
showAdvanced && React.createElement('div', { style: { ...s.block, marginTop: 8, display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1784
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 12, lineHeight: 1.6 } },
|
|
1785
|
+
'以下设置按平台保存,只影响该平台通过 Bot 新建的会话(留空 = 使用 DSH 默认值)。'),
|
|
1786
|
+
// Agent 预设:决定远程会话挂载哪些工具/技能
|
|
1787
|
+
React.createElement('div', null,
|
|
1788
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'Agent 预设 — 决定远程会话可用的工具与技能,留空用 DSH 默认预设'),
|
|
1789
|
+
React.createElement('input', {
|
|
1790
|
+
style: { ...s.input, width: '100%' },
|
|
1791
|
+
list: 'dsh-bridge-agent-presets',
|
|
1792
|
+
placeholder: presetOptions?.default ? `留空 = DSH 默认(${presetOptions.default})` : '留空 = DSH 默认预设',
|
|
1793
|
+
value: cfgDraft?.agentPreset ?? '',
|
|
1794
|
+
onChange: (e) => setCfgDraft(d => ({ ...d, agentPreset: e.target.value })),
|
|
1795
|
+
}),
|
|
1796
|
+
React.createElement('datalist', { id: 'dsh-bridge-agent-presets' },
|
|
1797
|
+
(presetOptions?.presets ?? []).map((p) =>
|
|
1798
|
+
React.createElement('option', { key: p.id, value: p.id }, p.name && p.name !== p.id ? p.name : null)),
|
|
1799
|
+
),
|
|
1800
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
|
|
1801
|
+
presetOptions == null
|
|
1802
|
+
? '正在读取本机可用预设…'
|
|
1803
|
+
: presetOptions.available
|
|
1804
|
+
? `本机可用:${(presetOptions.presets ?? []).map((p) => p.id).join(' / ') || '(无)'}`
|
|
1805
|
+
: '当前 DSH 版本未提供预设列表,请手动填写预设名'),
|
|
1806
|
+
),
|
|
1807
|
+
// 工作区
|
|
1808
|
+
React.createElement('div', null,
|
|
1809
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '工作区目录 — 该平台 /new 新建会话的默认目录,留空用首个已注册工作区'),
|
|
1810
|
+
React.createElement('input', {
|
|
1811
|
+
style: { ...s.input, width: '100%' },
|
|
1812
|
+
placeholder: '绝对路径,例如 /home/me/project 或 D:\\project',
|
|
1813
|
+
value: cfgDraft?.cwd ?? '',
|
|
1814
|
+
onChange: (e) => setCfgDraft(d => ({ ...d, cwd: e.target.value })),
|
|
1815
|
+
}),
|
|
1816
|
+
),
|
|
1817
|
+
// 模型路由
|
|
1818
|
+
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 8 } },
|
|
1819
|
+
React.createElement('div', null,
|
|
1820
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '模型提供方(可选)'),
|
|
1821
|
+
React.createElement('input', {
|
|
1822
|
+
style: { ...s.input, width: '100%' },
|
|
1823
|
+
placeholder: '留空 = DSH 默认',
|
|
1824
|
+
value: cfgDraft?.agentProvider ?? '',
|
|
1825
|
+
onChange: (e) => setCfgDraft(d => ({ ...d, agentProvider: e.target.value })),
|
|
1826
|
+
}),
|
|
1827
|
+
),
|
|
1828
|
+
React.createElement('div', null,
|
|
1829
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '模型(可选)'),
|
|
1830
|
+
React.createElement('input', {
|
|
1831
|
+
style: { ...s.input, width: '100%' },
|
|
1832
|
+
placeholder: '留空 = DSH 默认',
|
|
1833
|
+
value: cfgDraft?.agentModel ?? '',
|
|
1834
|
+
onChange: (e) => setCfgDraft(d => ({ ...d, agentModel: e.target.value })),
|
|
1835
|
+
}),
|
|
1836
|
+
),
|
|
1837
|
+
),
|
|
1838
|
+
// 会话节奏参数
|
|
1839
|
+
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 8 } },
|
|
1840
|
+
[
|
|
1841
|
+
['digestIntervalSec', '进度摘要间隔(秒)'],
|
|
1842
|
+
['approvalTimeoutSec', '审批超时(秒)'],
|
|
1843
|
+
['maxMessageChars', '单条消息上限(字符)'],
|
|
1844
|
+
['sendChunkDelayMs', '分块发送间隔(毫秒)'],
|
|
1845
|
+
].map(([key, label]) =>
|
|
1846
|
+
React.createElement('div', { key },
|
|
1847
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, label),
|
|
1848
|
+
React.createElement('input', {
|
|
1849
|
+
style: { ...s.input, width: '100%' },
|
|
1850
|
+
inputMode: 'numeric',
|
|
1851
|
+
value: cfgDraft?.[key] ?? '',
|
|
1852
|
+
onChange: (e) => setCfgDraft(d => ({ ...d, [key]: e.target.value })),
|
|
1853
|
+
}),
|
|
1854
|
+
)),
|
|
1855
|
+
),
|
|
1856
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1857
|
+
React.createElement('button', {
|
|
1858
|
+
style: { ...s.btnPri, opacity: (busy || !cfgDirty) ? 0.5 : 1 },
|
|
1859
|
+
onClick: saveConfig,
|
|
1860
|
+
disabled: busy || !cfgDirty,
|
|
1861
|
+
}, busy ? '保存中…' : '保存高级设置'),
|
|
1862
|
+
React.createElement('button', {
|
|
1863
|
+
style: { ...s.btnGhost },
|
|
1864
|
+
onClick: resetDefaults,
|
|
1865
|
+
disabled: busy,
|
|
1866
|
+
}, '恢复推荐节奏参数'),
|
|
1867
|
+
),
|
|
1868
|
+
),
|
|
1869
|
+
),
|
|
1750
1870
|
// 飞书 / Telegram 扫码直达对话引导卡片
|
|
1751
1871
|
(platformId === 'feishu' || platformId === 'telegram') && platform.botQr && React.createElement('div', {
|
|
1752
1872
|
style: {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// DSH 原生浏览器鉴权的兼容层(lib/auth/dsh-native-cookie.js)
|
|
2
|
+
//
|
|
3
|
+
// 背景
|
|
4
|
+
// ---------------------------------------------------------------
|
|
5
|
+
// DSH 从 0.1.2 起为 `dsh web` 引入了"原生浏览器鉴权":
|
|
6
|
+
// 1. 启动时打印带一次性 launch token 的地址:http://127.0.0.1:<port>/?token=…
|
|
7
|
+
// 2. `GET /?token=<token>` 校验通过后 303 重定向到 `/`,并下发签名会话 cookie
|
|
8
|
+
// 3. 此后所有受保护路由(`/`、`/api`、以及任何经
|
|
9
|
+
// `ctx.connection.rpc.handle()` 注册的通道)都要求该 cookie,否则 401
|
|
10
|
+
//
|
|
11
|
+
// cookie 的构造(见 @deepseek-ai/dsh-client-connection 的 BrowserAuth):
|
|
12
|
+
// 名称:`dsh-auth-` + base64url(sha256(authority)),authority = `127.0.0.1:<port>`
|
|
13
|
+
// 值 :`v1.<base64url(JSON payload)>.<base64url(HMAC-SHA256(secret, body))>`
|
|
14
|
+
// 载荷:`{ version: 1, authority, issuedAt, expiresAt }`(均为安全整数)
|
|
15
|
+
// 密钥:`<DSH_HOME>/.credentials.yaml` 里 record
|
|
16
|
+
// `client-connection/browser-session` 的 `payload.secret`(base64url 32 字节)
|
|
17
|
+
//
|
|
18
|
+
// 为什么本插件需要它
|
|
19
|
+
// ---------------------------------------------------------------
|
|
20
|
+
// 本插件的自建反向代理监听自己的端口(默认 3082),对外提供局域网/隧道访问,
|
|
21
|
+
// 再把请求转发到 DSH 回环地址。浏览器持有的是"代理 origin"的 cookie,
|
|
22
|
+
// 而 DSH 的 cookie 绑定的是 `127.0.0.1:<dshPort>` 这个 authority,两者不是同一个,
|
|
23
|
+
// 因此代理必须自行注入一枚合法的 loopback 会话 cookie,否则远程访问会一律 401。
|
|
24
|
+
//
|
|
25
|
+
// 本模块只做"按 DSH 的格式生成一枚回环会话 cookie",不引入任何三方依赖。
|
|
26
|
+
|
|
27
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
28
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
/** DSH 会话 cookie 的名称前缀(与 dsh-client-connection 的 COOKIE_PREFIX 一致)。 */
|
|
33
|
+
export const DSH_AUTH_COOKIE_PREFIX = 'dsh-auth-';
|
|
34
|
+
|
|
35
|
+
/** cookie 载荷版本(与 dsh-client-connection 的 COOKIE_PAYLOAD_VERSION 一致)。 */
|
|
36
|
+
export const DSH_AUTH_COOKIE_VERSION = 1;
|
|
37
|
+
|
|
38
|
+
/** 会话 cookie 有效期(毫秒)。与 DSH 默认的 cookieMaxAgeDays=30 对齐。 */
|
|
39
|
+
export const DSH_AUTH_COOKIE_MAX_AGE_MS = 30 * 24 * 3600 * 1000;
|
|
40
|
+
|
|
41
|
+
/** 承载签名密钥的 DSH 凭据记录键。 */
|
|
42
|
+
export const DSH_BROWSER_SESSION_RECORD = 'client-connection/browser-session';
|
|
43
|
+
|
|
44
|
+
function base64Url(value) {
|
|
45
|
+
return Buffer.from(value).toString('base64url');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 计算 DSH 会话 cookie 的名称(authority 绑定)。
|
|
50
|
+
* @param {number|string} targetPort DSH 回环端口
|
|
51
|
+
* @param {string} [host] 默认 127.0.0.1
|
|
52
|
+
* @returns {string} 形如 `dsh-auth-<base64url(sha256("127.0.0.1:3080"))>`
|
|
53
|
+
*/
|
|
54
|
+
export function dshAuthCookieName(targetPort, host = '127.0.0.1') {
|
|
55
|
+
const authority = `${host}:${targetPort}`;
|
|
56
|
+
return DSH_AUTH_COOKIE_PREFIX + base64Url(createHash('sha256').update(authority).digest());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从 DSH 凭据文件内容中取出 browser-session 记录的签名密钥。
|
|
61
|
+
*
|
|
62
|
+
* 必须精确定位 `client-connection/browser-session` 这条 record:
|
|
63
|
+
* 凭据文件里还有 `refs:` 段(各家模型 API key)以及可能存在其它插件写入的
|
|
64
|
+
* record,一旦取到别处的 `secret:`,签名就会无效 —— 现象是"远程访问一律 401",
|
|
65
|
+
* 且极难定位。因此先锚定 record 键,再在其后的小范围内取 secret;
|
|
66
|
+
* 仅当锚点缺失(异常/未来格式)时才退回"全文件第一个 secret:"的旧行为。
|
|
67
|
+
*
|
|
68
|
+
* @param {string} content 凭据文件文本
|
|
69
|
+
* @returns {string} base64url 编码的密钥;取不到时返回空串
|
|
70
|
+
*/
|
|
71
|
+
export function readBrowserSessionSecret(content) {
|
|
72
|
+
if (typeof content !== 'string' || content === '') return '';
|
|
73
|
+
|
|
74
|
+
// 锚定 record 键所在行,随后只在该行往后的一小段内查找 secret
|
|
75
|
+
const anchorMatch = /^[ \t]*client-connection\/browser-session:[ \t]*$/m.exec(content);
|
|
76
|
+
if (anchorMatch) {
|
|
77
|
+
const tail = content.slice(anchorMatch.index);
|
|
78
|
+
const scoped = /secret:[ \t]*([A-Za-z0-9_-]+)/.exec(tail);
|
|
79
|
+
if (scoped) return scoped[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 兜底:老格式/异常格式下沿用全文件首个 secret:
|
|
83
|
+
const fallback = /secret:[ \t]*([A-Za-z0-9_-]+)/.exec(content);
|
|
84
|
+
return fallback ? fallback[1] : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 生成一枚绑定到 loopback authority 的 DSH 原生会话 cookie。
|
|
89
|
+
*
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @param {number|string} opts.targetPort DSH 回环端口
|
|
92
|
+
* @param {string} opts.secret base64url 编码的签名密钥
|
|
93
|
+
* @param {string} [opts.host] 默认 127.0.0.1
|
|
94
|
+
* @param {number} [opts.now] 当前时间戳(测试注入用)
|
|
95
|
+
* @param {number} [opts.maxAgeMs] 有效期
|
|
96
|
+
* @returns {string} `name=value`;密钥缺失时返回空串
|
|
97
|
+
*/
|
|
98
|
+
export function buildDshLoopbackCookie({
|
|
99
|
+
targetPort,
|
|
100
|
+
secret,
|
|
101
|
+
host = '127.0.0.1',
|
|
102
|
+
now = Date.now(),
|
|
103
|
+
maxAgeMs = DSH_AUTH_COOKIE_MAX_AGE_MS,
|
|
104
|
+
}) {
|
|
105
|
+
if (!secret || !targetPort) return '';
|
|
106
|
+
|
|
107
|
+
const authority = `${host}:${targetPort}`;
|
|
108
|
+
const name = dshAuthCookieName(targetPort, host);
|
|
109
|
+
// 与 DSH 一致:issuedAt 略微回拨,避免与宿主的同秒比较产生边界问题
|
|
110
|
+
const issuedAt = now - 1000;
|
|
111
|
+
const expiresAt = issuedAt + maxAgeMs;
|
|
112
|
+
|
|
113
|
+
const body = base64Url(Buffer.from(JSON.stringify({
|
|
114
|
+
version: DSH_AUTH_COOKIE_VERSION,
|
|
115
|
+
authority,
|
|
116
|
+
issuedAt,
|
|
117
|
+
expiresAt,
|
|
118
|
+
}), 'utf8'));
|
|
119
|
+
const key = Buffer.from(secret, 'base64url');
|
|
120
|
+
const sig = base64Url(createHmac('sha256', key).update(body).digest());
|
|
121
|
+
return `${name}=v${DSH_AUTH_COOKIE_VERSION}.${body}.${sig}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 读取本机 DSH 凭据并生成回环会话 cookie(失败时返回空串,绝不抛出)。
|
|
126
|
+
*
|
|
127
|
+
* @param {number|string} targetPort DSH 回环端口
|
|
128
|
+
* @param {object} [env] 便于测试注入的环境
|
|
129
|
+
* @param {string} [env.dshHome] DSH 主目录(默认 $DSH_HOME 或 ~/.dsh)
|
|
130
|
+
* @param {Function} [env.readFileSync] 文件读取实现(默认 node:fs)
|
|
131
|
+
* @param {Function} [env.existsSync] 存在性判断实现
|
|
132
|
+
* @param {number} [env.now] 当前时间戳
|
|
133
|
+
* @returns {string} `name=value`,或空串
|
|
134
|
+
*/
|
|
135
|
+
export function getDshLoopbackCookie(targetPort, env = {}) {
|
|
136
|
+
const readFile = env.readFileSync ?? readFileSync;
|
|
137
|
+
const exists = env.existsSync ?? existsSync;
|
|
138
|
+
try {
|
|
139
|
+
const dshHome = env.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
140
|
+
const credPath = join(dshHome, '.credentials.yaml');
|
|
141
|
+
if (!exists(credPath)) return '';
|
|
142
|
+
const secret = readBrowserSessionSecret(readFile(credPath, 'utf8'));
|
|
143
|
+
if (!secret) return '';
|
|
144
|
+
return buildDshLoopbackCookie({ targetPort, secret, now: env.now });
|
|
145
|
+
} catch {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -40,6 +40,8 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
40
40
|
platformStop: 'platformStop',
|
|
41
41
|
platformStart: 'platformStart',
|
|
42
42
|
platformUnbind: 'platformUnbind',
|
|
43
|
+
// 本机可用的 DSH agent preset(设置页下拉用;老版本 DSH 返回 available:false)
|
|
44
|
+
listAgentPresets: 'listAgentPresets',
|
|
43
45
|
// 微信 Bot(v1.x 向后兼容别名,deprecated)
|
|
44
46
|
wechatGetStatus: 'wechatGetStatus',
|
|
45
47
|
wechatLogin: 'wechatLogin',
|