@wenbin_wb/dsh-bridge 2.7.1 → 2.8.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.
@@ -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
@@ -189,6 +189,7 @@ AI 生成过程中,QQ 会显示机器人的"正在输入"状态:
189
189
  - `/stop` - 停止当前任务
190
190
  - `/status` - 查看状态与会话摘要
191
191
  - `/workspaces` - 列出可用工作区
192
+ - `/addworkspace <路径>` - 注册添加新的电脑工作区目录
192
193
  - `/help` - 显示帮助
193
194
 
194
195
  ### 支持的消息类型
Binary file
Binary file
@@ -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` | 结束当前活动会话 | 挂载快捷开始按键 |
@@ -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) {
@@ -181,7 +181,10 @@ export class FeishuService extends Platform {
181
181
  async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, appId, appSecret, domain } = {}) {
182
182
  if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
183
183
  if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
184
- if (maxMessageChars != null) this.node.config.maxMessageChars = Number(maxMessageChars)
184
+ if (maxMessageChars != null) {
185
+ const val = Number(maxMessageChars)
186
+ this.node.config.maxMessageChars = (val >= 200) ? val : 2000
187
+ }
185
188
  if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
186
189
  if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
187
190
  this.gateway.updateConfig({
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
@@ -857,6 +858,247 @@ class BridgeService {
857
858
  }
858
859
  }
859
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
+
860
1102
  async diagnoseNetwork() {
861
1103
  const results = [];
862
1104
 
@@ -1198,7 +1440,10 @@ function apply(ctx, config = {}) {
1198
1440
  wechat.node.config.allowFrom = Array.isArray(cfg.allowFrom) ? cfg.allowFrom : [];
1199
1441
  if (cfg.digestIntervalSec != null) wechat.node.config.digestIntervalSec = Number(cfg.digestIntervalSec);
1200
1442
  if (cfg.approvalTimeoutSec != null) wechat.node.config.approvalTimeoutSec = Number(cfg.approvalTimeoutSec);
1201
- if (cfg.maxMessageChars != null) wechat.node.config.maxMessageChars = Number(cfg.maxMessageChars);
1443
+ if (cfg.maxMessageChars != null) {
1444
+ const val = Number(cfg.maxMessageChars);
1445
+ wechat.node.config.maxMessageChars = (val >= 200) ? val : 2000;
1446
+ }
1202
1447
  if (cfg.sendChunkDelayMs != null) wechat.node.config.sendChunkDelayMs = Number(cfg.sendChunkDelayMs);
1203
1448
 
1204
1449
  wechat.node._restoringConfig = (async () => {
@@ -1310,7 +1555,10 @@ function apply(ctx, config = {}) {
1310
1555
  telegram.node.config.allowFrom = Array.isArray(cfg.allowFrom) ? cfg.allowFrom : [];
1311
1556
  if (cfg.digestIntervalSec != null) telegram.node.config.digestIntervalSec = Number(cfg.digestIntervalSec);
1312
1557
  if (cfg.approvalTimeoutSec != null) telegram.node.config.approvalTimeoutSec = Number(cfg.approvalTimeoutSec);
1313
- if (cfg.maxMessageChars != null) telegram.node.config.maxMessageChars = Number(cfg.maxMessageChars);
1558
+ if (cfg.maxMessageChars != null) {
1559
+ const val = Number(cfg.maxMessageChars);
1560
+ telegram.node.config.maxMessageChars = (val >= 200) ? val : 4096;
1561
+ }
1314
1562
  if (cfg.sendChunkDelayMs != null) telegram.node.config.sendChunkDelayMs = Number(cfg.sendChunkDelayMs);
1315
1563
 
1316
1564
  telegram.node._restoringConfig = (async () => {
@@ -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 { resolve, normalize } from 'node:path'
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 = {
@@ -218,11 +220,13 @@ export class ConversationBridge {
218
220
  this.onActiveSessionChange = onActiveSessionChange
219
221
 
220
222
  const maxChars = platform.capabilities?.maxMessageChars ?? 2000
223
+ const rawMax = Number(config.maxMessageChars)
224
+ const safeMaxChars = (Number.isFinite(rawMax) && rawMax > 0) ? rawMax : maxChars
221
225
  this.config = {
222
226
  allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
223
227
  digestIntervalSec: config.digestIntervalSec ?? 300,
224
228
  approvalTimeoutSec: config.approvalTimeoutSec ?? 600,
225
- maxMessageChars: config.maxMessageChars ?? maxChars,
229
+ maxMessageChars: safeMaxChars,
226
230
  sendChunkDelayMs: config.sendChunkDelayMs ?? 1500,
227
231
  cwd: config.cwd,
228
232
  agentPreset: config.agentPreset,
@@ -974,6 +978,41 @@ async function routeCommand(node, text) {
974
978
  ].join('\n'))
975
979
  return true
976
980
  }
981
+ case 'addworkspace': {
982
+ const targetPath = rest.join(' ').trim()
983
+ if (!targetPath) {
984
+ await node.sendText(`❌ **缺少工作区路径**\n\n> 用法:\`/addworkspace <电脑绝对路径>\`\n> 示例:\`/addworkspace D:\\IdeaProjects\\my-app\``)
985
+ return true
986
+ }
987
+ try {
988
+ const safetyCheck = await isSafeWorkspacePath(targetPath)
989
+ if (!safetyCheck.valid) {
990
+ await node.sendText(`⚠️ **${safetyCheck.error || '路径安全校验未通过'}**:\`${targetPath}\`\n\n> 出于安全考虑,禁止将系统关键目录或敏感配置文件所在路径登记为工作区。`)
991
+ return true
992
+ }
993
+ const resolved = safetyCheck.path
994
+ const title = basename(resolved) || resolved
995
+ if (node.ctx.workspaceRegistry?.add) {
996
+ await node.ctx.workspaceRegistry.add({ path: resolved, title }).catch(() => {})
997
+ } else if (node.ctx.workspaceRegistry?.register) {
998
+ await node.ctx.workspaceRegistry.register({ path: resolved, title }).catch(() => {})
999
+ }
1000
+ const workspaces = await listWorkspaces(node)
1001
+ const foundIndex = workspaces.findIndex(w => normalize(w.path) === normalize(resolved))
1002
+ const numStr = foundIndex >= 0 ? `@${foundIndex + 1}` : ''
1003
+ await node.sendText([
1004
+ `✓ **工作区添加成功**!`,
1005
+ `- **名称**:${title}`,
1006
+ `- **路径**:\`${resolved}\``,
1007
+ foundIndex >= 0 ? `- **快捷编号**:\`${numStr}\`` : '',
1008
+ '',
1009
+ `> 发送 \`/new <提示词> ${numStr || '@' + resolved}\` 即可直接在此工作区创建会话。`,
1010
+ ].filter(Boolean).join('\n'))
1011
+ } catch (err) {
1012
+ await node.sendText(`❌ **添加工作区失败**:${err instanceof Error ? err.message : String(err)}`)
1013
+ }
1014
+ return true
1015
+ }
977
1016
  case 'new': {
978
1017
  // 解析尾部 @N 或 @路径 作为工作区 cwd
979
1018
  const args = rest.join(' ').trim()
@@ -1178,6 +1217,7 @@ function helpText() {
1178
1217
  '| 指令 | 说明 |',
1179
1218
  '| :--- | :--- |',
1180
1219
  '| `/workspaces` | 查看可用工作区表格列表 |',
1220
+ '| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 |',
1181
1221
  '| `/status` | 查看 Agent 运行状态看板 |',
1182
1222
  '| `/help` | 查看此帮助菜单 |',
1183
1223
  '',
package/lib/qq/index.js CHANGED
@@ -268,7 +268,10 @@ export class QqService extends Platform {
268
268
  async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, appId, clientSecret } = {}) {
269
269
  if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
270
270
  if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
271
- if (maxMessageChars != null) this.node.config.maxMessageChars = Number(maxMessageChars)
271
+ if (maxMessageChars != null) {
272
+ const val = Number(maxMessageChars)
273
+ this.node.config.maxMessageChars = (val >= 200) ? val : 2000
274
+ }
272
275
  if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
273
276
  if (appId !== undefined || clientSecret !== undefined) {
274
277
  this.gateway.setCredentials({