@wenbin_wb/dsh-bridge 2.7.0 → 2.8.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 +36 -0
- package/README.en.md +31 -8
- package/README.md +31 -8
- package/client/client.js +1023 -5
- package/client/index.js +1120 -7
- package/docs/feishu-usage.md +1 -0
- package/docs/qq-usage.md +1 -0
- package/docs/screenshots/mobile-chat.jpg +0 -0
- package/docs/screenshots/mobile-drawer.jpg +0 -0
- package/docs/screenshots/mobile-remote-settings.jpg +0 -0
- package/docs/screenshots/mobile-settings-im.jpg +0 -0
- package/docs/screenshots/mobile-settings-lan.jpg +0 -0
- package/docs/screenshots/mobile-settings-security.jpg +0 -0
- package/docs/screenshots/mobile-settings-tunnel.jpg +0 -0
- package/docs/screenshots/mobile-workspace-picker.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/docs/telegram-usage.md +1 -0
- package/docs/wechat-usage.md +2 -0
- package/lib/bridge-rpc-constants.js +4 -0
- package/lib/bridge-rpc.js +41 -0
- package/lib/index.js +269 -29
- package/lib/platform/conversation-bridge.js +39 -1
- package/lib/security/path-validator.js +195 -0
- package/lib/security/rate-limiter.js +93 -0
- package/package.json +2 -2
package/docs/feishu-usage.md
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
| `/new <词> @N` | 在指定工作区新建会话 | `/new 帮我写个脚本 @1` |
|
|
87
87
|
| `/rename <新标题>` | 重命名当前活动会话 | `/rename 优化登录交互` |
|
|
88
88
|
| `/workspaces` | 查看所有已注册的工作区列表 | `/workspaces` |
|
|
89
|
+
| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 | `/addworkspace D:\projects\app` |
|
|
89
90
|
| `/status` | 查看 Agent 运行状态看板 | `/status` |
|
|
90
91
|
| `/stop` | 中断停止当前正在执行的任务 | `/stop` |
|
|
91
92
|
| `/end` | 结束当前会话回到空闲状态 | `/end` |
|
package/docs/qq-usage.md
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/telegram-usage.md
CHANGED
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
| `/sessions`(或 `/list`) | 查看所有会话列表 | 附带一键切换按键 |
|
|
74
74
|
| `/use N`(或 `/resume N`) | 切换活动会话至序号 N | 快速切换上下文 |
|
|
75
75
|
| `/workspaces` | 列出本地所有可用项目工作区 | 查看工作区路径 |
|
|
76
|
+
| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 | 自动绑定并生成快捷序号 |
|
|
76
77
|
| `/status` | 查看当前 Agent 运行状态看板 | 附带刷新/停止/结束按键 |
|
|
77
78
|
| `/stop` | 强制停止当前正在运行的 Agent 任务 | 即刻中断执行 |
|
|
78
79
|
| `/end` | 结束当前活动会话 | 挂载快捷开始按键 |
|
package/docs/wechat-usage.md
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
| *(普通文本)* | 发给当前活动 agent |
|
|
34
34
|
| `/sessions` | 列出所有会话(按**工作区**分组,显示**会话标题**) |
|
|
35
35
|
| `/use N` | 切换到会话 N |
|
|
36
|
+
| `/rename <新标题>` | 重命名当前活动会话 |
|
|
36
37
|
| `/workspaces` | 列出可用工作区 |
|
|
38
|
+
| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 |
|
|
37
39
|
| `/new <提示词>` | 在**当前工作区**新建会话并开始 |
|
|
38
40
|
| `/new <提示词> @N` | 在**编号 N 的工作区**新建会话 |
|
|
39
41
|
| `/new <提示词> @路径` | 在**指定目录**新建会话 |
|
|
@@ -19,6 +19,10 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
19
19
|
importBackup: 'importBackup',
|
|
20
20
|
diagnoseNetwork: 'diagnoseNetwork',
|
|
21
21
|
getSystemMetrics: 'getSystemMetrics',
|
|
22
|
+
// 远程工作区管理与目录浏览
|
|
23
|
+
listRemoteDirectories: 'listRemoteDirectories',
|
|
24
|
+
addRemoteWorkspace: 'addRemoteWorkspace',
|
|
25
|
+
listWorkspaces: 'listWorkspaces',
|
|
22
26
|
// 访问安全认证(密码保护 / 扫码免密 Token)
|
|
23
27
|
authGetStatus: 'authGetStatus',
|
|
24
28
|
authUpdateConfig: 'authUpdateConfig',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import QRCode from 'qrcode';
|
|
5
5
|
import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from './bridge-rpc-constants.js';
|
|
6
|
+
import { RateLimiter } from './security/rate-limiter.js';
|
|
6
7
|
|
|
7
8
|
export { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS };
|
|
8
9
|
|
|
10
|
+
const rpcRateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60000 });
|
|
11
|
+
|
|
9
12
|
function ok(value) {
|
|
10
13
|
return { ok: true, value };
|
|
11
14
|
}
|
|
@@ -269,6 +272,44 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
269
272
|
return ok(metrics);
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
// ---- 远程工作区管理与目录浏览 ----
|
|
276
|
+
|
|
277
|
+
if (endpoint === BRIDGE_ENDPOINTS.listRemoteDirectories) {
|
|
278
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
279
|
+
if (adminErr) return adminErr;
|
|
280
|
+
|
|
281
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
282
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 30);
|
|
283
|
+
if (!rateCheck.allowed) {
|
|
284
|
+
return fail('bad-request', `请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const result = await service.listRemoteDirectories(payload?.path);
|
|
288
|
+
return ok(result);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (endpoint === BRIDGE_ENDPOINTS.addRemoteWorkspace) {
|
|
292
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
293
|
+
if (adminErr) return adminErr;
|
|
294
|
+
|
|
295
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
296
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 20);
|
|
297
|
+
if (!rateCheck.allowed) {
|
|
298
|
+
return fail('bad-request', `添加工作区请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = await service.addWorkspace(payload?.path);
|
|
302
|
+
return ok(result);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (endpoint === BRIDGE_ENDPOINTS.listWorkspaces) {
|
|
306
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
307
|
+
if (adminErr) return adminErr;
|
|
308
|
+
|
|
309
|
+
const result = await service.getWorkspaces();
|
|
310
|
+
return ok(result);
|
|
311
|
+
}
|
|
312
|
+
|
|
272
313
|
// ---- 平台管理器(多 IM 平台)----
|
|
273
314
|
|
|
274
315
|
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
package/lib/index.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import { createServer, request as httpRequest, get as httpGet } from 'node:http';
|
|
9
9
|
import { get as httpsGet } from 'node:https';
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
|
-
import { join, dirname } from 'node:path';
|
|
11
|
+
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
|
-
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
14
|
+
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import QRCode from 'qrcode';
|
|
17
17
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
@@ -24,6 +24,7 @@ import { FeishuService } from './feishu/index.js';
|
|
|
24
24
|
import { TelegramService } from './telegram/index.js';
|
|
25
25
|
import { AuthManager } from './auth/manager.js';
|
|
26
26
|
import { renderLoginPage } from './auth/login-template.js';
|
|
27
|
+
import { isSafeWorkspacePath, isSensitiveFolderName } from './security/path-validator.js';
|
|
27
28
|
|
|
28
29
|
const name = 'dsh-bridge';
|
|
29
30
|
// 微信 Bot 会话桥依赖 DSH 提供的会话/agent/审批/工作区/持久化服务,需显式 inject
|
|
@@ -742,9 +743,9 @@ class BridgeService {
|
|
|
742
743
|
const isWin = process.platform === 'win32';
|
|
743
744
|
|
|
744
745
|
const tasks = [
|
|
745
|
-
{ cmd:
|
|
746
|
-
{ cmd:
|
|
747
|
-
{ cmd:
|
|
746
|
+
{ cmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
747
|
+
{ cmd: 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
748
|
+
{ cmd: 'npm', args: ['install', pkgSpec] },
|
|
748
749
|
];
|
|
749
750
|
|
|
750
751
|
let lastError = null;
|
|
@@ -752,32 +753,30 @@ class BridgeService {
|
|
|
752
753
|
for (const task of tasks) {
|
|
753
754
|
try {
|
|
754
755
|
const res = await new Promise((resolve, reject) => {
|
|
755
|
-
|
|
756
|
-
|
|
756
|
+
let cp;
|
|
757
|
+
try {
|
|
758
|
+
cp = spawn(task.cmd, task.args, {
|
|
757
759
|
windowsHide: true,
|
|
758
|
-
shell: false,
|
|
759
|
-
timeout:
|
|
760
|
-
});
|
|
761
|
-
let stdout = '';
|
|
762
|
-
let stderr = '';
|
|
763
|
-
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
764
|
-
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
765
|
-
cp.on('error', (err) => {
|
|
766
|
-
if (executable !== task.fallbackCmd) {
|
|
767
|
-
runExecutable(task.fallbackCmd);
|
|
768
|
-
} else {
|
|
769
|
-
reject(err);
|
|
770
|
-
}
|
|
760
|
+
shell: isWin ? true : false,
|
|
761
|
+
timeout: 120000,
|
|
771
762
|
});
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
763
|
+
} catch (spawnErr) {
|
|
764
|
+
return reject(spawnErr);
|
|
765
|
+
}
|
|
766
|
+
let stdout = '';
|
|
767
|
+
let stderr = '';
|
|
768
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
769
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
770
|
+
cp.on('error', (err) => {
|
|
771
|
+
reject(err);
|
|
772
|
+
});
|
|
773
|
+
cp.on('close', (code) => {
|
|
774
|
+
if (code === 0) {
|
|
775
|
+
resolve({ stdout, stderr });
|
|
776
|
+
} else {
|
|
777
|
+
reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
778
|
+
}
|
|
779
|
+
});
|
|
781
780
|
});
|
|
782
781
|
|
|
783
782
|
const output = res.stdout || res.stderr || '升级成功';
|
|
@@ -859,6 +858,247 @@ class BridgeService {
|
|
|
859
858
|
}
|
|
860
859
|
}
|
|
861
860
|
|
|
861
|
+
// 获取当前所有已注册的工作区
|
|
862
|
+
async getWorkspaces() {
|
|
863
|
+
try {
|
|
864
|
+
const list = await this.ctx?.workspaceRegistry?.list?.() ?? [];
|
|
865
|
+
const out = [];
|
|
866
|
+
for (const ws of list) {
|
|
867
|
+
if (ws && ws.path) {
|
|
868
|
+
out.push({
|
|
869
|
+
id: ws.id,
|
|
870
|
+
title: ws.title ?? basename(ws.path),
|
|
871
|
+
path: ws.path,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return out.sort((a, b) => String(a.path).localeCompare(String(b.path)));
|
|
876
|
+
} catch {
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// 远程添加工作区目录到 DSH 体系
|
|
882
|
+
async addWorkspace(workspacePath) {
|
|
883
|
+
if (!workspacePath || typeof workspacePath !== 'string') {
|
|
884
|
+
return { ok: false, error: '缺少工作区目录路径' };
|
|
885
|
+
}
|
|
886
|
+
const safetyCheck = await isSafeWorkspacePath(workspacePath);
|
|
887
|
+
if (!safetyCheck.valid) {
|
|
888
|
+
return { ok: false, error: safetyCheck.error || '路径安全校验未通过' };
|
|
889
|
+
}
|
|
890
|
+
const resolved = safetyCheck.path;
|
|
891
|
+
|
|
892
|
+
const title = basename(resolved) || resolved;
|
|
893
|
+
let added = false;
|
|
894
|
+
let workspaceId = null;
|
|
895
|
+
|
|
896
|
+
if (this.ctx?.workspaceRegistry) {
|
|
897
|
+
if (typeof this.ctx.workspaceRegistry.create === 'function') {
|
|
898
|
+
try {
|
|
899
|
+
const entity = await this.ctx.workspaceRegistry.create(resolved, title);
|
|
900
|
+
added = true;
|
|
901
|
+
workspaceId = entity?.id ?? null;
|
|
902
|
+
} catch (e) {
|
|
903
|
+
this.logger?.warn?.('workspaceRegistry.create 失败: %s', e.message);
|
|
904
|
+
}
|
|
905
|
+
} else if (typeof this.ctx.workspaceRegistry.add === 'function') {
|
|
906
|
+
try {
|
|
907
|
+
const res = await this.ctx.workspaceRegistry.add({ path: resolved, title });
|
|
908
|
+
added = true;
|
|
909
|
+
workspaceId = res?.id ?? null;
|
|
910
|
+
} catch (e) {
|
|
911
|
+
this.logger?.warn?.('workspaceRegistry.add 失败: %s', e.message);
|
|
912
|
+
}
|
|
913
|
+
} else if (typeof this.ctx.workspaceRegistry.register === 'function') {
|
|
914
|
+
try {
|
|
915
|
+
const res = await this.ctx.workspaceRegistry.register({ path: resolved, title });
|
|
916
|
+
added = true;
|
|
917
|
+
workspaceId = res?.id ?? null;
|
|
918
|
+
} catch (e) {
|
|
919
|
+
this.logger?.warn?.('workspaceRegistry.register 失败: %s', e.message);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const list = await this.getWorkspaces();
|
|
925
|
+
if (!workspaceId) {
|
|
926
|
+
const match = list.find(w => w.path === resolved || (w.path && w.path.toLowerCase() === resolved.toLowerCase()));
|
|
927
|
+
if (match) workspaceId = match.id;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
let sessionId = null;
|
|
931
|
+
if (this.ctx?.sessions && typeof this.ctx.sessions.create === 'function') {
|
|
932
|
+
try {
|
|
933
|
+
const session = this.ctx.sessions.create(undefined, { meta: { cwd: resolved } });
|
|
934
|
+
if (session?.id) {
|
|
935
|
+
sessionId = session.id;
|
|
936
|
+
if (workspaceId && this.ctx?.workspaceRegistry?.get) {
|
|
937
|
+
const entity = this.ctx.workspaceRegistry.get(workspaceId);
|
|
938
|
+
if (entity && typeof entity.attachSession === 'function') {
|
|
939
|
+
await entity.attachSession(session.id).catch(() => {});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} catch (e) {
|
|
944
|
+
this.logger?.debug?.('sessions.create 初始化 session 提示: %s', e.message);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
ok: true,
|
|
950
|
+
path: resolved,
|
|
951
|
+
title,
|
|
952
|
+
workspaceId,
|
|
953
|
+
sessionId,
|
|
954
|
+
workspaces: list,
|
|
955
|
+
registered: added
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 远程目录列表浏览与常用路径推荐
|
|
960
|
+
async listRemoteDirectories(targetPath) {
|
|
961
|
+
const isWin = process.platform === 'win32';
|
|
962
|
+
const home = homedir();
|
|
963
|
+
|
|
964
|
+
// 1. 获取快速访问常用根目录
|
|
965
|
+
const roots = [
|
|
966
|
+
{ name: '🏠 用户主目录', path: home },
|
|
967
|
+
];
|
|
968
|
+
const commonSubdirs = [
|
|
969
|
+
{ name: '💻 桌面', sub: 'Desktop' },
|
|
970
|
+
{ name: '📁 文档', sub: 'Documents' },
|
|
971
|
+
{ name: '📥 下载', sub: 'Downloads' },
|
|
972
|
+
{ name: '💡 IdeaProjects', sub: 'IdeaProjects' },
|
|
973
|
+
{ name: '🔨 Projects', sub: 'Projects' },
|
|
974
|
+
{ name: '📦 workspace', sub: 'workspace' },
|
|
975
|
+
{ name: '💻 code', sub: 'code' },
|
|
976
|
+
{ name: '💻 src', sub: 'src' },
|
|
977
|
+
];
|
|
978
|
+
for (const item of commonSubdirs) {
|
|
979
|
+
const fullPath = join(home, item.sub);
|
|
980
|
+
try {
|
|
981
|
+
const s = await stat(fullPath);
|
|
982
|
+
if (s.isDirectory()) {
|
|
983
|
+
roots.push({ name: item.name, path: fullPath });
|
|
984
|
+
}
|
|
985
|
+
} catch {}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 2. Windows 盘符探测
|
|
989
|
+
const drives = [];
|
|
990
|
+
if (isWin) {
|
|
991
|
+
const letters = 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
992
|
+
for (const letter of letters) {
|
|
993
|
+
const driveRoot = `${letter}:\\`;
|
|
994
|
+
try {
|
|
995
|
+
await access(driveRoot);
|
|
996
|
+
drives.push({ name: `${letter}: 盘`, path: driveRoot });
|
|
997
|
+
} catch {}
|
|
998
|
+
}
|
|
999
|
+
if (drives.length === 0) drives.push({ name: 'C: 盘', path: 'C:\\' });
|
|
1000
|
+
} else {
|
|
1001
|
+
drives.push({ name: '根目录 /', path: '/' });
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// 3. 解析当前请求路径并进行安全校验
|
|
1005
|
+
let rawTarget = targetPath && typeof targetPath === 'string' ? targetPath.trim() : '';
|
|
1006
|
+
if (isWin && /^[A-Za-z]:$/.test(rawTarget)) {
|
|
1007
|
+
rawTarget = `${rawTarget}\\`;
|
|
1008
|
+
}
|
|
1009
|
+
let candidatePath = rawTarget ? resolve(rawTarget) : home;
|
|
1010
|
+
|
|
1011
|
+
// 安全校验:遇非法或黑名单目录时安全回退至用户主目录
|
|
1012
|
+
let currentPath = home;
|
|
1013
|
+
const pathCheck = await isSafeWorkspacePath(candidatePath);
|
|
1014
|
+
if (pathCheck.valid && pathCheck.path) {
|
|
1015
|
+
currentPath = pathCheck.path;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// 4. 读取子文件夹列表(过滤敏感目录与不安全软链接)
|
|
1019
|
+
const entries = [];
|
|
1020
|
+
let readError = null;
|
|
1021
|
+
try {
|
|
1022
|
+
const dirents = await readdir(currentPath, { withFileTypes: true });
|
|
1023
|
+
for (const d of dirents) {
|
|
1024
|
+
if (isSensitiveFolderName(d.name)) continue;
|
|
1025
|
+
|
|
1026
|
+
let isDir = d.isDirectory();
|
|
1027
|
+
const targetEntryPath = join(currentPath, d.name);
|
|
1028
|
+
|
|
1029
|
+
// 如果是符号链接,安全探测其真实目标
|
|
1030
|
+
if (d.isSymbolicLink()) {
|
|
1031
|
+
try {
|
|
1032
|
+
const symCheck = await isSafeWorkspacePath(targetEntryPath);
|
|
1033
|
+
if (!symCheck.valid) continue;
|
|
1034
|
+
isDir = true;
|
|
1035
|
+
} catch {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (isDir) {
|
|
1041
|
+
entries.push({
|
|
1042
|
+
name: d.name,
|
|
1043
|
+
path: targetEntryPath,
|
|
1044
|
+
isDirectory: true,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
readError = err.message;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
1053
|
+
|
|
1054
|
+
const parentPath = dirname(currentPath) !== currentPath ? dirname(currentPath) : null;
|
|
1055
|
+
|
|
1056
|
+
// 5. 生成结构化面包屑导航路径
|
|
1057
|
+
const breadcrumbs = [];
|
|
1058
|
+
if (isWin) {
|
|
1059
|
+
const match = currentPath.match(/^([A-Za-z]:)(?:\\(.*))?$/);
|
|
1060
|
+
if (match) {
|
|
1061
|
+
const driveLetter = match[1];
|
|
1062
|
+
const rest = match[2] || '';
|
|
1063
|
+
breadcrumbs.push({ name: `${driveLetter}`, path: `${driveLetter}\\` });
|
|
1064
|
+
if (rest) {
|
|
1065
|
+
const parts = rest.split('\\').filter(Boolean);
|
|
1066
|
+
let curr = `${driveLetter}\\`;
|
|
1067
|
+
for (const p of parts) {
|
|
1068
|
+
curr = join(curr, p);
|
|
1069
|
+
breadcrumbs.push({ name: p, path: curr });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
} else {
|
|
1073
|
+
breadcrumbs.push({ name: currentPath, path: currentPath });
|
|
1074
|
+
}
|
|
1075
|
+
} else {
|
|
1076
|
+
breadcrumbs.push({ name: '根目录 /', path: '/' });
|
|
1077
|
+
const parts = currentPath.split('/').filter(Boolean);
|
|
1078
|
+
let curr = '/';
|
|
1079
|
+
for (const p of parts) {
|
|
1080
|
+
curr = join(curr, p);
|
|
1081
|
+
breadcrumbs.push({ name: p, path: curr });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// 6. 获取当前已注册的工作区作为快捷参考
|
|
1086
|
+
const currentWorkspaces = await this.getWorkspaces();
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
ok: !readError,
|
|
1090
|
+
error: readError ? `读取文件夹失败: ${readError}` : undefined,
|
|
1091
|
+
currentPath,
|
|
1092
|
+
parentPath,
|
|
1093
|
+
breadcrumbs,
|
|
1094
|
+
entries: entries.slice(0, 150),
|
|
1095
|
+
totalEntries: entries.length,
|
|
1096
|
+
roots,
|
|
1097
|
+
drives,
|
|
1098
|
+
workspaces: currentWorkspaces,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
862
1102
|
async diagnoseNetwork() {
|
|
863
1103
|
const results = [];
|
|
864
1104
|
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
21
21
|
import { randomUUID } from 'node:crypto'
|
|
22
22
|
import { statSync } from 'node:fs'
|
|
23
|
-
import {
|
|
23
|
+
import { stat } from 'node:fs/promises'
|
|
24
|
+
import { resolve, normalize, basename } from 'node:path'
|
|
25
|
+
import { isSafeWorkspacePath } from '../security/path-validator.js'
|
|
24
26
|
|
|
25
27
|
// 纯文本标记(用户偏好不用 emoji)
|
|
26
28
|
export const BRIDGE_MARK = {
|
|
@@ -974,6 +976,41 @@ async function routeCommand(node, text) {
|
|
|
974
976
|
].join('\n'))
|
|
975
977
|
return true
|
|
976
978
|
}
|
|
979
|
+
case 'addworkspace': {
|
|
980
|
+
const targetPath = rest.join(' ').trim()
|
|
981
|
+
if (!targetPath) {
|
|
982
|
+
await node.sendText(`❌ **缺少工作区路径**\n\n> 用法:\`/addworkspace <电脑绝对路径>\`\n> 示例:\`/addworkspace D:\\IdeaProjects\\my-app\``)
|
|
983
|
+
return true
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const safetyCheck = await isSafeWorkspacePath(targetPath)
|
|
987
|
+
if (!safetyCheck.valid) {
|
|
988
|
+
await node.sendText(`⚠️ **${safetyCheck.error || '路径安全校验未通过'}**:\`${targetPath}\`\n\n> 出于安全考虑,禁止将系统关键目录或敏感配置文件所在路径登记为工作区。`)
|
|
989
|
+
return true
|
|
990
|
+
}
|
|
991
|
+
const resolved = safetyCheck.path
|
|
992
|
+
const title = basename(resolved) || resolved
|
|
993
|
+
if (node.ctx.workspaceRegistry?.add) {
|
|
994
|
+
await node.ctx.workspaceRegistry.add({ path: resolved, title }).catch(() => {})
|
|
995
|
+
} else if (node.ctx.workspaceRegistry?.register) {
|
|
996
|
+
await node.ctx.workspaceRegistry.register({ path: resolved, title }).catch(() => {})
|
|
997
|
+
}
|
|
998
|
+
const workspaces = await listWorkspaces(node)
|
|
999
|
+
const foundIndex = workspaces.findIndex(w => normalize(w.path) === normalize(resolved))
|
|
1000
|
+
const numStr = foundIndex >= 0 ? `@${foundIndex + 1}` : ''
|
|
1001
|
+
await node.sendText([
|
|
1002
|
+
`✓ **工作区添加成功**!`,
|
|
1003
|
+
`- **名称**:${title}`,
|
|
1004
|
+
`- **路径**:\`${resolved}\``,
|
|
1005
|
+
foundIndex >= 0 ? `- **快捷编号**:\`${numStr}\`` : '',
|
|
1006
|
+
'',
|
|
1007
|
+
`> 发送 \`/new <提示词> ${numStr || '@' + resolved}\` 即可直接在此工作区创建会话。`,
|
|
1008
|
+
].filter(Boolean).join('\n'))
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
await node.sendText(`❌ **添加工作区失败**:${err instanceof Error ? err.message : String(err)}`)
|
|
1011
|
+
}
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
977
1014
|
case 'new': {
|
|
978
1015
|
// 解析尾部 @N 或 @路径 作为工作区 cwd
|
|
979
1016
|
const args = rest.join(' ').trim()
|
|
@@ -1178,6 +1215,7 @@ function helpText() {
|
|
|
1178
1215
|
'| 指令 | 说明 |',
|
|
1179
1216
|
'| :--- | :--- |',
|
|
1180
1217
|
'| `/workspaces` | 查看可用工作区表格列表 |',
|
|
1218
|
+
'| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 |',
|
|
1181
1219
|
'| `/status` | 查看 Agent 运行状态看板 |',
|
|
1182
1220
|
'| `/help` | 查看此帮助菜单 |',
|
|
1183
1221
|
'',
|