@wenbin_wb/dsh-bridge 1.0.8 → 1.2.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/README.md +53 -5
- package/README.zh-CN.md +53 -5
- package/client/client.js +501 -42
- package/client/index.js +458 -44
- package/docs/banner.jpg +0 -0
- package/docs/custom-tunnel.md +220 -0
- package/docs/wechat-usage.md +101 -0
- package/lib/bridge-rpc-constants.js +21 -0
- package/lib/bridge-rpc.js +89 -13
- package/lib/index.js +45 -2
- package/lib/wechat/gateway.js +807 -0
- package/lib/wechat/index.js +189 -0
- package/lib/wechat/media.js +275 -0
- package/lib/wechat/node.js +1040 -0
- package/package.json +9 -4
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# 自建隧道服务器搭建教程
|
|
2
|
+
|
|
3
|
+
dsh-bridge 的「自建隧道」功能需要一台有公网 IP 的服务器来中转流量。本教程提供一键部署脚本,执行完成后直接复制输出的配置信息填入 dsh-bridge 即可。
|
|
4
|
+
|
|
5
|
+
## 前置条件
|
|
6
|
+
|
|
7
|
+
- 一台运行 Linux 的公网服务器(VPS、云主机均可,国内外皆可)
|
|
8
|
+
- 服务器有 root 权限
|
|
9
|
+
- Node.js 18+ 会**自动安装**,无需手动准备
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 一键部署(推荐)
|
|
14
|
+
|
|
15
|
+
在服务器上以 root 身份执行以下命令:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/wenbin-wb/dsh-bridge/main/scripts/install-tunnel-server.sh)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
脚本会自动完成:
|
|
22
|
+
|
|
23
|
+
- 检测并安装 Node.js 22 LTS
|
|
24
|
+
- 自动获取服务器公网 IP,自动选择可用端口
|
|
25
|
+
- 随机生成 16 位访问路径 + 32 位连接令牌(双重保护)
|
|
26
|
+
- 部署隧道服务端到 `/opt/dsh-tunnel`
|
|
27
|
+
- 注册 systemd 服务并设为开机自启
|
|
28
|
+
- 放行防火墙端口(支持 ufw / firewalld)
|
|
29
|
+
- 验证服务是否正常运行
|
|
30
|
+
|
|
31
|
+
执行完成后,终端会打印如下信息:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ DSH 设置 → 远程访问 → 自建隧道 │
|
|
36
|
+
├──────────────────────────────────────────────────────────────┤
|
|
37
|
+
│ WebSocket 地址 │
|
|
38
|
+
│ ws://YOUR_IP:3000/connect │
|
|
39
|
+
│ │
|
|
40
|
+
│ 访问令牌 │
|
|
41
|
+
│ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx │
|
|
42
|
+
└──────────────────────────────────────────────────────────────┘
|
|
43
|
+
|
|
44
|
+
连接成功后,dsh-bridge 会显示以下公网访问地址:
|
|
45
|
+
http://YOUR_IP:3000/a7f2k9m3x1b4c8d2
|
|
46
|
+
|
|
47
|
+
安全说明:该地址含随机路径(/a7f2k9m3x1b4c8d2),不知道路径无法访问。
|
|
48
|
+
端口扫描只能看到 404,不会暴露 DSH 的存在。
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
将 WebSocket 地址和访问令牌复制到 DSH 设置页「远程访问 → 自建隧道」,保存后点「开启」即可。
|
|
52
|
+
|
|
53
|
+
### 安全设计说明
|
|
54
|
+
|
|
55
|
+
脚本部署的服务端采用**双重保护**机制:
|
|
56
|
+
|
|
57
|
+
| 层 | 机制 | 说明 |
|
|
58
|
+
|----|------|------|
|
|
59
|
+
| WebSocket 连接层 | 连接令牌(TOKEN) | dsh-bridge 连接服务端时验证,防止他人建立隧道连接 |
|
|
60
|
+
| HTTP 访问层 | 随机路径前缀 | 所有不含该路径的请求返回 404,端口扫描看不出服务存在 |
|
|
61
|
+
|
|
62
|
+
最终对外的公网地址形如 `http://IP:PORT/a7f2k9m3x1b4c8d2`,路径本身即是密钥——不知道完整地址就无法访问你的 DSH。
|
|
63
|
+
|
|
64
|
+
### 自定义参数(可选)
|
|
65
|
+
|
|
66
|
+
如需自定义端口、令牌或指定域名,可通过环境变量传入:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 自定义端口
|
|
70
|
+
PORT=8080 bash <(curl -fsSL ...)
|
|
71
|
+
|
|
72
|
+
# 自定义令牌
|
|
73
|
+
TOKEN=my-secret-token bash <(curl -fsSL ...)
|
|
74
|
+
|
|
75
|
+
# 有域名时指定(会自动使用 wss:// 安全连接)
|
|
76
|
+
DOMAIN=tunnel.example.com bash <(curl -fsSL ...)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 日常管理
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 查看服务状态
|
|
83
|
+
systemctl status dsh-tunnel
|
|
84
|
+
|
|
85
|
+
# 查看实时日志
|
|
86
|
+
journalctl -u dsh-tunnel -f
|
|
87
|
+
|
|
88
|
+
# 重启服务
|
|
89
|
+
systemctl restart dsh-tunnel
|
|
90
|
+
|
|
91
|
+
# 修改配置后重启
|
|
92
|
+
nano /opt/dsh-tunnel/.env
|
|
93
|
+
systemctl restart dsh-tunnel
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 启用 HTTPS / WSS(可选,有域名时推荐)
|
|
99
|
+
|
|
100
|
+
默认部署使用 HTTP,如果你有域名并配置了 SSL 证书,可以通过 Nginx 反向代理启用 HTTPS,连接更安全稳定。
|
|
101
|
+
|
|
102
|
+
### 安装 Nginx 和 Certbot
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Debian/Ubuntu
|
|
106
|
+
apt-get install -y nginx certbot python3-certbot-nginx
|
|
107
|
+
|
|
108
|
+
# CentOS/Rocky
|
|
109
|
+
dnf install -y nginx certbot python3-certbot-nginx
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 申请证书并配置 Nginx
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 申请免费 Let's Encrypt 证书
|
|
116
|
+
certbot --nginx -d tunnel.example.com
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Nginx 配置 `/etc/nginx/sites-available/dsh-tunnel`(Debian 系):
|
|
120
|
+
|
|
121
|
+
```nginx
|
|
122
|
+
server {
|
|
123
|
+
listen 443 ssl;
|
|
124
|
+
server_name tunnel.example.com;
|
|
125
|
+
|
|
126
|
+
ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
|
|
127
|
+
ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;
|
|
128
|
+
|
|
129
|
+
location / {
|
|
130
|
+
proxy_pass http://127.0.0.1:3000;
|
|
131
|
+
proxy_http_version 1.1;
|
|
132
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
133
|
+
proxy_set_header Connection "upgrade";
|
|
134
|
+
proxy_set_header Host $host;
|
|
135
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
136
|
+
proxy_read_timeout 3600s;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
server {
|
|
141
|
+
listen 80;
|
|
142
|
+
server_name tunnel.example.com;
|
|
143
|
+
return 301 https://$host$request_uri;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
启用并重启 Nginx:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
ln -s /etc/nginx/sites-available/dsh-tunnel /etc/nginx/sites-enabled/
|
|
151
|
+
nginx -t && systemctl restart nginx
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
配置完成后,修改 `/opt/dsh-tunnel/.env`:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
PUBLIC_URL=https://tunnel.example.com
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
重启服务并更新 dsh-bridge 中的地址为 `wss://tunnel.example.com/connect`:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
systemctl restart dsh-tunnel
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 工作原理
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
手机/外网设备
|
|
172
|
+
│ HTTPS 请求
|
|
173
|
+
▼
|
|
174
|
+
隧道服务端(你的公网服务器)
|
|
175
|
+
│ WebSocket 实时转发
|
|
176
|
+
▼
|
|
177
|
+
dsh-bridge(你的本地电脑,主动发起连接)
|
|
178
|
+
│ HTTP 本地转发
|
|
179
|
+
▼
|
|
180
|
+
DSH(127.0.0.1:3080)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
本地电脑主动连接到服务端,**本地无需开放任何端口**,防火墙不需要特殊配置。
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 常见问题
|
|
188
|
+
|
|
189
|
+
**连接超时 / 无法连接**
|
|
190
|
+
|
|
191
|
+
1. 检查服务器**云控制台安全组**是否放行了端口(默认 3000)——云主机有两层防火墙:云安全组(控制台配置)和系统防火墙(ufw/firewalld),两层都要放行
|
|
192
|
+
2. 确认服务正在运行:`systemctl status dsh-tunnel`
|
|
193
|
+
3. 验证健康检查(在服务器本机执行):`curl http://127.0.0.1:3000/healthz`,应返回 `{"ok":true,...}`
|
|
194
|
+
4. 外网访问 `healthz` 返回 404 是正常的(安全设计,防止探测)
|
|
195
|
+
|
|
196
|
+
**历史记录加载失败 / 提示 "The user aborted a request."**
|
|
197
|
+
|
|
198
|
+
历史消息较多时,API 响应时间较长。请确保服务端版本 ≥ 1.0.6(脚本已将 API 请求超时提升至 120s)。重新运行安装脚本即可更新:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/wenbin-wb/dsh-bridge/main/scripts/install-tunnel-server.sh)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**重装后端口变了,安全组不匹配**
|
|
205
|
+
|
|
206
|
+
插件会自动保留已有配置(令牌/端口/路径不变)。若出现端口变化,通常是上次重装时旧进程仍在占用端口导致自动选了新端口。脚本 1.0.6 起已修复:清理旧进程提前到端口检测之前,重装时始终复用原端口。
|
|
207
|
+
|
|
208
|
+
WebSocket 长连接需要较长的超时配置。如果使用 Nginx,确保 `proxy_read_timeout` 设为 `3600s`。
|
|
209
|
+
|
|
210
|
+
**页面加载缓慢**
|
|
211
|
+
|
|
212
|
+
自建隧道的速度取决于服务器带宽和网络延迟。国内用户建议选择国内云服务器节点。
|
|
213
|
+
|
|
214
|
+
**重新部署 / 更新**
|
|
215
|
+
|
|
216
|
+
重新执行一键部署命令即可,会覆盖旧版本并保留配置文件:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/wenbin-wb/dsh-bridge/main/scripts/install-tunnel-server.sh)
|
|
220
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 微信 Bot 使用说明
|
|
2
|
+
|
|
3
|
+
> dsh-bridge 的微信 Bot(ClawBot / iLink)让你**直接在微信里**与你的 DeepSeek Harness agent 对话、控制和审批——无需公网、无需隧道,走腾讯官方 iLink Bot API。
|
|
4
|
+
|
|
5
|
+
扫码登录微信个人号后,发消息就能驱动 agent;`/` 开头的命令管理会话、切换工作区、审批权限。会话与工作区信息跨重启持久化,不会丢。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、快速开始
|
|
10
|
+
|
|
11
|
+
1. 打开 DSH 设置页 → 「远程访问」→ 「IM 机器人」标签页 → 选中「微信」平台
|
|
12
|
+
2. 点击「扫码登录」,用微信扫二维码并按提示确认
|
|
13
|
+
3. 登录成功后,**向该微信 Bot 发送第一条消息,自动完成白名单授权**(一步到位)
|
|
14
|
+
4. 之后就可以直接在微信里下命令、发消息了
|
|
15
|
+
|
|
16
|
+
> 💡 登录后左侧「IM 机器人」标签会出现绿色圆点,表示微信 Bot 已连接。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、基本对话
|
|
21
|
+
|
|
22
|
+
直接发送普通文本即可与当前活动的 agent 对话。支持发送**图片 / 文件 / 语音**(语音自动转文字),agent 也能在需要时把文件发回给你。
|
|
23
|
+
|
|
24
|
+
- 长回复会自动**分多条发送**,并在消息间留出间隔,避免刷屏
|
|
25
|
+
- 任务处理中会周期性发送「[处理中] 第 N 轮 | …」的心跳进度
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 三、命令一览
|
|
30
|
+
|
|
31
|
+
| 命令 | 说明 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| *(普通文本)* | 发给当前活动 agent |
|
|
34
|
+
| `/sessions` | 列出所有会话(按**工作区**分组,显示**会话标题**) |
|
|
35
|
+
| `/use N` | 切换到会话 N |
|
|
36
|
+
| `/workspaces` | 列出可用工作区 |
|
|
37
|
+
| `/new <提示词>` | 在**当前工作区**新建会话并开始 |
|
|
38
|
+
| `/new <提示词> @N` | 在**编号 N 的工作区**新建会话 |
|
|
39
|
+
| `/new <提示词> @路径` | 在**指定目录**新建会话 |
|
|
40
|
+
| `/stop` | 停止当前任务 |
|
|
41
|
+
| `/status` | 查看当前 agent 状态与会话摘要 |
|
|
42
|
+
| `/yes` `/no`(或 `1`/`2`) | 回应权限审批请求 |
|
|
43
|
+
| `/start` | 首次扫码后自动开始一个会话 |
|
|
44
|
+
| `/help` | 查看全部命令 |
|
|
45
|
+
|
|
46
|
+
### 例子
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
/new 帮我重构这个项目的路由 # 默认工作区新建
|
|
50
|
+
/new 继续上周的 ypbin 开发 @2 # 在 2 号工作区新建
|
|
51
|
+
/sessions # 按工作区分组列出所有会话(带标题)
|
|
52
|
+
/use 3 # 切到 3 号会话继续聊
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 四、会话与工作区
|
|
58
|
+
|
|
59
|
+
### 会话持久化
|
|
60
|
+
|
|
61
|
+
- 会话数据保存到 DSH 的持久化存储,**重启 DSH 后会话不丢失**,下次直接发消息就能接着上次继续
|
|
62
|
+
- `/sessions` 会按工作区(项目目录)分组展示,并为每个会话显示**标题**(DSH 自动生成),方便区分
|
|
63
|
+
|
|
64
|
+
### 工作区(项目目录)
|
|
65
|
+
|
|
66
|
+
- `/workspaces` 列出可用的工作区
|
|
67
|
+
- 新建会话时用 `@N` 或 `@路径` 指定工作区,让 agent 在指定项目目录下工作
|
|
68
|
+
- 不指定则使用默认工作区(当前启动目录)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 五、权限审批
|
|
73
|
+
|
|
74
|
+
当 agent 需要执行敏感操作(改文件、跑命令、调用外部服务等)时,会在微信里给你发送一条审批问询:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
[待确认] 1. agent 想要执行某个操作
|
|
78
|
+
请回复 /yes(允许)或 /no(拒绝)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- 回复 `/yes` `/no`,或仅有一条待确认时直接回复 `1` / `2`
|
|
82
|
+
- 默认 **10 分钟**内未回复则**自动拒绝**(可在设置页「高级设置」调整审批超时)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 六、安全说明
|
|
87
|
+
|
|
88
|
+
- **强制白名单**:仅白名单内的微信用户能驱动 agent,其他人发的消息一律忽略、**绝不喂给模型**
|
|
89
|
+
- **审批默认拒绝**:未及时回复的权限请求自动拒绝
|
|
90
|
+
- **凭证加密存储**:经 DSH 凭证服务保存,不落配置明文
|
|
91
|
+
- **建议专用微信号**:iLink 每个账号只允许一个 poller,与 hermes-agent / OpenClaw 并存会互相 403;为避免影响主号,**请用专用微信小号承载 Bot**
|
|
92
|
+
|
|
93
|
+
> 声明:iLink 为腾讯官方开放通道,仍需遵守《微信 ClawBot 功能使用条款》,腾讯保留内容过滤和限速的权利。不建议用于核心业务。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 七、注意事项
|
|
98
|
+
|
|
99
|
+
- **解绑账号**:设置页微信卡片里点「解绑账号」可清除登录凭证,下次需重新扫码
|
|
100
|
+
- **高级设置**:设置页「高级设置」可调整心跳间隔、审批超时、每气泡字数、分块发送延迟
|
|
101
|
+
- 发送媒体文件时请耐心等待上传/下载,大文件耗时较长
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// DSH Bridge - RPC constants (dependency-free, safe to import from browser client)
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_RPC_CHANNEL = '/dsh-bridge';
|
|
4
|
+
|
|
5
|
+
export const BRIDGE_ENDPOINTS = {
|
|
6
|
+
getStatus: 'getStatus',
|
|
7
|
+
startCustomTunnel: 'startCustomTunnel',
|
|
8
|
+
stopCustomTunnel: 'stopCustomTunnel',
|
|
9
|
+
startCloudflared: 'startCloudflared',
|
|
10
|
+
stopCloudflared: 'stopCloudflared',
|
|
11
|
+
resetCloudflared: 'resetCloudflared',
|
|
12
|
+
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
13
|
+
checkVersion: 'checkVersion',
|
|
14
|
+
wechatGetStatus: 'wechatGetStatus',
|
|
15
|
+
wechatLogin: 'wechatLogin',
|
|
16
|
+
wechatSetAllowFrom: 'wechatSetAllowFrom',
|
|
17
|
+
wechatSetConfig: 'wechatSetConfig',
|
|
18
|
+
wechatStop: 'wechatStop',
|
|
19
|
+
wechatStart: 'wechatStart',
|
|
20
|
+
wechatUnbind: 'wechatUnbind',
|
|
21
|
+
};
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
// DSH Bridge - RPC Interface
|
|
1
|
+
// DSH Bridge - RPC Interface (server side)
|
|
2
2
|
// Loopback-only RPC methods for browser UI
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import QRCode from 'qrcode';
|
|
5
|
+
import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from './bridge-rpc-constants.js';
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
getStatus: 'getStatus',
|
|
8
|
-
startCustomTunnel: 'startCustomTunnel',
|
|
9
|
-
stopCustomTunnel: 'stopCustomTunnel',
|
|
10
|
-
startCloudflared: 'startCloudflared',
|
|
11
|
-
stopCloudflared: 'stopCloudflared',
|
|
12
|
-
resetCloudflared: 'resetCloudflared',
|
|
13
|
-
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
14
|
-
checkVersion: 'checkVersion',
|
|
15
|
-
};
|
|
7
|
+
export { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS };
|
|
16
8
|
|
|
17
9
|
function ok(value) {
|
|
18
10
|
return { ok: true, value };
|
|
@@ -32,7 +24,37 @@ function fail(code, message, details = {}) {
|
|
|
32
24
|
};
|
|
33
25
|
}
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
// 把登录态里的二维码载荷渲染成浏览器可展示的 dataURL(带缓存,避免重复生成)
|
|
28
|
+
async function renderQr(loginState) {
|
|
29
|
+
if (!loginState?.qrPayload) return null;
|
|
30
|
+
const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
|
|
31
|
+
if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
|
|
32
|
+
let url = null;
|
|
33
|
+
const payload = loginState.qrPayload;
|
|
34
|
+
if (loginState.qrKind === 'img') {
|
|
35
|
+
url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
|
|
36
|
+
} else {
|
|
37
|
+
try {
|
|
38
|
+
url = await QRCode.toDataURL(payload, {
|
|
39
|
+
width: 300, margin: 2, color: { dark: '#1F2421', light: '#FFFFFF' },
|
|
40
|
+
});
|
|
41
|
+
} catch { url = null; }
|
|
42
|
+
}
|
|
43
|
+
renderQr.cache = { key: cacheKey, url };
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 归一化 wechat 状态返回:把 loginState 里的 qrPayload 渲染成 qr dataURL。 */
|
|
48
|
+
async function wechatStatusValue(wechatService, logger) {
|
|
49
|
+
const status = wechatService.getStatus();
|
|
50
|
+
const qr = await renderQr(status.login).catch((err) => {
|
|
51
|
+
logger.warn('dsh-bridge: render wechat qr failed: %s', err?.message ?? err);
|
|
52
|
+
return null;
|
|
53
|
+
});
|
|
54
|
+
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function installBridgeRpc(ctx, { service, wechat, logger, saveCustomTunnelConfig }) {
|
|
36
58
|
if (!ctx?.connection?.rpc?.handle) {
|
|
37
59
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
38
60
|
return () => {};
|
|
@@ -101,6 +123,60 @@ export function installBridgeRpc(ctx, { service, logger, saveCustomTunnelConfig
|
|
|
101
123
|
return ok(result);
|
|
102
124
|
}
|
|
103
125
|
|
|
126
|
+
// ---- 微信 Bot ----
|
|
127
|
+
|
|
128
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatGetStatus) {
|
|
129
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
130
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
131
|
+
return ok(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatLogin) {
|
|
135
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
136
|
+
const { qrType } = payload;
|
|
137
|
+
const result = await wechat.login({ qrType });
|
|
138
|
+
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
139
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
140
|
+
return ok(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatSetAllowFrom) {
|
|
144
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
145
|
+
await wechat.setAllowFrom(payload.allowFrom);
|
|
146
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
147
|
+
return ok(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatSetConfig) {
|
|
151
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
152
|
+
await wechat.setConfig(payload);
|
|
153
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
154
|
+
return ok(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatStop) {
|
|
158
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
159
|
+
await wechat.stop();
|
|
160
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
161
|
+
return ok(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
|
|
165
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
166
|
+
await wechat.gateway.start().catch((err) => {
|
|
167
|
+
logger.error('wechat start failed: %s', err?.message ?? err);
|
|
168
|
+
});
|
|
169
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
170
|
+
return ok(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatUnbind) {
|
|
174
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
175
|
+
await wechat.unbind();
|
|
176
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
177
|
+
return ok(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
104
180
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
105
181
|
} catch (err) {
|
|
106
182
|
logger.error('RPC endpoint %s failed: %s', endpoint, err.message);
|
package/lib/index.js
CHANGED
|
@@ -16,9 +16,11 @@ import QRCode from 'qrcode';
|
|
|
16
16
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
17
17
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
18
18
|
import { CloudflaredManager } from './cloudflared-manager.mjs';
|
|
19
|
+
import { WechatService } from './wechat/index.js';
|
|
19
20
|
|
|
20
21
|
const name = 'dsh-bridge';
|
|
21
|
-
|
|
22
|
+
// 微信 Bot 会话桥依赖 DSH 提供的会话/agent/审批/工作区/持久化服务,需显式 inject
|
|
23
|
+
const inject = ['connection', 'webServer', 'sessions', 'agents', 'approval', 'workspaceRegistry', 'sessionPersistence'];
|
|
22
24
|
|
|
23
25
|
// 从 package.json 动态读取版本号,发版只需改 package.json 一处
|
|
24
26
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
@@ -320,6 +322,9 @@ class BridgeService {
|
|
|
320
322
|
: null,
|
|
321
323
|
state: this.customTunnelState,
|
|
322
324
|
},
|
|
325
|
+
|
|
326
|
+
// 轻量摘要,供 UI Tab 状态点使用(完整状态由 wechatGetStatus 提供)
|
|
327
|
+
wechat: this.wechat ? { status: this.wechat.gateway?.status ?? 'idle' } : null,
|
|
323
328
|
};
|
|
324
329
|
}
|
|
325
330
|
|
|
@@ -478,8 +483,45 @@ function apply(ctx, config = {}) {
|
|
|
478
483
|
}
|
|
479
484
|
}).catch(() => {});
|
|
480
485
|
|
|
486
|
+
// 微信 Bot(ClawBot/iLink)
|
|
487
|
+
const wechat = new WechatService({
|
|
488
|
+
ctx,
|
|
489
|
+
logger,
|
|
490
|
+
config: config.wechat ?? {},
|
|
491
|
+
onPersist: async (patch) => {
|
|
492
|
+
const stored = await loadConfig();
|
|
493
|
+
stored.wechat = { ...(stored.wechat ?? {}), ...patch };
|
|
494
|
+
await saveConfig(stored);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// 启动时读取已保存的微信 Bot 配置(凭证 + 白名单 + 活动会话)
|
|
499
|
+
loadConfig().then((stored) => {
|
|
500
|
+
if (stored?.wechat) {
|
|
501
|
+
const cfg = stored.wechat;
|
|
502
|
+
wechat.node.config.allowFrom = Array.isArray(cfg.allowFrom) ? cfg.allowFrom : [];
|
|
503
|
+
// 恢复活动会话 ID(直接覆盖,不判断当前值)
|
|
504
|
+
if (cfg.activeSessionId) {
|
|
505
|
+
wechat.node.activeSessionId = cfg.activeSessionId;
|
|
506
|
+
logger.info('dsh-bridge: restored wechat active session: %s', cfg.activeSessionId);
|
|
507
|
+
} else {
|
|
508
|
+
// 没有持久化的会话时,回退到选第一个
|
|
509
|
+
wechat.node._pickDefaultSession().catch(() => {});
|
|
510
|
+
}
|
|
511
|
+
if (cfg.token && cfg.accountId) {
|
|
512
|
+
wechat.gateway.setCredentials({
|
|
513
|
+
token: cfg.token,
|
|
514
|
+
accountId: cfg.accountId,
|
|
515
|
+
baseUrl: cfg.baseUrl,
|
|
516
|
+
});
|
|
517
|
+
logger.info('dsh-bridge: loaded saved wechat bot config, starting polling');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}).catch(() => {});
|
|
521
|
+
|
|
481
522
|
const disposeRpc = installBridgeRpc(ctx, {
|
|
482
523
|
service,
|
|
524
|
+
wechat,
|
|
483
525
|
logger,
|
|
484
526
|
saveCustomTunnelConfig: async (serverUrl, accessToken) => {
|
|
485
527
|
const stored = await loadConfig();
|
|
@@ -496,8 +538,9 @@ function apply(ctx, config = {}) {
|
|
|
496
538
|
|
|
497
539
|
ctx.effect(() => async () => {
|
|
498
540
|
try { disposeRpc(); } catch {}
|
|
541
|
+
await wechat.destroy();
|
|
499
542
|
await service.dispose();
|
|
500
|
-
}, 'dsh-bridge: stop proxy and tunnels');
|
|
543
|
+
}, 'dsh-bridge: stop wechat, proxy and tunnels');
|
|
501
544
|
}
|
|
502
545
|
|
|
503
546
|
export { name, inject, apply };
|