@wenbin_wb/dsh-bridge 2.6.1 → 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.
@@ -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 {
@@ -14,6 +14,11 @@ export const BRIDGE_ENDPOINTS = {
14
14
  saveCustomTunnelConfig: 'saveCustomTunnelConfig',
15
15
  checkVersion: 'checkVersion',
16
16
  upgradePlugin: 'upgradePlugin',
17
+ restartDsh: 'restartDsh',
18
+ exportBackup: 'exportBackup',
19
+ importBackup: 'importBackup',
20
+ diagnoseNetwork: 'diagnoseNetwork',
21
+ getSystemMetrics: 'getSystemMetrics',
17
22
  // 访问安全认证(密码保护 / 扫码免密 Token)
18
23
  authGetStatus: 'authGetStatus',
19
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 () => {};
@@ -232,6 +232,43 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
232
232
  return ok(result);
233
233
  }
234
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
+
235
272
  // ---- 平台管理器(多 IM 平台)----
236
273
 
237
274
  if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
@@ -168,9 +168,15 @@ export class CloudflaredManager {
168
168
 
169
169
  let resolved = false;
170
170
 
171
+ let timeoutTimer = null;
172
+
171
173
  const tryResolve = () => {
172
174
  if (!resolved) {
173
175
  resolved = true;
176
+ if (timeoutTimer) {
177
+ clearTimeout(timeoutTimer);
178
+ timeoutTimer = null;
179
+ }
174
180
  resolve();
175
181
  }
176
182
  };
@@ -222,6 +228,10 @@ export class CloudflaredManager {
222
228
  });
223
229
 
224
230
  this.process.on('exit', (code) => {
231
+ if (timeoutTimer) {
232
+ clearTimeout(timeoutTimer);
233
+ timeoutTimer = null;
234
+ }
225
235
  this.process = null;
226
236
  this.url = null;
227
237
  if (!resolved) {
@@ -232,11 +242,15 @@ export class CloudflaredManager {
232
242
  });
233
243
 
234
244
  this.process.on('error', (err) => {
245
+ if (timeoutTimer) {
246
+ clearTimeout(timeoutTimer);
247
+ timeoutTimer = null;
248
+ }
235
249
  if (!resolved) reject(err);
236
250
  });
237
251
 
238
252
  // 连接超时 90 秒
239
- setTimeout(() => {
253
+ timeoutTimer = setTimeout(() => {
240
254
  if (!resolved) {
241
255
  this.stop();
242
256
  reject(new Error('等待隧道 URL 超时(90秒)'));
package/lib/index.js CHANGED
@@ -5,9 +5,9 @@
5
5
  // 2. Cloudflare 隧道(一键获取公网地址)
6
6
  // 3. 自建隧道(WebSocket 反向隧道 + Token 认证)
7
7
 
8
- import { createServer, request as httpRequest } from 'node:http';
8
+ import { createServer, request as httpRequest, get as httpGet } from 'node:http';
9
9
  import { get as httpsGet } from 'node:https';
10
- import { networkInterfaces, homedir } from 'node:os';
10
+ import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
11
11
  import { join, dirname } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { readFileSync } from 'node:fs';
@@ -562,6 +562,9 @@ class BridgeService {
562
562
 
563
563
  // 轻量摘要,供 UI Tab 状态点使用(完整状态由 wechatGetStatus 提供)
564
564
  wechat: this.wechat ? { status: this.wechat.gateway?.status ?? 'idle' } : null,
565
+
566
+ // 宿主系统运行监控指标
567
+ system: this.getSystemMetrics(),
565
568
  };
566
569
  }
567
570
 
@@ -787,6 +790,214 @@ class BridgeService {
787
790
  return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
788
791
  }
789
792
 
793
+ // 优雅重启 DSH 服务(支持守护进程自动拉起或独立派生子进程重启)
794
+ async restartDsh() {
795
+ this.logger?.info('收到 DSH 重启请求,正在调度重启...');
796
+ setTimeout(() => {
797
+ try {
798
+ if (process.env.DSH_DAEMON || process.env.PM2_HOME) {
799
+ process.exit(0);
800
+ } else {
801
+ // 常规 Node/CLI 模式:派生与当前参数一致的独立后台子进程并退出当前进程
802
+ const child = spawn(process.execPath, process.argv.slice(1), {
803
+ cwd: process.cwd(),
804
+ env: process.env,
805
+ detached: true,
806
+ stdio: 'ignore',
807
+ windowsHide: false,
808
+ });
809
+ child.unref();
810
+ process.exit(0);
811
+ }
812
+ } catch (err) {
813
+ this.logger?.error('派生重启进程失败: %s,执行直接退出', err.message);
814
+ process.exit(0);
815
+ }
816
+ }, 600);
817
+
818
+ return { ok: true, message: 'DSH 服务正在重启中,前端将在几秒后自动重新连接…' };
819
+ }
820
+
821
+ getSystemMetrics() {
822
+ try {
823
+ const totalMem = totalmem();
824
+ const freeMem = freemem();
825
+ const usedMem = totalMem - freeMem;
826
+ const memUsage = process.memoryUsage();
827
+ const cpusList = cpus() || [];
828
+ const cpuCount = cpusList.length;
829
+ const cpuModel = cpusList[0]?.model || 'Generic CPU';
830
+
831
+ return {
832
+ os: {
833
+ platform: platform(),
834
+ arch: arch(),
835
+ release: release(),
836
+ hostname: hostname(),
837
+ nodeVersion: process.version,
838
+ },
839
+ uptime: {
840
+ processSec: Math.floor(process.uptime()),
841
+ systemSec: Math.floor(uptime()),
842
+ },
843
+ cpu: {
844
+ model: cpuModel,
845
+ cores: cpuCount,
846
+ loadAvg: typeof loadavg === 'function' ? loadavg() : [0, 0, 0],
847
+ },
848
+ memory: {
849
+ totalBytes: totalMem,
850
+ freeBytes: freeMem,
851
+ usedBytes: usedMem,
852
+ usedPercent: Math.round((usedMem / totalMem) * 100),
853
+ processHeapUsed: memUsage.heapUsed,
854
+ processRss: memUsage.rss,
855
+ },
856
+ };
857
+ } catch {
858
+ return null;
859
+ }
860
+ }
861
+
862
+ async diagnoseNetwork() {
863
+ const results = [];
864
+
865
+ // 1. 本地代理端口检测
866
+ results.push({
867
+ item: 'local_proxy',
868
+ name: `本地反向代理端口 (${this.proxyPort})`,
869
+ status: this.proxy ? 'pass' : 'fail',
870
+ detail: this.proxy ? `正常运行中 (代理目标端口: ${this.dshPort})` : '代理未启动',
871
+ });
872
+
873
+ // 2. 局域网网卡检测
874
+ const lanIp = selectLanIPv4();
875
+ results.push({
876
+ item: 'lan_interface',
877
+ name: '局域网 IP 分配与可用性',
878
+ status: lanIp ? 'pass' : 'warn',
879
+ detail: lanIp ? `检测到有效局域网 IPv4: ${lanIp}` : '未检测到活跃局域网 IPv4 地址 (可能未连接 Wi-Fi/以太网)',
880
+ });
881
+
882
+ // 3. Cloudflare 边缘连通性测试
883
+ const cfStart = Date.now();
884
+ try {
885
+ await new Promise((resolve, reject) => {
886
+ const req = httpsGet('https://1.1.1.1', { timeout: 3500 }, (res) => {
887
+ res.resume();
888
+ resolve();
889
+ });
890
+ req.on('error', reject);
891
+ req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (3.5s)')); });
892
+ });
893
+ const cfLatency = Date.now() - cfStart;
894
+ results.push({
895
+ item: 'cloudflare_edge',
896
+ name: 'Cloudflare Anycast 边缘网络',
897
+ status: 'pass',
898
+ latencyMs: cfLatency,
899
+ detail: `连接畅通 (延迟 ${cfLatency}ms)`,
900
+ });
901
+ } catch (err) {
902
+ results.push({
903
+ item: 'cloudflare_edge',
904
+ name: 'Cloudflare Anycast 边缘网络',
905
+ status: 'warn',
906
+ detail: `连接异常: ${err.message} (临时公网隧道可能受阻)`,
907
+ });
908
+ }
909
+
910
+ // 4. 国内 npm 高速镜像源 (npmmirror)
911
+ const npmStart = Date.now();
912
+ try {
913
+ await new Promise((resolve, reject) => {
914
+ const req = httpsGet('https://registry.npmmirror.com', { timeout: 3500 }, (res) => {
915
+ res.resume();
916
+ resolve();
917
+ });
918
+ req.on('error', reject);
919
+ req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (3.5s)')); });
920
+ });
921
+ const npmLatency = Date.now() - npmStart;
922
+ results.push({
923
+ item: 'npmmirror',
924
+ name: '国内 npm 高速镜像源 (npmmirror)',
925
+ status: 'pass',
926
+ latencyMs: npmLatency,
927
+ detail: `连接畅通 (延迟 ${npmLatency}ms)`,
928
+ });
929
+ } catch (err) {
930
+ results.push({
931
+ item: 'npmmirror',
932
+ name: '国内 npm 高速镜像源 (npmmirror)',
933
+ status: 'warn',
934
+ detail: `连接超时或异常: ${err.message}`,
935
+ });
936
+ }
937
+
938
+ // 5. 自建隧道部署服务器连通性检测
939
+ const customServerUrl = this.customTunnelConfig?.serverUrl?.trim();
940
+ if (customServerUrl) {
941
+ const isRunning = Boolean(this.customTunnelClient?.running);
942
+ const ctStart = Date.now();
943
+ try {
944
+ const parsedUrl = new URL(customServerUrl);
945
+ const isSecure = parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'wss:';
946
+ const getter = isSecure ? httpsGet : httpGet;
947
+ const probeUrl = new URL(customServerUrl);
948
+ probeUrl.protocol = isSecure ? 'https:' : 'http:';
949
+
950
+ await new Promise((resolve, reject) => {
951
+ const req = getter(probeUrl.toString(), { timeout: 4000 }, (res) => {
952
+ res.resume();
953
+ resolve();
954
+ });
955
+ req.on('error', reject);
956
+ req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (4.0s)')); });
957
+ });
958
+ const ctLatency = Date.now() - ctStart;
959
+ results.push({
960
+ item: 'custom_tunnel_server',
961
+ name: `自建隧道部署服务器 (${parsedUrl.hostname}${parsedUrl.port ? `:${parsedUrl.port}` : ''})`,
962
+ status: 'pass',
963
+ latencyMs: ctLatency,
964
+ detail: `服务器连通良好 (延迟 ${ctLatency}ms · 状态: ${isRunning ? '客户端在线运行中' : '待连接/就绪'})`,
965
+ });
966
+ } catch (err) {
967
+ if (isRunning) {
968
+ results.push({
969
+ item: 'custom_tunnel_server',
970
+ name: `自建隧道部署服务器 (${customServerUrl})`,
971
+ status: 'pass',
972
+ detail: '客户端在线运行中 (WebSocket 通道已建立)',
973
+ });
974
+ } else {
975
+ results.push({
976
+ item: 'custom_tunnel_server',
977
+ name: `自建隧道部署服务器 (${customServerUrl})`,
978
+ status: 'warn',
979
+ detail: `无法连通自建服务器: ${err.message}`,
980
+ });
981
+ }
982
+ }
983
+ } else {
984
+ results.push({
985
+ item: 'custom_tunnel_server',
986
+ name: '自建隧道部署服务器',
987
+ status: 'pass',
988
+ detail: '未配置自建服务器(若已部署自建隧道可在「公网隧道」中配置)',
989
+ });
990
+ }
991
+
992
+ const allPassed = results.every(r => r.status === 'pass');
993
+ return {
994
+ ok: true,
995
+ timestamp: new Date().toISOString(),
996
+ overall: allPassed ? 'healthy' : 'warning',
997
+ results,
998
+ };
999
+ }
1000
+
790
1001
  async dispose() {
791
1002
  this.stopCustomTunnel();
792
1003
  this.stopCloudflared();
@@ -1147,6 +1358,63 @@ function apply(ctx, config = {}) {
1147
1358
  await saveConfig(stored);
1148
1359
  service.customTunnelConfig = { serverUrl, accessToken };
1149
1360
  },
1361
+ exportBackup: async () => {
1362
+ const stored = await loadConfig();
1363
+ return {
1364
+ version: VERSION,
1365
+ exportedAt: new Date().toISOString(),
1366
+ config: stored,
1367
+ };
1368
+ },
1369
+ importBackup: async (backup) => {
1370
+ if (!backup || typeof backup !== 'object' || !backup.config || typeof backup.config !== 'object') {
1371
+ throw new Error('无效的备份数据结构:缺少 config 节点');
1372
+ }
1373
+ const incoming = backup.config;
1374
+ await saveConfig(incoming);
1375
+
1376
+ // 重新载入 Auth
1377
+ if (incoming.auth) {
1378
+ if (incoming.auth.enabled != null) authManager.enabled = Boolean(incoming.auth.enabled);
1379
+ if (incoming.auth.mode) authManager.mode = incoming.auth.mode;
1380
+ if (incoming.auth.scope) authManager.scope = incoming.auth.scope;
1381
+ if (incoming.auth.adminPolicy) authManager.adminPolicy = incoming.auth.adminPolicy;
1382
+ if (incoming.auth.passwordHash) authManager.passwordHash = incoming.auth.passwordHash;
1383
+ if (incoming.auth.passwordSalt) authManager.passwordSalt = incoming.auth.passwordSalt;
1384
+ if (incoming.auth.adminPasswordHash) authManager.adminPasswordHash = incoming.auth.adminPasswordHash;
1385
+ if (incoming.auth.adminPasswordSalt) authManager.adminPasswordSalt = incoming.auth.adminPasswordSalt;
1386
+ if (incoming.auth.secretToken) authManager.secretToken = incoming.auth.secretToken;
1387
+ }
1388
+ // 重新载入 Tunnels
1389
+ if (incoming.cloudflared) {
1390
+ service.cloudflaredConfig = incoming.cloudflared;
1391
+ }
1392
+ if (incoming.customTunnel) {
1393
+ service.customTunnelConfig = incoming.customTunnel;
1394
+ }
1395
+ // 重新载入各 IM 平台白名单与配置
1396
+ if (incoming.wechat) wechat.node.config.allowFrom = incoming.wechat.allowFrom ?? [];
1397
+ if (incoming.qq) {
1398
+ qq.node.config.allowFrom = incoming.qq.allowFrom ?? [];
1399
+ if (incoming.qq.appId && incoming.qq.clientSecret) {
1400
+ qq.gateway.setCredentials({ appId: incoming.qq.appId, clientSecret: incoming.qq.clientSecret });
1401
+ }
1402
+ }
1403
+ if (incoming.feishu) {
1404
+ feishu.node.config.allowFrom = incoming.feishu.allowFrom ?? [];
1405
+ if (incoming.feishu.appId && incoming.feishu.appSecret) {
1406
+ feishu.gateway.setCredentials({ appId: incoming.feishu.appId, appSecret: incoming.feishu.appSecret });
1407
+ }
1408
+ }
1409
+ if (incoming.telegram) {
1410
+ telegram.node.config.allowFrom = incoming.telegram.allowFrom ?? [];
1411
+ if (incoming.telegram.botToken) {
1412
+ telegram.gateway.setCredentials({ botToken: incoming.telegram.botToken, proxy: incoming.telegram.proxy || '' });
1413
+ }
1414
+ }
1415
+
1416
+ return { ok: true, message: '配置已成功导入并刷新生效!' };
1417
+ },
1150
1418
  });
1151
1419
 
1152
1420
  // 代理随插件自动启动
@@ -241,6 +241,7 @@ export class ConversationBridge {
241
241
  // 配置恢复状态追踪(用于防止 handleInbound 在配置加载前处理消息)
242
242
  this._configRestored = false
243
243
  this._restoringConfig = null
244
+ this._restoringSessionMap = new Map() // sessionId -> Promise<Agent|null>
244
245
 
245
246
  this.mark = BRIDGE_MARK
246
247
  this._attachOutbound()
@@ -478,56 +479,75 @@ export class ConversationBridge {
478
479
 
479
480
  let agent = this.activeAgent()
480
481
  if (!agent && this.activeSessionId) {
481
- // agent 不在内存(DSH 重启后或切换到持久化会话)→ re-attach。
482
- try {
483
- const agentOptions = {}
484
- if (this.config.agentProvider) agentOptions.provider = this.config.agentProvider
485
- if (this.config.agentModel) agentOptions.model = this.config.agentModel
486
- if (!agentOptions.provider || !agentOptions.model) {
482
+ const sessionId = this.activeSessionId
483
+ if (this._restoringSessionMap.has(sessionId)) {
484
+ try {
485
+ await this._restoringSessionMap.get(sessionId)
486
+ } catch { /* 错误已在原始 Promise 中捕获 */ }
487
+ agent = this.activeAgent()
488
+ } else {
489
+ const restorePromise = (async () => {
490
+ // agent 不在内存(DSH 重启后或切换到持久化会话)→ re-attach。
487
491
  try {
488
- const def = this.ctx.get?.('agentDefaultModel')?.currentSelection?.()
489
- if (def) {
490
- if (!agentOptions.provider && def.provider) agentOptions.provider = def.provider
491
- if (!agentOptions.model && def.model) agentOptions.model = def.model
492
+ const agentOptions = {}
493
+ if (this.config.agentProvider) agentOptions.provider = this.config.agentProvider
494
+ if (this.config.agentModel) agentOptions.model = this.config.agentModel
495
+ if (!agentOptions.provider || !agentOptions.model) {
496
+ try {
497
+ const def = this.ctx.get?.('agentDefaultModel')?.currentSelection?.()
498
+ if (def) {
499
+ if (!agentOptions.provider && def.provider) agentOptions.provider = def.provider
500
+ if (!agentOptions.model && def.model) agentOptions.model = def.model
501
+ }
502
+ } catch { /* ignore */ }
492
503
  }
493
- } catch { /* ignore */ }
494
- }
495
504
 
496
- // 判断该 session 是否已持久化:已持久化 → agents.resume(从持久化加载历史恢复,
497
- // 避免 agents.create 用空 seed 与已持久化事件冲突);未持久化 → agents.create。
498
- let persisted = false
499
- try {
500
- const headers = await this.ctx.sessionPersistence?.list?.()
501
- persisted = Array.isArray(headers) && headers.some((h) => h?.id === this.activeSessionId)
502
- } catch { /* 读取失败则按未持久化处理 */ }
503
-
504
- let handle
505
- if (persisted) {
506
- handle = await this.ctx.agents.resume({
507
- resumeSessionId: this.activeSessionId,
508
- agentOptions,
509
- })
510
- } else {
511
- // 读取持久化会话的 cwd 做 fallback(新建会话时用)
512
- let sessionCwd = this.config.cwd || process.cwd()
513
- const meta = {
514
- cwd: sessionCwd,
515
- agentPreset: this.config.agentPreset || 'routing-suite',
505
+ // 判断该 session 是否已持久化:已持久化 → agents.resume(从持久化加载历史恢复,
506
+ // 避免 agents.create 用空 seed 与已持久化事件冲突);未持久化 → agents.create。
507
+ let persisted = false
508
+ try {
509
+ const headers = await this.ctx.sessionPersistence?.list?.()
510
+ persisted = Array.isArray(headers) && headers.some((h) => h?.id === sessionId)
511
+ } catch { /* 读取失败则按未持久化处理 */ }
512
+
513
+ let handle
514
+ if (persisted) {
515
+ handle = await this.ctx.agents.resume({
516
+ resumeSessionId: sessionId,
517
+ agentOptions,
518
+ })
519
+ } else {
520
+ // 读取持久化会话的 cwd 做 fallback(新建会话时用)
521
+ let sessionCwd = this.config.cwd || process.cwd()
522
+ const meta = {
523
+ cwd: sessionCwd,
524
+ agentPreset: this.config.agentPreset || 'routing-suite',
525
+ }
526
+ handle = await this.ctx.agents.create({
527
+ sessionId: sessionId,
528
+ meta,
529
+ agentOptions,
530
+ })
531
+ }
532
+ const resumedAgent = handle?.agent
533
+ this.logger?.info?.(`[dsh-bridge ${this.platform.id}] re-attached agent to session ${sessionId} (${persisted ? 'resume' : 'create'})`)
534
+ return resumedAgent
535
+ } catch (err) {
536
+ const reason = err instanceof Error ? err.message : String(err)
537
+ this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to re-attach agent: ${reason}`)
538
+ // 清除失效的 activeSessionId,避免用户误以为还在旧会话中
539
+ if (this.activeSessionId === sessionId) {
540
+ this.activeSessionId = null
541
+ }
542
+ await this.sendText(`❌ **恢复会话失败**:${reason}\n\n> 发送 \`/new <提示词>\` 可新建一个会话。`)
543
+ return null
516
544
  }
517
- handle = await this.ctx.agents.create({
518
- sessionId: this.activeSessionId,
519
- meta,
520
- agentOptions,
521
- })
522
- }
523
- agent = handle.agent
524
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] re-attached agent to session ${this.activeSessionId} (${persisted ? 'resume' : 'create'})`)
525
- } catch (err) {
526
- const reason = err instanceof Error ? err.message : String(err)
527
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to re-attach agent: ${reason}`)
528
- // 清除失效的 activeSessionId,避免用户误以为还在旧会话中
529
- this.activeSessionId = null
530
- await this.sendText(`❌ **恢复会话失败**:${reason}\n\n> 发送 \`/new <提示词>\` 可新建一个会话。`)
545
+ })().finally(() => {
546
+ this._restoringSessionMap.delete(sessionId)
547
+ })
548
+
549
+ this._restoringSessionMap.set(sessionId, restorePromise)
550
+ agent = await restorePromise
531
551
  }
532
552
  }
533
553
  if (!agent) {
@@ -908,6 +928,31 @@ async function routeCommand(node, text) {
908
928
  await node.sendText(`✓ **已切换到会话 #${index}**${titleLine}\n- **会话 ID**:\`${fmtSessionId(session.id)}\``)
909
929
  return true
910
930
  }
931
+ case 'rename': {
932
+ if (!node.activeSessionId) {
933
+ await node.sendText(`❌ **当前没有活动会话**\n\n> 请先使用 \`/sessions\` 查看会话列表并通过 \`/use 编号\` 切换到目标会话,或通过 \`/new <提示词>\` 创建新会话。`)
934
+ return true
935
+ }
936
+ const newTitle = rest.join(' ').trim()
937
+ if (!newTitle) {
938
+ await node.sendText(`❌ **缺少新标题参数**\n\n> 用法:\`/rename <新标题>\`\n> 示例:\`/rename 优化登录交互逻辑\``)
939
+ return true
940
+ }
941
+
942
+ try {
943
+ const session = node.activeSession()
944
+ if (session) {
945
+ session.title = newTitle
946
+ }
947
+ if (node.ctx.sessionPersistence?.update) {
948
+ await node.ctx.sessionPersistence.update(node.activeSessionId, { title: newTitle }).catch(() => {})
949
+ }
950
+ await node.sendText(`✓ **会话重命名成功**\n- **会话 ID**:\`${fmtSessionId(node.activeSessionId)}\`\n- **新标题**:${newTitle}`)
951
+ } catch (err) {
952
+ await node.sendText(`❌ **重命名失败**:${err instanceof Error ? err.message : String(err)}`)
953
+ }
954
+ return true
955
+ }
911
956
  case 'workspaces': {
912
957
  const workspaces = await listWorkspaces(node)
913
958
  if (workspaces.length === 0) {
@@ -1125,6 +1170,7 @@ function helpText() {
1125
1170
  '| `/use <编号>` | 切换到指定编号会话 | `/use 1` 或 `/resume 1` |',
1126
1171
  '| `/new <提示词>` | 在当前工作区新建会话 | `/new 帮我写个脚本` |',
1127
1172
  '| `/new <词> @N` | 在指定工作区新建会话 | `/new 帮我写个脚本 @1` |',
1173
+ '| `/rename <新标题>` | 重命名当前活动会话 | `/rename 优化登录交互` |',
1128
1174
  '| `/stop` | 中断停止当前正在执行的任务 | `/stop` |',
1129
1175
  '| `/end` | 结束当前会话(回到空闲) | `/end` |',
1130
1176
  '',
package/lib/qq/gateway.js CHANGED
@@ -174,11 +174,14 @@ export class QqGateway extends Service {
174
174
  }
175
175
 
176
176
  async start() {
177
+ if (this._startingPromise) return this._startingPromise
177
178
  if (!this.configured) { this.setStatus('idle'); return }
178
- if (!this.loopTask) {
179
+ if (this.loopTask) return
180
+ this._startingPromise = (async () => {
179
181
  this.stopRequested = false
180
182
  this.loopTask = this.runLoop().finally(() => { this.loopTask = null })
181
- }
183
+ })().finally(() => { this._startingPromise = null })
184
+ return this._startingPromise
182
185
  }
183
186
 
184
187
  async persist(patch) {