@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.
- package/README.en.md +14 -2
- package/README.md +13 -1
- package/assets/logo-plugin-message-link-gradient.webp +0 -0
- package/lib/client.js +417 -189
- package/lib/index.js +317 -316
- package/package.json +2 -2
- package/plugin-src/client/bot-general-settings.js +120 -0
- package/plugin-src/client/channels/dingtalk/api.js +3 -1
- package/plugin-src/client/channels/feishu/api.js +6 -2
- package/plugin-src/client/channels/qq/api.js +3 -1
- package/plugin-src/client/channels/shared/token-api.js +3 -1
- package/plugin-src/client/channels/wecom/api.js +3 -1
- package/plugin-src/client/channels/wecom-app/api.js +3 -1
- package/plugin-src/client/channels/weixin/api.js +3 -1
- package/plugin-src/client/channels/whatsapp/api.js +3 -1
- package/plugin-src/client/delivery-settings.js +5 -1
- package/plugin-src/client/i18n.js +14 -0
- package/plugin-src/client/styles.js +19 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/email/rpc.mjs +1 -0
- package/plugin-src/host/channels/feishu/production.mjs +1 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +31 -0
- package/plugin-src/host/channels/imessage/rpc.mjs +1 -0
- package/plugin-src/host/channels/matrix/rpc.mjs +10 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +10 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/scripts/verify-package.mjs +11 -6
- package/src/channels/dingtalk/dingtalk-bridge.mjs +31 -2
- package/src/channels/dingtalk/dingtalk-menu.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-runtime.mjs +2 -1
- package/src/channels/discord/discord-runtime.mjs +3 -1
- package/src/channels/feishu/bridge.mjs +91 -15
- package/src/channels/feishu/feishu-cards.mjs +16 -11
- package/src/channels/feishu/feishu-runtime.mjs +10 -1
- package/src/channels/feishu/multi-bot-controller.mjs +18 -0
- package/src/channels/feishu/plugin-config-store.mjs +2 -1
- package/src/channels/feishu/slash-command-panel.mjs +1 -0
- package/src/channels/feishu/step-push-mode.mjs +21 -0
- package/src/channels/matrix/matrix-crypto.mjs +19 -5
- package/src/channels/qq/qq-bridge.mjs +40 -2
- package/src/channels/qq/qq-menu.mjs +2 -2
- package/src/channels/shared/bot-workspace-store.mjs +69 -1
- package/src/channels/shared/busy-message-dispatcher.mjs +94 -0
- package/src/channels/shared/clear-command.mjs +85 -0
- package/src/channels/shared/command-catalog.mjs +3 -0
- package/src/channels/shared/command-permission.mjs +2 -0
- package/src/channels/shared/control-command.mjs +20 -11
- package/src/channels/shared/harness-approval.mjs +102 -2
- package/src/channels/shared/i18n-en/feishu.mjs +6 -4
- package/src/channels/shared/i18n-en/qq.mjs +2 -1
- package/src/channels/shared/i18n-en/shared-b.mjs +27 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +4 -0
- package/src/channels/shared/message-mode.mjs +19 -0
- package/src/channels/shared/text-harness-bridge.mjs +48 -5
- package/src/channels/wecom/wecom-bridge.mjs +26 -2
- package/src/channels/wecom/wecom-cards.mjs +1 -1
- package/src/channels/wecom-app/wecom-app-bridge.mjs +26 -2
- 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.#
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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('任务已结束,没有正在运行的任务,无法补充指令。请直接发送消息开始新任务。'));
|