@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.
@@ -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
  ![飞书对话与卡片审批](screenshots/feishu-chat.jpg)
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` - 查看状态与会话摘要
@@ -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` | 列出本地所有可用项目工作区 | 查看工作区路径 |
@@ -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 = ['tunnel', '--url', `http://127.0.0.1:${this.port}`];
156
- this.logger?.info('启动 cloudflared: %s %s', this.binaryPath, args.join(' '));
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
- // URL 同时在 stdout 和 stderr 里找(不同版本行为不同)
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 隧道就绪: %s', this.url);
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', '隧道已注册,等待 URL...');
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秒)'));