@xmanrui/dsh-im 4.36.1 → 4.38.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.
Files changed (63) hide show
  1. package/README.en.md +14 -2
  2. package/README.md +13 -1
  3. package/assets/logo-plugin-message-link-gradient.webp +0 -0
  4. package/lib/client.js +417 -189
  5. package/lib/index.js +317 -316
  6. package/package.json +2 -2
  7. package/plugin-src/client/bot-general-settings.js +120 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +3 -1
  9. package/plugin-src/client/channels/feishu/api.js +6 -2
  10. package/plugin-src/client/channels/qq/api.js +3 -1
  11. package/plugin-src/client/channels/shared/token-api.js +3 -1
  12. package/plugin-src/client/channels/wecom/api.js +3 -1
  13. package/plugin-src/client/channels/wecom-app/api.js +3 -1
  14. package/plugin-src/client/channels/weixin/api.js +3 -1
  15. package/plugin-src/client/channels/whatsapp/api.js +3 -1
  16. package/plugin-src/client/delivery-settings.js +5 -1
  17. package/plugin-src/client/i18n.js +14 -0
  18. package/plugin-src/client/styles.js +19 -0
  19. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  20. package/plugin-src/host/channels/email/rpc.mjs +1 -0
  21. package/plugin-src/host/channels/feishu/production.mjs +1 -0
  22. package/plugin-src/host/channels/feishu/rpc.mjs +31 -0
  23. package/plugin-src/host/channels/imessage/rpc.mjs +1 -0
  24. package/plugin-src/host/channels/matrix/rpc.mjs +10 -0
  25. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  26. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  27. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  28. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  29. package/plugin-src/host/channels/wecom-app/rpc.mjs +10 -0
  30. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  31. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  32. package/scripts/verify-package.mjs +11 -6
  33. package/src/channels/dingtalk/dingtalk-bridge.mjs +31 -2
  34. package/src/channels/dingtalk/dingtalk-menu.mjs +1 -1
  35. package/src/channels/dingtalk/dingtalk-runtime.mjs +2 -1
  36. package/src/channels/discord/discord-runtime.mjs +3 -1
  37. package/src/channels/feishu/bridge.mjs +91 -15
  38. package/src/channels/feishu/feishu-cards.mjs +16 -11
  39. package/src/channels/feishu/feishu-runtime.mjs +10 -1
  40. package/src/channels/feishu/multi-bot-controller.mjs +18 -0
  41. package/src/channels/feishu/plugin-config-store.mjs +2 -1
  42. package/src/channels/feishu/slash-command-panel.mjs +1 -0
  43. package/src/channels/feishu/step-push-mode.mjs +21 -0
  44. package/src/channels/matrix/matrix-crypto.mjs +19 -5
  45. package/src/channels/qq/qq-bridge.mjs +40 -2
  46. package/src/channels/qq/qq-menu.mjs +2 -2
  47. package/src/channels/shared/bot-workspace-store.mjs +69 -1
  48. package/src/channels/shared/busy-message-dispatcher.mjs +94 -0
  49. package/src/channels/shared/clear-command.mjs +85 -0
  50. package/src/channels/shared/command-catalog.mjs +3 -0
  51. package/src/channels/shared/command-permission.mjs +2 -0
  52. package/src/channels/shared/control-command.mjs +20 -11
  53. package/src/channels/shared/harness-approval.mjs +102 -2
  54. package/src/channels/shared/i18n-en/feishu.mjs +6 -4
  55. package/src/channels/shared/i18n-en/qq.mjs +2 -1
  56. package/src/channels/shared/i18n-en/shared-b.mjs +27 -0
  57. package/src/channels/shared/i18n-en/shared-c.mjs +4 -0
  58. package/src/channels/shared/message-mode.mjs +19 -0
  59. package/src/channels/shared/text-harness-bridge.mjs +48 -5
  60. package/src/channels/wecom/wecom-bridge.mjs +26 -2
  61. package/src/channels/wecom/wecom-cards.mjs +1 -1
  62. package/src/channels/wecom-app/wecom-app-bridge.mjs +26 -2
  63. package/src/channels/weixin/weixin-bridge.mjs +22 -2
@@ -1,7 +1,9 @@
1
+ import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
1
2
  import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
2
3
  import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
3
4
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
4
5
  import { runCompactCommand } from '../shared/compact-command.mjs';
6
+ import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
5
7
  import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
6
8
  import {
7
9
  isControlCommand,
@@ -101,6 +103,7 @@ function helpText() {
101
103
  t('/menu 或 /m 打开可点击的功能菜单'),
102
104
  t('/new 开启一个全新会话'),
103
105
  t('/compact 压缩当前会话的较早上下文'),
106
+ t('/clear 清空当前会话上下文(保留 Session 绑定和历史记录)'),
104
107
  t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
105
108
  t('/workspace 工作区序号或绝对路径 切换工作区'),
106
109
  t('/workspacelist 列出工作区绝对路径'),
@@ -445,6 +448,7 @@ export class QqHarnessBridge {
445
448
  #fetchImpl;
446
449
  #fileUploadTimeoutMs;
447
450
  #queues = new Map();
451
+ #busyMessages = new BusyMessageDispatcher();
448
452
  #pendingInteractions = new Map();
449
453
  #interactionKeys = new Map();
450
454
  // Keep the accepted configuration through the existing queue/reply lifecycle.
@@ -599,6 +603,7 @@ export class QqHarnessBridge {
599
603
  }
600
604
  const commandRunner = explicitMenu || numericMenu
601
605
  ? () => this.#runMenuCommand(message, menuInput, key)
606
+ : isClearCommand(commandText) ? runClearCommand
602
607
  : isHistoryCommand(commandText) ? runHistoryCommand
603
608
  : isPermissionCommand(commandText) ? runPermissionCommand
604
609
  : hasQqFileAttachments(message) ? null : isControlCommand(commandText)
@@ -683,7 +688,25 @@ export class QqHarnessBridge {
683
688
  pending.queue = current;
684
689
  return current;
685
690
  }
686
- return this.#enqueueMessage(message, messageId, key);
691
+ return this.#busyMessages.dispatch({
692
+ key, messageId, text: commandText,
693
+ eligible: () => addressed && menuTextOnly && !explicitMenu && !numericMenu
694
+ && canAutomaticallySteer(this.#accessPolicy, message.kind === 'c2c' ? 'direct' : 'group', sender),
695
+ harness: this.#harness, state: this.#state, status: this.#status,
696
+ signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
697
+ acceptedMessageIds: this.#acceptedMessageIds,
698
+ pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
699
+ enhancement: {
700
+ snapshot: this.#acceptedMessageIds.get(messageId),
701
+ source: () => ({ channel: 'qq', senderId: sender,
702
+ senderName: message.kind === 'group' ? message.senderName : undefined,
703
+ chatId: message.kind === 'group' ? message.groupOpenid : sender }),
704
+ },
705
+ isQueued: () => this.#queues.has(key),
706
+ enqueue: (options) => this.#enqueueMessage(message, messageId, key, options),
707
+ send: (reply) => addressed ? this.#bot.sendText(message.replyTarget, reply) : Promise.resolve(),
708
+ onError: (error) => this.#handleInteractionFailure(message, messageId, error),
709
+ });
687
710
  }
688
711
 
689
712
  #enqueueMessage(message, messageId, key, {
@@ -765,7 +788,7 @@ export class QqHarnessBridge {
765
788
  const pendingInteraction = this.#pendingInteractions.has(key) || this.#approvals.hasPending(key);
766
789
  const isBusy = () => this.#queues.has(key) || this.#pendingInteractions.has(key)
767
790
  || this.#approvals.hasPending(key) || this.#batchInputs.status(key).phase !== 'idle';
768
- const needsIdle = /^\/(?:new|session|workspace|compact)(?:\s|$)/u.test(command);
791
+ const needsIdle = /^\/(?:new|session|workspace|compact|clear)(?:\s|$)/u.test(command);
769
792
  if (isBusy() && needsIdle) {
770
793
  return { message: t('当前任务仍在运行,请先停止任务或等待任务完成后再执行此操作。') };
771
794
  }
@@ -806,6 +829,7 @@ export class QqHarnessBridge {
806
829
  await this.#state.clearSession(key);
807
830
  return { message: t('已开启新会话。请发送你的问题。') };
808
831
  });
832
+ if (isClearCommand(command)) return runClearCommand(command, this.#harness, this.#state, key, options);
809
833
  if (command === '/compact') return runCompactCommand(command, this.#harness, this.#state, key, options);
810
834
  return runWorkspaceCommand(command, this.#harness, key);
811
835
  };
@@ -819,6 +843,7 @@ export class QqHarnessBridge {
819
843
  }
820
844
 
821
845
  async waitForIdle() {
846
+ await this.#busyMessages.whenIdle();
822
847
  await Promise.allSettled([
823
848
  ...this.#queues.values(),
824
849
  ...[...this.#pendingInteractions.values()].flatMap((pending) => (
@@ -843,6 +868,7 @@ export class QqHarnessBridge {
843
868
  hasFiles: hasQqFileAttachments(message),
844
869
  pendingInteraction: this.#pendingInteractions.has(key)
845
870
  || this.#approvals.hasPending(key),
871
+ busy: this.#queues.has(key),
846
872
  control: { owner: this, key },
847
873
  deferredDelivery: this.#deferred,
848
874
  enhancement: captureContextEnhancementSource(
@@ -1020,6 +1046,18 @@ export class QqHarnessBridge {
1020
1046
  await markMessageSeen();
1021
1047
  return;
1022
1048
  }
1049
+ const clearCommand = await runClearCommand(text, this.#harness, this.#state, key, {
1050
+ signal: this.#signal,
1051
+ hasImages,
1052
+ hasFiles,
1053
+ pendingInteraction: this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
1054
+ busy: this.#queues.has(key),
1055
+ });
1056
+ if (clearCommand) {
1057
+ await this.#bot.sendText(target, clearCommand.message);
1058
+ await markMessageSeen();
1059
+ return;
1060
+ }
1023
1061
  const workspaceCommand = hasImages || hasFiles
1024
1062
  ? null
1025
1063
  : await runWorkspaceCommand(text, this.#harness, key);
@@ -122,7 +122,7 @@ export async function qqMenuView(name, harness, state, key, { signal, busy = fal
122
122
  columns: 3, entries: name === 'status' ? [back()] : [
123
123
  section(t('💬 会话选择'), 'sessions'), section(t('📁 工作区'), 'workspaces'), section(t('🤖 模式/预设'), 'presets'),
124
124
  section(t('🧠 模型'), 'models'), command(t('🆕 新会话'), '/new'), section(t('📋 会话列表'), 'sessions'),
125
- command(t('⏹ 停止'), '/stop'), command(t('📐 压缩'), '/compact'), section(t('💬 补充指令'), 'steer'),
125
+ command(t('⏹ 停止'), '/stop'), command(t('📐 压缩'), '/compact'), command(t('🧹 清空上下文'), '/clear'), section(t('💬 补充指令'), 'steer'),
126
126
  { label: t('切换归档显示'), action: { kind: 'archive', include: !archived } },
127
127
  section(t('📊 状态'), 'status'), section(t('📖 帮助'), 'help'),
128
128
  ] };
@@ -163,7 +163,7 @@ export async function qqMenuView(name, harness, state, key, { signal, busy = fal
163
163
  t('发送 /m 打开助手中心,点击按钮或回复当前列表的数字选择。'),
164
164
  t('设置:会话、工作区、模式/预设、模型。'),
165
165
  t('会话:新会话、会话列表、归档显示切换。'),
166
- t('任务:停止、压缩、快捷或自定义补充指令。'),
166
+ t('任务:停止、压缩、清空上下文、快捷或自定义补充指令。'),
167
167
  t('信息:状态、帮助。'),
168
168
  t('菜单有效期为 15 分钟;发送普通消息会退出数字选择。'),
169
169
  t('回答问题、审批和批量输入时,数字优先用于当前交互。'),
@@ -1,4 +1,5 @@
1
1
  import { t } from './i18n.mjs';
2
+ import { normalizeBusyMessageMode, validateBusyMessageMode } from './message-mode.mjs';
2
3
  import { atConnectionStage, connectionStageError, extractConnectionEvidence } from './connection-error.mjs';
3
4
  import { validateBotAlias, withBotAlias } from './bot-alias.mjs';
4
5
  import { defaultImWorkspace, ensureImWorkspaceDirectory, sameWorkspacePath } from './default-workspace.mjs';
@@ -307,6 +308,13 @@ function normalizeDocument(value) {
307
308
  } catch { /* A damaged display name must not disable the bot. */ }
308
309
  }
309
310
  }
311
+ const busyMessageModes = Object.create(null);
312
+ if (value.busyMessageModes && typeof value.busyMessageModes === 'object'
313
+ && !Array.isArray(value.busyMessageModes)) {
314
+ for (const [id, mode] of Object.entries(value.busyMessageModes)) {
315
+ if (Object.hasOwn(workspaces, id) && mode === 'steer') busyMessageModes[id] = mode;
316
+ }
317
+ }
310
318
  const accessPolicies = normalizeAccessPolicies(value.accessPolicies, workspaces);
311
319
  const version = Math.max(
312
320
  value.version,
@@ -324,6 +332,7 @@ function normalizeDocument(value) {
324
332
  deliveryTargets,
325
333
  accessPolicies,
326
334
  aliases,
335
+ busyMessageModes,
327
336
  };
328
337
  }
329
338
 
@@ -337,8 +346,10 @@ function storedDocument({
337
346
  deliveryTargets,
338
347
  accessPolicies,
339
348
  aliases,
349
+ busyMessageModes,
340
350
  }) {
341
351
  const document = { version, workspaces };
352
+ if (Object.keys(busyMessageModes).length > 0) document.busyMessageModes = busyMessageModes;
342
353
  if (Object.keys(aliases).length > 0) document.aliases = aliases;
343
354
  if (Object.keys(conversationWorkspaces).length > 0) {
344
355
  document.conversationWorkspaces = conversationWorkspaces;
@@ -400,6 +411,7 @@ export class BotWorkspaceStore {
400
411
  #agentPresets = {};
401
412
  #models = {};
402
413
  #aliases = Object.create(null);
414
+ #busyMessageModes = Object.create(null);
403
415
  #contextEnhancement = {};
404
416
  #deliveryTargets = Object.create(null);
405
417
  #accessPolicies = Object.create(null);
@@ -431,6 +443,7 @@ export class BotWorkspaceStore {
431
443
  this.#agentPresets = normalized.agentPresets;
432
444
  this.#models = normalized.models;
433
445
  this.#aliases = normalized.aliases;
446
+ this.#busyMessageModes = normalized.busyMessageModes;
434
447
  this.#contextEnhancement = normalized.contextEnhancement;
435
448
  this.#deliveryTargets = normalized.deliveryTargets;
436
449
  this.#accessPolicies = normalized.accessPolicies;
@@ -442,6 +455,7 @@ export class BotWorkspaceStore {
442
455
  this.#agentPresets = {};
443
456
  this.#models = {};
444
457
  this.#aliases = Object.create(null);
458
+ this.#busyMessageModes = Object.create(null);
445
459
  this.#contextEnhancement = {};
446
460
  this.#deliveryTargets = Object.create(null);
447
461
  this.#accessPolicies = Object.create(null);
@@ -519,6 +533,11 @@ export class BotWorkspaceStore {
519
533
  return this.has(id) && Object.hasOwn(this.#aliases, id) ? this.#aliases[id] : '';
520
534
  }
521
535
 
536
+ busyMessageModeFor(botId) {
537
+ const id = botIdOf(botId);
538
+ return normalizeBusyMessageMode(this.has(id) ? this.#busyMessageModes[id] : undefined);
539
+ }
540
+
522
541
  contextEnhancementFor(botId) {
523
542
  const id = botIdOf(botId);
524
543
  return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
@@ -890,6 +909,27 @@ export class BotWorkspaceStore {
890
909
  });
891
910
  }
892
911
 
912
+ async setBusyMessageMode(botId, value, { incarnation } = {}) {
913
+ const id = botIdOf(botId);
914
+ const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
915
+ const mode = validateBusyMessageMode(value);
916
+ return this.#enqueue(id, async () => {
917
+ if (!this.has(id) || expectedIncarnation !== this.incarnationFor(id)) {
918
+ const error = new Error('找不到要修改的机器人。');
919
+ error.code = 'workspace-bot-not-found';
920
+ throw error;
921
+ }
922
+ const next = { ...this.#busyMessageModes };
923
+ if (mode === 'steer') next[id] = mode;
924
+ else delete next[id];
925
+ // Publish only after the same atomic write used by the other bot settings.
926
+ await this.#persist(this.#contextEnhancement, this.#deliveryTargets,
927
+ this.#version, this.#accessPolicies, this.#aliases, this.#conversationWorkspaces, next);
928
+ this.#busyMessageModes = next;
929
+ return mode;
930
+ });
931
+ }
932
+
893
933
  async setContextEnhancement(botId, value, { incarnation } = {}) {
894
934
  const id = botIdOf(botId);
895
935
  const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
@@ -1253,6 +1293,7 @@ export class BotWorkspaceStore {
1253
1293
  ...Object.keys(this.#agentPresets),
1254
1294
  ...Object.keys(this.#models),
1255
1295
  ...Object.keys(this.#aliases),
1296
+ ...Object.keys(this.#busyMessageModes),
1256
1297
  ...Object.keys(this.#contextEnhancement),
1257
1298
  ...Object.keys(this.#deliveryTargets),
1258
1299
  ...Object.keys(this.#accessPolicies),
@@ -1276,6 +1317,7 @@ export class BotWorkspaceStore {
1276
1317
  agentPreset: this.agentPresetFor(bot.botId),
1277
1318
  model: this.modelFor(bot.botId),
1278
1319
  contextEnhancement: this.contextEnhancementFor(bot.botId),
1320
+ busyMessageMode: this.busyMessageModeFor(bot.botId),
1279
1321
  accessPolicy: this.accessPolicyFor(bot.botId),
1280
1322
  }
1281
1323
  : bot),
@@ -1314,17 +1356,19 @@ export class BotWorkspaceStore {
1314
1356
  const hadPreset = Object.hasOwn(this.#agentPresets, id);
1315
1357
  const hadModel = Object.hasOwn(this.#models, id);
1316
1358
  const hadAlias = Object.hasOwn(this.#aliases, id);
1359
+ const hadMessageMode = Object.hasOwn(this.#busyMessageModes, id);
1317
1360
  const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
1318
1361
  const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
1319
1362
  const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
1320
1363
  const hadConversationWorkspaces = Object.hasOwn(this.#conversationWorkspaces, id);
1321
1364
  const needsCleanup = hadWorkspace || hadPreset || hadModel || hadAlias || hadContextEnhancement
1322
- || hadDeliveryTargets || hadAccessPolicy || hadConversationWorkspaces
1365
+ || hadDeliveryTargets || hadAccessPolicy || hadConversationWorkspaces || hadMessageMode
1323
1366
  || this.#dirtyRemovals.has(id);
1324
1367
  delete this.#workspaces[id];
1325
1368
  delete this.#agentPresets[id];
1326
1369
  delete this.#models[id];
1327
1370
  delete this.#aliases[id];
1371
+ delete this.#busyMessageModes[id];
1328
1372
  delete this.#contextEnhancement[id];
1329
1373
  delete this.#deliveryTargets[id];
1330
1374
  delete this.#accessPolicies[id];
@@ -1365,6 +1409,7 @@ export class BotWorkspaceStore {
1365
1409
  accessPolicies = this.#accessPolicies,
1366
1410
  aliases = this.#aliases,
1367
1411
  conversationWorkspaces = this.#conversationWorkspaces,
1412
+ busyMessageModes = this.#busyMessageModes,
1368
1413
  ) {
1369
1414
  await writeStoredDocument(this.#path, storedDocument({
1370
1415
  version,
@@ -1376,6 +1421,7 @@ export class BotWorkspaceStore {
1376
1421
  deliveryTargets,
1377
1422
  accessPolicies,
1378
1423
  aliases,
1424
+ busyMessageModes,
1379
1425
  }));
1380
1426
  this.#dirtyRemovals.clear();
1381
1427
  }
@@ -1385,6 +1431,7 @@ export class BotWorkspaceStore {
1385
1431
  || Object.keys(this.#agentPresets).length > 0
1386
1432
  || Object.keys(this.#models).length > 0
1387
1433
  || Object.keys(this.#aliases).length > 0
1434
+ || Object.keys(this.#busyMessageModes).length > 0
1388
1435
  || Object.keys(this.#contextEnhancement).length > 0
1389
1436
  || Object.keys(this.#deliveryTargets).length > 0
1390
1437
  || Object.keys(this.#accessPolicies).length > 0
@@ -1689,6 +1736,9 @@ export function createBotWorkspaceScope(
1689
1736
  }
1690
1737
  };
1691
1738
  }
1739
+ if (property === 'currentBusyMessageMode') {
1740
+ return () => isCurrentScope() ? workspaces.busyMessageModeFor(botId) : 'queue';
1741
+ }
1692
1742
  if (property === 'currentWorkspace') {
1693
1743
  return () => {
1694
1744
  if (!isCurrentScope()) {
@@ -2252,6 +2302,23 @@ export function createWorkspaceAwareController(controller, {
2252
2302
  return result;
2253
2303
  });
2254
2304
  };
2305
+ const updateBusyMessageMode = (botId, value, projectStatus) => {
2306
+ const incarnation = workspaces.incarnationFor(botId);
2307
+ const busyMessageMode = validateBusyMessageMode(value);
2308
+ return withBotTransition(botId, async () => {
2309
+ const snapshot = await decorate(await controller.status());
2310
+ if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
2311
+ const error = new Error('找不到要修改的机器人。');
2312
+ error.code = 'workspace-bot-not-found';
2313
+ throw error;
2314
+ }
2315
+ const updated = { ...snapshot, bots: snapshot.bots.map((bot) => bot.botId === botId
2316
+ ? { ...bot, busyMessageMode } : bot) };
2317
+ const result = projectStatus ? await projectStatus(updated) : updated;
2318
+ await workspaces.setBusyMessageMode(botId, busyMessageMode, { incarnation });
2319
+ return result;
2320
+ });
2321
+ };
2255
2322
  const updateContextEnhancement = (botId, value, projectStatus) => {
2256
2323
  const incarnation = workspaces.incarnationFor(botId);
2257
2324
  const config = validateContextEnhancementConfig(value);
@@ -2369,6 +2436,7 @@ export function createWorkspaceAwareController(controller, {
2369
2436
  if (property === 'updateAgentPreset') return updateAgentPreset;
2370
2437
  if (property === 'updateModel') return updateModel;
2371
2438
  if (property === 'updateAlias') return updateAlias;
2439
+ if (property === 'updateBusyMessageMode') return updateBusyMessageMode;
2372
2440
  if (property === 'updateContextEnhancement') return updateContextEnhancement;
2373
2441
  if (property === 'updateAccessPolicy') return updateAccessPolicy;
2374
2442
  const value = Reflect.get(target, property, target);
@@ -0,0 +1,94 @@
1
+ import { steerMessage } from './control-command.mjs';
2
+ import { isSharedLocalCommand } from './command-permission.mjs';
3
+ import { evaluateInboundAccess } from './inbound-access.mjs';
4
+ import { normalizeBusyMessageMode } from './message-mode.mjs';
5
+ import { t } from './i18n.mjs';
6
+ import {
7
+ messageFailureDiagnostic, messageFailureText, setLastMessageFailure,
8
+ } from './message-failure.mjs';
9
+
10
+ export function canAutomaticallySteer(policy, conversationType, senderIds) {
11
+ // Legacy bridges have already checked their owner/allowlist at acceptance.
12
+ return !policy || evaluateInboundAccess(policy, {
13
+ conversationType, senderIds, text: '/steer', isCommand: true,
14
+ }).allowed;
15
+ }
16
+
17
+ /** Serialize only routing/submission, leaving the existing task queues intact. */
18
+ export class BusyMessageDispatcher {
19
+ #tails = new Map();
20
+ #tasks = new Set();
21
+
22
+ dispatch({
23
+ key, messageId, text, commandText = text, eligible, harness, state, status,
24
+ control, signal, enhancement, pendingInteraction, acceptedMessageIds,
25
+ mode = harness.currentBusyMessageMode?.(), onFailure,
26
+ enqueue, isQueued, send, logger = console, onSteered, onError,
27
+ }) {
28
+ // Read once, before any await: a settings change cannot reinterpret this input.
29
+ const steer = normalizeBusyMessageMode(mode) === 'steer'
30
+ && (typeof eligible === 'function' ? eligible() : eligible) && Boolean(text?.trim()) && !isSharedLocalCommand(commandText);
31
+ const report = async (error) => {
32
+ onFailure?.(error);
33
+ if (signal?.aborted || error?.code === 'turn-stopped') return;
34
+ if (onError) return onError(error);
35
+ status.lastError = error?.message ?? String(error);
36
+ const failure = setLastMessageFailure(status, error);
37
+ logger.error?.('[dsh-im] failed to route a message:', messageFailureDiagnostic(error, failure));
38
+ await Promise.resolve().then(() => send(messageFailureText(failure))).catch(() => undefined);
39
+ };
40
+ const queue = (alreadyRecorded = false) => {
41
+ const waiting = isQueued();
42
+ // Reserve FIFO position before sending the optional receipt.
43
+ const completion = enqueue({ alreadyRecorded });
44
+ const receipt = waiting
45
+ ? Promise.resolve().then(() => send(t('已排队,等待前序消息处理。'))).catch((error) => {
46
+ logger.warn?.('[dsh-im] failed to send queue receipt:', error);
47
+ })
48
+ : Promise.resolve();
49
+ return { completion: Promise.all([completion, receipt]).then(([result]) => result) };
50
+ };
51
+ const route = async () => {
52
+ signal?.throwIfAborted();
53
+ if (!steer || pendingInteraction()) return queue();
54
+ if (state.hasSeen(messageId)) return {};
55
+ await state.markSeen(messageId);
56
+ status.messagesReceived = (status.messagesReceived ?? 0) + 1;
57
+ status.lastMessageAt = new Date().toISOString();
58
+ signal?.throwIfAborted();
59
+ const submitted = await steerMessage(text, harness, state, key, {
60
+ signal, control, enhancement, pendingInteraction: pendingInteraction(),
61
+ });
62
+ if (!submitted) return queue(true);
63
+ // Submission is final even if its receipt fails.
64
+ await send(t('已提交补充指令,Agent 会在下一步读取。'));
65
+ await onSteered?.();
66
+ status.lastError = null;
67
+ return {};
68
+ };
69
+
70
+ const previous = this.#tails.get(key);
71
+ // Preserve synchronous queue reservation for the default/attachment path.
72
+ const routed = !previous && !steer
73
+ ? Promise.resolve(queue())
74
+ : (previous ?? Promise.resolve()).catch(() => undefined).then(route);
75
+ const tail = routed.then(() => undefined, () => undefined);
76
+ this.#tails.set(key, tail);
77
+ void tail.then(() => {
78
+ if (this.#tails.get(key) === tail) this.#tails.delete(key);
79
+ });
80
+ const task = routed.catch(async (error) => {
81
+ await report(error);
82
+ return {};
83
+ }).then(({ completion }) => completion).finally(() => {
84
+ acceptedMessageIds.delete(messageId);
85
+ this.#tasks.delete(task);
86
+ });
87
+ this.#tasks.add(task);
88
+ return task;
89
+ }
90
+
91
+ async whenIdle() {
92
+ await Promise.allSettled([...this.#tasks]);
93
+ }
94
+ }
@@ -0,0 +1,85 @@
1
+ import { t } from './i18n.mjs';
2
+ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
3
+
4
+ const CLEAR_COMMAND = /^\/clear(?=$|\s)([\s\S]*)$/iu;
5
+ const CLEAR_USAGE = '用法:/clear(不带参数且不可附带图片或文件)';
6
+
7
+ function commandResult(message) {
8
+ return { handled: true, message, messages: [message] };
9
+ }
10
+
11
+ function clearErrorMessage(error) {
12
+ const code = error?.code ?? error?.failure?.code;
13
+ if (code === 'session-not-found') {
14
+ return t('当前聊天绑定的会话已不存在,未清空上下文;请先发送新消息开启会话。');
15
+ }
16
+ if (code === 'agent-busy' || code === 'pending-interaction') {
17
+ return t('当前会话正在生成回复或等待交互,请先完成交互或发送 /stop,再执行 /clear。');
18
+ }
19
+ if (code === 'cancelled' || error?.name === 'AbortError') {
20
+ return t('上下文清理已取消,当前会话未修改。');
21
+ }
22
+ if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
23
+ return t('工作区或机器人状态已发生变化,未清空当前会话,请重试。');
24
+ }
25
+ if (code === 'commands-unavailable') {
26
+ return t('当前 Host 不支持 /clear,当前会话未修改。');
27
+ }
28
+ return t('清空当前会话上下文失败,当前会话未修改,请稍后重试。');
29
+ }
30
+
31
+ /**
32
+ * Execute Host's argument-free /clear command without changing the dsh-im
33
+ * Session binding. Hosts that do not register /clear return undefined and
34
+ * are reported explicitly; the command never falls back to /new.
35
+ */
36
+ export async function runClearCommand(text, harness, state, conversationKey, options = {}) {
37
+ if (!isClearCommand(text)) return null;
38
+ const match = CLEAR_COMMAND.exec(text.trim());
39
+ if (match[1].trim() || options.hasImages || options.hasFiles) {
40
+ return commandResult(t(CLEAR_USAGE));
41
+ }
42
+ if (options.pendingInteraction || options.busy) {
43
+ return commandResult(t('当前会话正在生成回复或等待交互,请先完成交互或发送 /stop,再执行 /clear。'));
44
+ }
45
+ if (typeof state?.sessionFor !== 'function') {
46
+ return commandResult(t('当前机器人没有可用的会话状态,未清空当前会话。'));
47
+ }
48
+ const sessionId = state.sessionFor(conversationKey);
49
+ if (typeof sessionId !== 'string' || !sessionId) {
50
+ return commandResult(t('当前聊天还没有可清空的会话,请先发送一条消息。'));
51
+ }
52
+ try {
53
+ let execution;
54
+ if (typeof harness?.workspaceSession === 'function') {
55
+ const session = harness.workspaceSession(sessionId, conversationKey);
56
+ if (typeof session?.executeCommand !== 'function') {
57
+ return commandResult(t('当前 Host 不支持 /clear,当前会话未修改。'));
58
+ }
59
+ execution = await session.executeCommand('/clear', options);
60
+ } else {
61
+ if (typeof harness?.executeCommand !== 'function') {
62
+ return commandResult(t('当前 Host 不支持 /clear,当前会话未修改。'));
63
+ }
64
+ execution = await harness.executeCommand(sessionId, '/clear', options);
65
+ }
66
+ if (execution === undefined) {
67
+ return commandResult(t('当前 Host 不支持 /clear,当前会话未修改。'));
68
+ }
69
+ const result = execution?.result;
70
+ if (!result || !['success', 'error'].includes(result.kind)
71
+ || (result.text !== undefined && typeof result.text !== 'string')) {
72
+ return commandResult(t('清空当前会话上下文失败,当前会话未修改,请稍后重试。'));
73
+ }
74
+ if (result.kind === 'error') {
75
+ return commandResult(clearErrorMessage({ ...result, failure: result.failure ?? result }));
76
+ }
77
+ return commandResult(t('当前会话上下文已清空;Session 绑定和历史记录仍保留。'));
78
+ } catch (error) {
79
+ return commandResult(clearErrorMessage(error));
80
+ }
81
+ }
82
+
83
+ export function isClearCommand(text) {
84
+ return typeof text === 'string' && CLEAR_COMMAND.test(text.trim());
85
+ }
@@ -29,6 +29,9 @@ export const SHARED_COMMAND_CATALOG = Object.freeze([
29
29
  defineCatalogCommand('compact', '压缩当前会话的较早上下文', [
30
30
  '/compact 压缩当前会话的较早上下文',
31
31
  ]),
32
+ defineCatalogCommand('clear', '清空当前会话上下文并保留会话绑定', [
33
+ '/clear 清空当前会话上下文(保留 Session 绑定和历史记录)',
34
+ ]),
32
35
  defineCatalogCommand('history', '查看最近历史消息(仅私聊)', [
33
36
  '/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
34
37
  ], { privateOnly: true }),
@@ -1,5 +1,6 @@
1
1
  import { isBatchInputCommand } from './batch-input.mjs';
2
2
  import { isCompactCommand } from './compact-command.mjs';
3
+ import { isClearCommand } from './clear-command.mjs';
3
4
  import { isControlCommand } from './control-command.mjs';
4
5
  import { isHistoryCommand } from './history-command.mjs';
5
6
  import { isModelCommand } from './model-command.mjs';
@@ -20,6 +21,7 @@ export function isSharedLocalCommand(text, {
20
21
  const command = text.trim();
21
22
  if (!command) return false;
22
23
  if (isBatchInputCommand(command) || isHistoryCommand(command)) return true;
24
+ if (isClearCommand(command)) return true;
23
25
  if (!hasFiles && (
24
26
  isControlCommand(command)
25
27
  || isModelCommand(command)
@@ -36,6 +36,23 @@ export function isControlCommand(text) {
36
36
  return typeof text === 'string' && CONTROL_COMMAND.test(text.trim());
37
37
  }
38
38
 
39
+ // Shared by explicit /steer and ordinary messages configured to steer. A false
40
+ // result guarantees no submission; exceptions must never trigger a second ask.
41
+ export async function steerMessage(text, harness, state, key, {
42
+ signal, control, enhancement, pendingInteraction = false,
43
+ } = {}) {
44
+ if (pendingInteraction) return false;
45
+ const session = boundSession(harness, state, key);
46
+ if (!session) return false;
47
+ if (typeof session.steerActiveTurn !== 'function') {
48
+ throw new TypeError('Harness session does not support steering active turns');
49
+ }
50
+ const steering = enhancement
51
+ ? enhanceContextContent(text, enhancement.snapshot, enhancement.source)
52
+ : text;
53
+ return session.steerActiveTurn(steering, control, requestOptions(signal));
54
+ }
55
+
39
56
  export async function runControlCommand(text, harness, state, key, {
40
57
  signal,
41
58
  hasImages = false,
@@ -92,19 +109,11 @@ export async function runControlCommand(text, harness, state, key, {
92
109
  if (!session) {
93
110
  return commandResult(t('当前聊天没有绑定会话,无法补充指令。请先绑定会话。'));
94
111
  }
95
- if (typeof session.steerActiveTurn !== 'function') {
96
- throw new TypeError('Harness session does not support steering active turns');
97
- }
98
112
  // A mid-turn correction carries the same provenance as the message that
99
113
  // opened the turn, so a group member who steers is identified too.
100
- const steering = enhancement
101
- ? enhanceContextContent(instruction, enhancement.snapshot, enhancement.source)
102
- : instruction;
103
- const steered = await session.steerActiveTurn(
104
- steering,
105
- control,
106
- requestOptions(signal),
107
- );
114
+ const steered = await steerMessage(instruction, harness, state, key, {
115
+ signal, control, enhancement,
116
+ });
108
117
  return steered
109
118
  ? commandResult(t('已提交补充指令,Agent 会在下一步读取。'))
110
119
  : commandResult(t('任务已结束,没有正在运行的任务,无法补充指令。请直接发送消息开始新任务。'));