@wenbin_wb/dsh-bridge 2.6.0 → 2.7.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/CHANGELOG.md +127 -0
- package/README.en.md +65 -33
- package/README.md +65 -33
- package/client/client.js +748 -81
- package/client/index.js +634 -43
- package/docs/feishu-usage.md +17 -0
- package/docs/qq-usage.md +1 -0
- package/docs/telegram-usage.md +1 -0
- package/docs/wechat-usage.md +1 -0
- package/lib/auth/login-template.js +4 -0
- package/lib/bridge-rpc-constants.js +7 -0
- package/lib/bridge-rpc.js +58 -1
- package/lib/cloudflared-manager.mjs +54 -8
- package/lib/index.js +361 -13
- package/lib/platform/conversation-bridge.js +92 -46
- package/lib/qq/gateway.js +5 -2
- package/lib/telegram/gateway.js +25 -17
- package/lib/wechat/gateway.js +27 -13
- package/package.json +3 -2
package/docs/feishu-usage.md
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
| `/use <编号>` | 切换到指定编号会话 | `/use 1` 或 `/resume 1` |
|
|
85
85
|
| `/new <提示词>` | 在当前工作区创建新会话并开始 | `/new 帮我写个脚本` |
|
|
86
86
|
| `/new <词> @N` | 在指定工作区新建会话 | `/new 帮我写个脚本 @1` |
|
|
87
|
+
| `/rename <新标题>` | 重命名当前活动会话 | `/rename 优化登录交互` |
|
|
87
88
|
| `/workspaces` | 查看所有已注册的工作区列表 | `/workspaces` |
|
|
88
89
|
| `/status` | 查看 Agent 运行状态看板 | `/status` |
|
|
89
90
|
| `/stop` | 中断停止当前正在执行的任务 | `/stop` |
|
|
@@ -96,3 +97,19 @@
|
|
|
96
97
|
|
|
97
98
|

|
|
98
99
|
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## ❓ 常见问题 (FAQ)
|
|
103
|
+
|
|
104
|
+
### Q1: 点击卡片上的「批准」/「拒绝」按钮提示无权限或无反应?
|
|
105
|
+
请依次核对以下三项飞书开放平台配置:
|
|
106
|
+
1. **是否订阅了卡片交互事件**:在开放平台后台 **「事件与回调」➔「事件配置」** 中,必须添加 **`card.action.trigger`**(消息卡片回传交互)事件。若只添加了接收消息事件,卡片点击不会下发回调。
|
|
107
|
+
2. **是否发布了新版本生效**:飞书平台的所有权限与事件变更,**必须在「版本管理与发布」中「创建版本」并申请发布通过后才会生效**。
|
|
108
|
+
3. **应用可用范围**:在「版本管理与发布」中,确认应用可用范围包含了您当前的飞书账号(建议设为「所有员工」或将自己加入可用成员)。
|
|
109
|
+
4. **快速应急处理**:如果卡片按钮暂时受网络或配置影响,可直接在聊天中回复文字 **`/yes`**(或 `1`)批准,回复 **`/no`**(或 `2`)拒绝。
|
|
110
|
+
|
|
111
|
+
### Q2: 机器人无法在群聊中回复消息?
|
|
112
|
+
1. 确保在「权限管理」中开通了 **`im:message.group_msg`**(获取群组中所有消息)权限;
|
|
113
|
+
2. 确保在「版本管理与发布」中发布了包含该权限的新版本;
|
|
114
|
+
3. 将机器人拉入群聊后,需要 **`@机器人`** 唤醒并发送指令。
|
|
115
|
+
|
package/docs/qq-usage.md
CHANGED
|
@@ -184,6 +184,7 @@ AI 生成过程中,QQ 会显示机器人的"正在输入"状态:
|
|
|
184
184
|
- `/new <提示词>` - 新建会话并开始
|
|
185
185
|
- `/sessions`(或 `/list`)- 列出所有会话(按工作区分组)
|
|
186
186
|
- `/use N`(或 `/resume N`)- 切换到/恢复会话 N
|
|
187
|
+
- `/rename <新标题>` - 重命名当前活动会话
|
|
187
188
|
- `/end` - 结束当前会话(清除活动会话并触发快捷按钮)
|
|
188
189
|
- `/stop` - 停止当前任务
|
|
189
190
|
- `/status` - 查看状态与会话摘要
|
package/docs/telegram-usage.md
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
| *(普通文本)* | 发送给当前活动 Agent 执行任务 | 实时打字机流式输出 |
|
|
70
70
|
| `/new <提示词>` | 在当前工作区新建会话并立即执行 | 启动全新轮次 |
|
|
71
71
|
| `/new <提示词> @N` | 在指定工作区序号新建会话 | 多工作区调度 |
|
|
72
|
+
| `/rename <新标题>` | 重命名当前活动会话标题 | 修改会话名称 |
|
|
72
73
|
| `/sessions`(或 `/list`) | 查看所有会话列表 | 附带一键切换按键 |
|
|
73
74
|
| `/use N`(或 `/resume N`) | 切换活动会话至序号 N | 快速切换上下文 |
|
|
74
75
|
| `/workspaces` | 列出本地所有可用项目工作区 | 查看工作区路径 |
|
package/docs/wechat-usage.md
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
| `/new <提示词>` | 在**当前工作区**新建会话并开始 |
|
|
38
38
|
| `/new <提示词> @N` | 在**编号 N 的工作区**新建会话 |
|
|
39
39
|
| `/new <提示词> @路径` | 在**指定目录**新建会话 |
|
|
40
|
+
| `/rename <新标题>` | 重命名当前活动会话 |
|
|
40
41
|
| `/stop` | 停止当前任务 |
|
|
41
42
|
| `/status` | 查看当前 agent 状态与会话摘要 |
|
|
42
43
|
| `/yes` `/no`(或 `1`/`2`) | 回应权限审批请求 |
|
|
@@ -7,6 +7,10 @@ export function renderLoginPage({ error = '', hasPassword = true, locked = false
|
|
|
7
7
|
<head>
|
|
8
8
|
<meta charset="UTF-8">
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
10
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
12
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
13
|
+
<meta name="theme-color" content="#121413">
|
|
10
14
|
<title>远程访问认证 - DeepSeek Harness</title>
|
|
11
15
|
<style>
|
|
12
16
|
:root {
|
|
@@ -9,9 +9,16 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
9
9
|
startCloudflared: 'startCloudflared',
|
|
10
10
|
stopCloudflared: 'stopCloudflared',
|
|
11
11
|
resetCloudflared: 'resetCloudflared',
|
|
12
|
+
saveCloudflaredConfig: 'saveCloudflaredConfig',
|
|
13
|
+
setTunnelAutoStart: 'setTunnelAutoStart',
|
|
12
14
|
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
13
15
|
checkVersion: 'checkVersion',
|
|
14
16
|
upgradePlugin: 'upgradePlugin',
|
|
17
|
+
restartDsh: 'restartDsh',
|
|
18
|
+
exportBackup: 'exportBackup',
|
|
19
|
+
importBackup: 'importBackup',
|
|
20
|
+
diagnoseNetwork: 'diagnoseNetwork',
|
|
21
|
+
getSystemMetrics: 'getSystemMetrics',
|
|
15
22
|
// 访问安全认证(密码保护 / 扫码免密 Token)
|
|
16
23
|
authGetStatus: 'authGetStatus',
|
|
17
24
|
authUpdateConfig: 'authUpdateConfig',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -70,7 +70,7 @@ function checkAdminAuth(authManager, payload) {
|
|
|
70
70
|
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig }) {
|
|
73
|
+
export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
|
|
74
74
|
if (!ctx?.connection?.rpc?.handle) {
|
|
75
75
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
76
76
|
return () => {};
|
|
@@ -144,6 +144,26 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
144
144
|
return ok(status);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
|
|
148
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
149
|
+
if (adminErr) return adminErr;
|
|
150
|
+
|
|
151
|
+
const { token = '', hostname = '' } = payload;
|
|
152
|
+
await service.saveCloudflaredConfig({ token, hostname });
|
|
153
|
+
const status = await service.getStatus();
|
|
154
|
+
return ok(status);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
|
|
158
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
159
|
+
if (adminErr) return adminErr;
|
|
160
|
+
|
|
161
|
+
const { tunnel, autoStart } = payload;
|
|
162
|
+
await service.setTunnelAutoStart({ tunnel, autoStart });
|
|
163
|
+
const status = await service.getStatus();
|
|
164
|
+
return ok(status);
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
148
168
|
const adminErr = checkAdminAuth(authManager, payload);
|
|
149
169
|
if (adminErr) return adminErr;
|
|
@@ -212,6 +232,43 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
212
232
|
return ok(result);
|
|
213
233
|
}
|
|
214
234
|
|
|
235
|
+
if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
|
|
236
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
237
|
+
if (adminErr) return adminErr;
|
|
238
|
+
|
|
239
|
+
const result = await service.restartDsh();
|
|
240
|
+
return ok(result);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (endpoint === BRIDGE_ENDPOINTS.exportBackup) {
|
|
244
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
245
|
+
if (adminErr) return adminErr;
|
|
246
|
+
|
|
247
|
+
if (!exportBackup) return fail('bad-request', '备份导出服务不可用');
|
|
248
|
+
const backup = await exportBackup();
|
|
249
|
+
return ok(backup);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (endpoint === BRIDGE_ENDPOINTS.importBackup) {
|
|
253
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
254
|
+
if (adminErr) return adminErr;
|
|
255
|
+
|
|
256
|
+
if (!importBackup) return fail('bad-request', '备份导入服务不可用');
|
|
257
|
+
const result = await importBackup(payload?.backup);
|
|
258
|
+
const status = await service.getStatus({ adminAuthValid: true });
|
|
259
|
+
return ok({ result, status });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (endpoint === BRIDGE_ENDPOINTS.diagnoseNetwork) {
|
|
263
|
+
const result = await service.diagnoseNetwork();
|
|
264
|
+
return ok(result);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (endpoint === BRIDGE_ENDPOINTS.getSystemMetrics) {
|
|
268
|
+
const metrics = service.getSystemMetrics();
|
|
269
|
+
return ok(metrics);
|
|
270
|
+
}
|
|
271
|
+
|
|
215
272
|
// ---- 平台管理器(多 IM 平台)----
|
|
216
273
|
|
|
217
274
|
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
|
@@ -74,9 +74,11 @@ async function downloadFile(url, dest, onProgress) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export class CloudflaredManager {
|
|
77
|
-
constructor({ port, home, onStateChange, logger }) {
|
|
77
|
+
constructor({ port, home, token, hostname, onStateChange, logger }) {
|
|
78
78
|
this.port = port;
|
|
79
79
|
this.home = home || join(homedir(), '.dsh-bridge');
|
|
80
|
+
this.token = token ? String(token).trim() : null;
|
|
81
|
+
this.hostname = hostname ? String(hostname).trim() : null;
|
|
80
82
|
this.onStateChange = onStateChange;
|
|
81
83
|
this.logger = logger;
|
|
82
84
|
|
|
@@ -152,8 +154,13 @@ export class CloudflaredManager {
|
|
|
152
154
|
|
|
153
155
|
this._setState('connecting', '正在连接 Cloudflare...');
|
|
154
156
|
|
|
155
|
-
const args =
|
|
156
|
-
|
|
157
|
+
const args = this.token
|
|
158
|
+
? ['tunnel', 'run', '--token', this.token]
|
|
159
|
+
: ['tunnel', '--url', `http://127.0.0.1:${this.port}`];
|
|
160
|
+
|
|
161
|
+
// 隐藏日志中的 token 敏感字段
|
|
162
|
+
const safeArgs = this.token ? ['tunnel', 'run', '--token', '***'] : args;
|
|
163
|
+
this.logger?.info('启动 cloudflared: %s %s', this.binaryPath, safeArgs.join(' '));
|
|
157
164
|
|
|
158
165
|
this.process = spawn(this.binaryPath, args, {
|
|
159
166
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -161,20 +168,51 @@ export class CloudflaredManager {
|
|
|
161
168
|
|
|
162
169
|
let resolved = false;
|
|
163
170
|
|
|
171
|
+
let timeoutTimer = null;
|
|
172
|
+
|
|
164
173
|
const tryResolve = () => {
|
|
165
174
|
if (!resolved) {
|
|
166
175
|
resolved = true;
|
|
176
|
+
if (timeoutTimer) {
|
|
177
|
+
clearTimeout(timeoutTimer);
|
|
178
|
+
timeoutTimer = null;
|
|
179
|
+
}
|
|
167
180
|
resolve();
|
|
168
181
|
}
|
|
169
182
|
};
|
|
170
183
|
|
|
171
|
-
//
|
|
184
|
+
// 1. 命名/Token 隧道:通过握手日志判定就绪,使用预设固定域名
|
|
185
|
+
const parseNamedTunnel = (text) => {
|
|
186
|
+
if (!this.token) return;
|
|
187
|
+
if (
|
|
188
|
+
(text.includes('Registered tunnel') ||
|
|
189
|
+
text.includes('registered connIndex') ||
|
|
190
|
+
text.includes('Connection') && text.includes('registered') ||
|
|
191
|
+
text.includes('Updated to new configuration') ||
|
|
192
|
+
text.includes('Route propagated')) &&
|
|
193
|
+
!resolved
|
|
194
|
+
) {
|
|
195
|
+
let fixedUrl = this.hostname
|
|
196
|
+
? (this.hostname.startsWith('http') ? this.hostname : `https://${this.hostname}`)
|
|
197
|
+
: null;
|
|
198
|
+
this.url = fixedUrl;
|
|
199
|
+
this._setState('ready', fixedUrl ? `固定隧道已建立 (${fixedUrl})` : '固定隧道已建立');
|
|
200
|
+
this.logger?.info('cloudflared 固定隧道就绪: %s', this.url || 'Token 模式');
|
|
201
|
+
tryResolve();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// 2. 免费临时隧道:从 stdout/stderr 解析随机分配的 trycloudflare.com 域名
|
|
172
206
|
const parseUrl = (text) => {
|
|
207
|
+
if (this.token) {
|
|
208
|
+
parseNamedTunnel(text);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
173
211
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
174
212
|
if (match && !resolved) {
|
|
175
213
|
this.url = match[0];
|
|
176
|
-
this._setState('ready', '
|
|
177
|
-
this.logger?.info('cloudflared
|
|
214
|
+
this._setState('ready', '临时隧道已建立');
|
|
215
|
+
this.logger?.info('cloudflared 临时隧道就绪: %s', this.url);
|
|
178
216
|
tryResolve();
|
|
179
217
|
}
|
|
180
218
|
};
|
|
@@ -185,11 +223,15 @@ export class CloudflaredManager {
|
|
|
185
223
|
this.logger?.debug('cloudflared: %s', text.trim());
|
|
186
224
|
parseUrl(text);
|
|
187
225
|
if (text.includes('Registered tunnel') && !resolved) {
|
|
188
|
-
this._setState('connecting', '
|
|
226
|
+
this._setState('connecting', '隧道已注册,等待就绪...');
|
|
189
227
|
}
|
|
190
228
|
});
|
|
191
229
|
|
|
192
230
|
this.process.on('exit', (code) => {
|
|
231
|
+
if (timeoutTimer) {
|
|
232
|
+
clearTimeout(timeoutTimer);
|
|
233
|
+
timeoutTimer = null;
|
|
234
|
+
}
|
|
193
235
|
this.process = null;
|
|
194
236
|
this.url = null;
|
|
195
237
|
if (!resolved) {
|
|
@@ -200,11 +242,15 @@ export class CloudflaredManager {
|
|
|
200
242
|
});
|
|
201
243
|
|
|
202
244
|
this.process.on('error', (err) => {
|
|
245
|
+
if (timeoutTimer) {
|
|
246
|
+
clearTimeout(timeoutTimer);
|
|
247
|
+
timeoutTimer = null;
|
|
248
|
+
}
|
|
203
249
|
if (!resolved) reject(err);
|
|
204
250
|
});
|
|
205
251
|
|
|
206
252
|
// 连接超时 90 秒
|
|
207
|
-
setTimeout(() => {
|
|
253
|
+
timeoutTimer = setTimeout(() => {
|
|
208
254
|
if (!resolved) {
|
|
209
255
|
this.stop();
|
|
210
256
|
reject(new Error('等待隧道 URL 超时(90秒)'));
|