@wenbin_wb/dsh-bridge 2.6.1 → 2.7.1
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 +136 -0
- package/README.en.md +40 -20
- package/README.md +40 -20
- package/client/client.js +568 -65
- package/client/index.js +485 -39
- package/docs/feishu-usage.md +1 -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 +5 -0
- package/lib/bridge-rpc.js +38 -1
- package/lib/cloudflared-manager.mjs +15 -1
- package/lib/index.js +295 -29
- 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/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 {
|
|
@@ -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
|
|
|
@@ -739,9 +742,9 @@ class BridgeService {
|
|
|
739
742
|
const isWin = process.platform === 'win32';
|
|
740
743
|
|
|
741
744
|
const tasks = [
|
|
742
|
-
{ cmd:
|
|
743
|
-
{ cmd:
|
|
744
|
-
{ cmd:
|
|
745
|
+
{ cmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
746
|
+
{ cmd: 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
747
|
+
{ cmd: 'npm', args: ['install', pkgSpec] },
|
|
745
748
|
];
|
|
746
749
|
|
|
747
750
|
let lastError = null;
|
|
@@ -749,32 +752,30 @@ class BridgeService {
|
|
|
749
752
|
for (const task of tasks) {
|
|
750
753
|
try {
|
|
751
754
|
const res = await new Promise((resolve, reject) => {
|
|
752
|
-
|
|
753
|
-
|
|
755
|
+
let cp;
|
|
756
|
+
try {
|
|
757
|
+
cp = spawn(task.cmd, task.args, {
|
|
754
758
|
windowsHide: true,
|
|
755
|
-
shell: false,
|
|
756
|
-
timeout:
|
|
757
|
-
});
|
|
758
|
-
let stdout = '';
|
|
759
|
-
let stderr = '';
|
|
760
|
-
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
761
|
-
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
762
|
-
cp.on('error', (err) => {
|
|
763
|
-
if (executable !== task.fallbackCmd) {
|
|
764
|
-
runExecutable(task.fallbackCmd);
|
|
765
|
-
} else {
|
|
766
|
-
reject(err);
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
cp.on('close', (code) => {
|
|
770
|
-
if (code === 0) {
|
|
771
|
-
resolve({ stdout, stderr });
|
|
772
|
-
} else {
|
|
773
|
-
reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
774
|
-
}
|
|
759
|
+
shell: isWin ? true : false,
|
|
760
|
+
timeout: 120000,
|
|
775
761
|
});
|
|
776
|
-
}
|
|
777
|
-
|
|
762
|
+
} catch (spawnErr) {
|
|
763
|
+
return reject(spawnErr);
|
|
764
|
+
}
|
|
765
|
+
let stdout = '';
|
|
766
|
+
let stderr = '';
|
|
767
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
768
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
769
|
+
cp.on('error', (err) => {
|
|
770
|
+
reject(err);
|
|
771
|
+
});
|
|
772
|
+
cp.on('close', (code) => {
|
|
773
|
+
if (code === 0) {
|
|
774
|
+
resolve({ stdout, stderr });
|
|
775
|
+
} else {
|
|
776
|
+
reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
777
|
+
}
|
|
778
|
+
});
|
|
778
779
|
});
|
|
779
780
|
|
|
780
781
|
const output = res.stdout || res.stderr || '升级成功';
|
|
@@ -787,6 +788,214 @@ class BridgeService {
|
|
|
787
788
|
return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
|
|
788
789
|
}
|
|
789
790
|
|
|
791
|
+
// 优雅重启 DSH 服务(支持守护进程自动拉起或独立派生子进程重启)
|
|
792
|
+
async restartDsh() {
|
|
793
|
+
this.logger?.info('收到 DSH 重启请求,正在调度重启...');
|
|
794
|
+
setTimeout(() => {
|
|
795
|
+
try {
|
|
796
|
+
if (process.env.DSH_DAEMON || process.env.PM2_HOME) {
|
|
797
|
+
process.exit(0);
|
|
798
|
+
} else {
|
|
799
|
+
// 常规 Node/CLI 模式:派生与当前参数一致的独立后台子进程并退出当前进程
|
|
800
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
801
|
+
cwd: process.cwd(),
|
|
802
|
+
env: process.env,
|
|
803
|
+
detached: true,
|
|
804
|
+
stdio: 'ignore',
|
|
805
|
+
windowsHide: false,
|
|
806
|
+
});
|
|
807
|
+
child.unref();
|
|
808
|
+
process.exit(0);
|
|
809
|
+
}
|
|
810
|
+
} catch (err) {
|
|
811
|
+
this.logger?.error('派生重启进程失败: %s,执行直接退出', err.message);
|
|
812
|
+
process.exit(0);
|
|
813
|
+
}
|
|
814
|
+
}, 600);
|
|
815
|
+
|
|
816
|
+
return { ok: true, message: 'DSH 服务正在重启中,前端将在几秒后自动重新连接…' };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
getSystemMetrics() {
|
|
820
|
+
try {
|
|
821
|
+
const totalMem = totalmem();
|
|
822
|
+
const freeMem = freemem();
|
|
823
|
+
const usedMem = totalMem - freeMem;
|
|
824
|
+
const memUsage = process.memoryUsage();
|
|
825
|
+
const cpusList = cpus() || [];
|
|
826
|
+
const cpuCount = cpusList.length;
|
|
827
|
+
const cpuModel = cpusList[0]?.model || 'Generic CPU';
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
os: {
|
|
831
|
+
platform: platform(),
|
|
832
|
+
arch: arch(),
|
|
833
|
+
release: release(),
|
|
834
|
+
hostname: hostname(),
|
|
835
|
+
nodeVersion: process.version,
|
|
836
|
+
},
|
|
837
|
+
uptime: {
|
|
838
|
+
processSec: Math.floor(process.uptime()),
|
|
839
|
+
systemSec: Math.floor(uptime()),
|
|
840
|
+
},
|
|
841
|
+
cpu: {
|
|
842
|
+
model: cpuModel,
|
|
843
|
+
cores: cpuCount,
|
|
844
|
+
loadAvg: typeof loadavg === 'function' ? loadavg() : [0, 0, 0],
|
|
845
|
+
},
|
|
846
|
+
memory: {
|
|
847
|
+
totalBytes: totalMem,
|
|
848
|
+
freeBytes: freeMem,
|
|
849
|
+
usedBytes: usedMem,
|
|
850
|
+
usedPercent: Math.round((usedMem / totalMem) * 100),
|
|
851
|
+
processHeapUsed: memUsage.heapUsed,
|
|
852
|
+
processRss: memUsage.rss,
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async diagnoseNetwork() {
|
|
861
|
+
const results = [];
|
|
862
|
+
|
|
863
|
+
// 1. 本地代理端口检测
|
|
864
|
+
results.push({
|
|
865
|
+
item: 'local_proxy',
|
|
866
|
+
name: `本地反向代理端口 (${this.proxyPort})`,
|
|
867
|
+
status: this.proxy ? 'pass' : 'fail',
|
|
868
|
+
detail: this.proxy ? `正常运行中 (代理目标端口: ${this.dshPort})` : '代理未启动',
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// 2. 局域网网卡检测
|
|
872
|
+
const lanIp = selectLanIPv4();
|
|
873
|
+
results.push({
|
|
874
|
+
item: 'lan_interface',
|
|
875
|
+
name: '局域网 IP 分配与可用性',
|
|
876
|
+
status: lanIp ? 'pass' : 'warn',
|
|
877
|
+
detail: lanIp ? `检测到有效局域网 IPv4: ${lanIp}` : '未检测到活跃局域网 IPv4 地址 (可能未连接 Wi-Fi/以太网)',
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// 3. Cloudflare 边缘连通性测试
|
|
881
|
+
const cfStart = Date.now();
|
|
882
|
+
try {
|
|
883
|
+
await new Promise((resolve, reject) => {
|
|
884
|
+
const req = httpsGet('https://1.1.1.1', { timeout: 3500 }, (res) => {
|
|
885
|
+
res.resume();
|
|
886
|
+
resolve();
|
|
887
|
+
});
|
|
888
|
+
req.on('error', reject);
|
|
889
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (3.5s)')); });
|
|
890
|
+
});
|
|
891
|
+
const cfLatency = Date.now() - cfStart;
|
|
892
|
+
results.push({
|
|
893
|
+
item: 'cloudflare_edge',
|
|
894
|
+
name: 'Cloudflare Anycast 边缘网络',
|
|
895
|
+
status: 'pass',
|
|
896
|
+
latencyMs: cfLatency,
|
|
897
|
+
detail: `连接畅通 (延迟 ${cfLatency}ms)`,
|
|
898
|
+
});
|
|
899
|
+
} catch (err) {
|
|
900
|
+
results.push({
|
|
901
|
+
item: 'cloudflare_edge',
|
|
902
|
+
name: 'Cloudflare Anycast 边缘网络',
|
|
903
|
+
status: 'warn',
|
|
904
|
+
detail: `连接异常: ${err.message} (临时公网隧道可能受阻)`,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// 4. 国内 npm 高速镜像源 (npmmirror)
|
|
909
|
+
const npmStart = Date.now();
|
|
910
|
+
try {
|
|
911
|
+
await new Promise((resolve, reject) => {
|
|
912
|
+
const req = httpsGet('https://registry.npmmirror.com', { timeout: 3500 }, (res) => {
|
|
913
|
+
res.resume();
|
|
914
|
+
resolve();
|
|
915
|
+
});
|
|
916
|
+
req.on('error', reject);
|
|
917
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (3.5s)')); });
|
|
918
|
+
});
|
|
919
|
+
const npmLatency = Date.now() - npmStart;
|
|
920
|
+
results.push({
|
|
921
|
+
item: 'npmmirror',
|
|
922
|
+
name: '国内 npm 高速镜像源 (npmmirror)',
|
|
923
|
+
status: 'pass',
|
|
924
|
+
latencyMs: npmLatency,
|
|
925
|
+
detail: `连接畅通 (延迟 ${npmLatency}ms)`,
|
|
926
|
+
});
|
|
927
|
+
} catch (err) {
|
|
928
|
+
results.push({
|
|
929
|
+
item: 'npmmirror',
|
|
930
|
+
name: '国内 npm 高速镜像源 (npmmirror)',
|
|
931
|
+
status: 'warn',
|
|
932
|
+
detail: `连接超时或异常: ${err.message}`,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// 5. 自建隧道部署服务器连通性检测
|
|
937
|
+
const customServerUrl = this.customTunnelConfig?.serverUrl?.trim();
|
|
938
|
+
if (customServerUrl) {
|
|
939
|
+
const isRunning = Boolean(this.customTunnelClient?.running);
|
|
940
|
+
const ctStart = Date.now();
|
|
941
|
+
try {
|
|
942
|
+
const parsedUrl = new URL(customServerUrl);
|
|
943
|
+
const isSecure = parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'wss:';
|
|
944
|
+
const getter = isSecure ? httpsGet : httpGet;
|
|
945
|
+
const probeUrl = new URL(customServerUrl);
|
|
946
|
+
probeUrl.protocol = isSecure ? 'https:' : 'http:';
|
|
947
|
+
|
|
948
|
+
await new Promise((resolve, reject) => {
|
|
949
|
+
const req = getter(probeUrl.toString(), { timeout: 4000 }, (res) => {
|
|
950
|
+
res.resume();
|
|
951
|
+
resolve();
|
|
952
|
+
});
|
|
953
|
+
req.on('error', reject);
|
|
954
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('连接超时 (4.0s)')); });
|
|
955
|
+
});
|
|
956
|
+
const ctLatency = Date.now() - ctStart;
|
|
957
|
+
results.push({
|
|
958
|
+
item: 'custom_tunnel_server',
|
|
959
|
+
name: `自建隧道部署服务器 (${parsedUrl.hostname}${parsedUrl.port ? `:${parsedUrl.port}` : ''})`,
|
|
960
|
+
status: 'pass',
|
|
961
|
+
latencyMs: ctLatency,
|
|
962
|
+
detail: `服务器连通良好 (延迟 ${ctLatency}ms · 状态: ${isRunning ? '客户端在线运行中' : '待连接/就绪'})`,
|
|
963
|
+
});
|
|
964
|
+
} catch (err) {
|
|
965
|
+
if (isRunning) {
|
|
966
|
+
results.push({
|
|
967
|
+
item: 'custom_tunnel_server',
|
|
968
|
+
name: `自建隧道部署服务器 (${customServerUrl})`,
|
|
969
|
+
status: 'pass',
|
|
970
|
+
detail: '客户端在线运行中 (WebSocket 通道已建立)',
|
|
971
|
+
});
|
|
972
|
+
} else {
|
|
973
|
+
results.push({
|
|
974
|
+
item: 'custom_tunnel_server',
|
|
975
|
+
name: `自建隧道部署服务器 (${customServerUrl})`,
|
|
976
|
+
status: 'warn',
|
|
977
|
+
detail: `无法连通自建服务器: ${err.message}`,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
results.push({
|
|
983
|
+
item: 'custom_tunnel_server',
|
|
984
|
+
name: '自建隧道部署服务器',
|
|
985
|
+
status: 'pass',
|
|
986
|
+
detail: '未配置自建服务器(若已部署自建隧道可在「公网隧道」中配置)',
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const allPassed = results.every(r => r.status === 'pass');
|
|
991
|
+
return {
|
|
992
|
+
ok: true,
|
|
993
|
+
timestamp: new Date().toISOString(),
|
|
994
|
+
overall: allPassed ? 'healthy' : 'warning',
|
|
995
|
+
results,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
790
999
|
async dispose() {
|
|
791
1000
|
this.stopCustomTunnel();
|
|
792
1001
|
this.stopCloudflared();
|
|
@@ -1147,6 +1356,63 @@ function apply(ctx, config = {}) {
|
|
|
1147
1356
|
await saveConfig(stored);
|
|
1148
1357
|
service.customTunnelConfig = { serverUrl, accessToken };
|
|
1149
1358
|
},
|
|
1359
|
+
exportBackup: async () => {
|
|
1360
|
+
const stored = await loadConfig();
|
|
1361
|
+
return {
|
|
1362
|
+
version: VERSION,
|
|
1363
|
+
exportedAt: new Date().toISOString(),
|
|
1364
|
+
config: stored,
|
|
1365
|
+
};
|
|
1366
|
+
},
|
|
1367
|
+
importBackup: async (backup) => {
|
|
1368
|
+
if (!backup || typeof backup !== 'object' || !backup.config || typeof backup.config !== 'object') {
|
|
1369
|
+
throw new Error('无效的备份数据结构:缺少 config 节点');
|
|
1370
|
+
}
|
|
1371
|
+
const incoming = backup.config;
|
|
1372
|
+
await saveConfig(incoming);
|
|
1373
|
+
|
|
1374
|
+
// 重新载入 Auth
|
|
1375
|
+
if (incoming.auth) {
|
|
1376
|
+
if (incoming.auth.enabled != null) authManager.enabled = Boolean(incoming.auth.enabled);
|
|
1377
|
+
if (incoming.auth.mode) authManager.mode = incoming.auth.mode;
|
|
1378
|
+
if (incoming.auth.scope) authManager.scope = incoming.auth.scope;
|
|
1379
|
+
if (incoming.auth.adminPolicy) authManager.adminPolicy = incoming.auth.adminPolicy;
|
|
1380
|
+
if (incoming.auth.passwordHash) authManager.passwordHash = incoming.auth.passwordHash;
|
|
1381
|
+
if (incoming.auth.passwordSalt) authManager.passwordSalt = incoming.auth.passwordSalt;
|
|
1382
|
+
if (incoming.auth.adminPasswordHash) authManager.adminPasswordHash = incoming.auth.adminPasswordHash;
|
|
1383
|
+
if (incoming.auth.adminPasswordSalt) authManager.adminPasswordSalt = incoming.auth.adminPasswordSalt;
|
|
1384
|
+
if (incoming.auth.secretToken) authManager.secretToken = incoming.auth.secretToken;
|
|
1385
|
+
}
|
|
1386
|
+
// 重新载入 Tunnels
|
|
1387
|
+
if (incoming.cloudflared) {
|
|
1388
|
+
service.cloudflaredConfig = incoming.cloudflared;
|
|
1389
|
+
}
|
|
1390
|
+
if (incoming.customTunnel) {
|
|
1391
|
+
service.customTunnelConfig = incoming.customTunnel;
|
|
1392
|
+
}
|
|
1393
|
+
// 重新载入各 IM 平台白名单与配置
|
|
1394
|
+
if (incoming.wechat) wechat.node.config.allowFrom = incoming.wechat.allowFrom ?? [];
|
|
1395
|
+
if (incoming.qq) {
|
|
1396
|
+
qq.node.config.allowFrom = incoming.qq.allowFrom ?? [];
|
|
1397
|
+
if (incoming.qq.appId && incoming.qq.clientSecret) {
|
|
1398
|
+
qq.gateway.setCredentials({ appId: incoming.qq.appId, clientSecret: incoming.qq.clientSecret });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (incoming.feishu) {
|
|
1402
|
+
feishu.node.config.allowFrom = incoming.feishu.allowFrom ?? [];
|
|
1403
|
+
if (incoming.feishu.appId && incoming.feishu.appSecret) {
|
|
1404
|
+
feishu.gateway.setCredentials({ appId: incoming.feishu.appId, appSecret: incoming.feishu.appSecret });
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (incoming.telegram) {
|
|
1408
|
+
telegram.node.config.allowFrom = incoming.telegram.allowFrom ?? [];
|
|
1409
|
+
if (incoming.telegram.botToken) {
|
|
1410
|
+
telegram.gateway.setCredentials({ botToken: incoming.telegram.botToken, proxy: incoming.telegram.proxy || '' });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return { ok: true, message: '配置已成功导入并刷新生效!' };
|
|
1415
|
+
},
|
|
1150
1416
|
});
|
|
1151
1417
|
|
|
1152
1418
|
// 代理随插件自动启动
|
|
@@ -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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
'',
|