@xmanrui/dsh-im 4.36.1 → 4.37.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/lib/client.js +322 -190
- package/lib/index.js +314 -313
- package/package.json +2 -2
- package/plugin-src/client/bot-general-settings.js +83 -0
- package/plugin-src/client/channels/dingtalk/api.js +3 -1
- package/plugin-src/client/channels/feishu/api.js +3 -1
- 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 +13 -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/rpc.mjs +13 -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/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 +69 -5
- package/src/channels/feishu/feishu-cards.mjs +8 -4
- package/src/channels/feishu/slash-command-panel.mjs +1 -0
- 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/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/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,3 +1,4 @@
|
|
|
1
|
+
import { SET_MESSAGE_MODE_ENDPOINT, validMessageModePayload } from '../../../../src/channels/shared/message-mode.mjs';
|
|
1
2
|
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
2
3
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
3
4
|
import QRCode from 'qrcode';
|
|
@@ -32,6 +33,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
|
32
33
|
setModel: SET_MODEL_ENDPOINT,
|
|
33
34
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
34
35
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
36
|
+
setBusyMessageMode: SET_MESSAGE_MODE_ENDPOINT,
|
|
35
37
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
36
38
|
setAlias: SET_ALIAS_ENDPOINT,
|
|
37
39
|
});
|
|
@@ -95,6 +97,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
95
97
|
return validAgentPresetPayload(payload)
|
|
96
98
|
? null : '请选择 Agent Preset。';
|
|
97
99
|
}
|
|
100
|
+
if (endpoint === WEIXIN_ENDPOINTS.setBusyMessageMode) {
|
|
101
|
+
return validMessageModePayload(payload)
|
|
102
|
+
? null : '请提交有效的消息处理方式。';
|
|
103
|
+
}
|
|
98
104
|
if (endpoint === WEIXIN_ENDPOINTS.setContextEnhancement) {
|
|
99
105
|
return validContextEnhancementPayload(payload)
|
|
100
106
|
? null : '请提交有效的上下文增强设置。';
|
|
@@ -235,6 +241,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl, logge
|
|
|
235
241
|
await controller.updateModel(payload.botId, payload.model),
|
|
236
242
|
cachedEncode,
|
|
237
243
|
);
|
|
244
|
+
} else if (endpoint === WEIXIN_ENDPOINTS.setBusyMessageMode) {
|
|
245
|
+
if (typeof controller.updateBusyMessageMode !== 'function') throw new Error('Message mode update is unavailable');
|
|
246
|
+
value = await controller.updateBusyMessageMode(
|
|
247
|
+
payload.botId, payload.busyMessageMode, (status) => publicStatus(status, cachedEncode),
|
|
248
|
+
);
|
|
238
249
|
} else if (endpoint === WEIXIN_ENDPOINTS.setContextEnhancement) {
|
|
239
250
|
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
240
251
|
value = await controller.updateContextEnhancement(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SET_MESSAGE_MODE_ENDPOINT, validMessageModePayload } from '../../../../src/channels/shared/message-mode.mjs';
|
|
1
2
|
import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
|
|
2
3
|
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
3
4
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
@@ -25,6 +26,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
25
26
|
setModel: SET_MODEL_ENDPOINT,
|
|
26
27
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
27
28
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
29
|
+
setBusyMessageMode: SET_MESSAGE_MODE_ENDPOINT,
|
|
28
30
|
});
|
|
29
31
|
export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
|
|
30
32
|
|
|
@@ -79,6 +81,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
79
81
|
return validAgentPresetPayload(payload)
|
|
80
82
|
? null : '请选择 Agent Preset。';
|
|
81
83
|
}
|
|
84
|
+
if (endpoint === WHATSAPP_ENDPOINTS.setBusyMessageMode) {
|
|
85
|
+
return validMessageModePayload(payload)
|
|
86
|
+
? null : '请提交有效的消息处理方式。';
|
|
87
|
+
}
|
|
82
88
|
if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
|
|
83
89
|
return validContextEnhancementPayload(payload)
|
|
84
90
|
? null : '请提交有效的上下文增强设置。';
|
|
@@ -192,6 +198,11 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
192
198
|
await controller.updateModel(payload.botId, payload.model),
|
|
193
199
|
cachedEncode,
|
|
194
200
|
);
|
|
201
|
+
} else if (endpoint === WHATSAPP_ENDPOINTS.setBusyMessageMode) {
|
|
202
|
+
if (typeof controller.updateBusyMessageMode !== 'function') throw new Error('Message mode update is unavailable');
|
|
203
|
+
value = await controller.updateBusyMessageMode(
|
|
204
|
+
payload.botId, payload.busyMessageMode, (status) => publicStatus(status, cachedEncode),
|
|
205
|
+
);
|
|
195
206
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
|
|
196
207
|
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
197
208
|
value = await controller.updateContextEnhancement(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
|
|
1
2
|
import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
|
|
2
3
|
import {
|
|
3
4
|
DINGTALK_DONE_REACTION_NAME,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
} from '../shared/harness-question.mjs';
|
|
16
17
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
17
18
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
19
|
+
import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
|
|
18
20
|
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
|
19
21
|
import {
|
|
20
22
|
isControlCommand,
|
|
@@ -87,6 +89,7 @@ const HELP_TEXT_LINES = [
|
|
|
87
89
|
'/m 或 /menu 打开下拉操作菜单',
|
|
88
90
|
'/new 开启一个全新会话',
|
|
89
91
|
'/compact 压缩当前会话的较早上下文',
|
|
92
|
+
'/clear 清空当前会话上下文(保留 Session 绑定和历史记录)',
|
|
90
93
|
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
|
91
94
|
'/workspace 工作区序号或绝对路径 切换工作区',
|
|
92
95
|
'/workspacelist 列出工作区绝对路径',
|
|
@@ -539,6 +542,7 @@ export class DingtalkHarnessBridge {
|
|
|
539
542
|
#maxMessageChars;
|
|
540
543
|
#signal;
|
|
541
544
|
#queues = new Map();
|
|
545
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
542
546
|
#pendingInteractions = new Map();
|
|
543
547
|
#interactionKeys = new Map();
|
|
544
548
|
#interactionTasks = new Set();
|
|
@@ -597,7 +601,7 @@ export class DingtalkHarnessBridge {
|
|
|
597
601
|
return structuredClone(this.#status);
|
|
598
602
|
}
|
|
599
603
|
|
|
600
|
-
accept(message, { contextSnapshot } = {}) {
|
|
604
|
+
accept(message, { contextSnapshot, busyMessageMode } = {}) {
|
|
601
605
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
602
606
|
const messageId = nonEmptyString(message?.msgId);
|
|
603
607
|
const sender = senderStaffId(message);
|
|
@@ -716,6 +720,7 @@ export class DingtalkHarnessBridge {
|
|
|
716
720
|
const commandRunner = isDingtalkMenuCommand(commandText)
|
|
717
721
|
&& !hasInboundFiles(promptMessage) && !hasInboundImages(promptMessage)
|
|
718
722
|
? async () => { await this.#showMenu(message, key); }
|
|
723
|
+
: isClearCommand(commandText) ? runClearCommand
|
|
719
724
|
: isHistoryCommand(commandText) ? runHistoryCommand
|
|
720
725
|
: hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
|
721
726
|
? runControlCommand
|
|
@@ -834,7 +839,28 @@ export class DingtalkHarnessBridge {
|
|
|
834
839
|
pending.queue = current;
|
|
835
840
|
return finish(current);
|
|
836
841
|
}
|
|
837
|
-
return finish(this.#
|
|
842
|
+
return finish(this.#busyMessages.dispatch({
|
|
843
|
+
key, messageId, text: commandText, mode: busyMessageMode,
|
|
844
|
+
onFailure: (error) => this.#finishStatusReaction(statusReaction,
|
|
845
|
+
this.#signal?.aborted || error?.code === 'turn-stopped' ? 'clear' : 'error'),
|
|
846
|
+
eligible: () => Boolean(sessionWebhook) && addressed && String(message.msgtype).toLowerCase() === 'text'
|
|
847
|
+
&& !hasInboundImages(promptMessage) && !hasInboundFiles(promptMessage)
|
|
848
|
+
&& !hasReplyReference(promptMessage) && !isDingtalkMenuCommand(commandText)
|
|
849
|
+
&& canAutomaticallySteer(this.#accessPolicy, conversationType, sender),
|
|
850
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
851
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
852
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
853
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
854
|
+
enhancement: {
|
|
855
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
856
|
+
source: () => ({ channel: 'dingtalk', senderId: sender, senderName: message.senderNick,
|
|
857
|
+
conversationTitle: message.conversationTitle, chatId: message.conversationId }),
|
|
858
|
+
},
|
|
859
|
+
isQueued: () => this.#queues.has(key),
|
|
860
|
+
enqueue: (options) => this.#enqueueMessage(message, messageId, sender, key, { ...options, statusReaction }),
|
|
861
|
+
send: (reply) => sessionWebhook && addressed
|
|
862
|
+
? this.#send(sessionWebhook, reply, this.#atUsersFor(message)) : Promise.resolve(),
|
|
863
|
+
}));
|
|
838
864
|
}
|
|
839
865
|
|
|
840
866
|
async #showMenu(message, key) {
|
|
@@ -933,6 +959,7 @@ export class DingtalkHarnessBridge {
|
|
|
933
959
|
result = { message: helpText() };
|
|
934
960
|
} else {
|
|
935
961
|
result = await runWorkspaceCommand(command, this.#harness, key)
|
|
962
|
+
?? await runClearCommand(command, this.#harness, this.#state, key, options)
|
|
936
963
|
?? await runCompactCommand(command, this.#harness, this.#state, key, options)
|
|
937
964
|
?? await runControlCommand(command, this.#harness, this.#state, key, options)
|
|
938
965
|
?? await runHistoryCommand(command, this.#harness, this.#state, key, options)
|
|
@@ -1133,6 +1160,7 @@ export class DingtalkHarnessBridge {
|
|
|
1133
1160
|
}
|
|
1134
1161
|
|
|
1135
1162
|
async waitForIdle() {
|
|
1163
|
+
await this.#busyMessages.whenIdle();
|
|
1136
1164
|
await Promise.allSettled([
|
|
1137
1165
|
...this.#queues.values(),
|
|
1138
1166
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
@@ -1162,6 +1190,7 @@ export class DingtalkHarnessBridge {
|
|
|
1162
1190
|
hasFiles: hasInboundFiles(prompt),
|
|
1163
1191
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
1164
1192
|
|| this.#approvals.hasPending(key),
|
|
1193
|
+
busy: this.#queues.has(key) || this.#batchInputs.status(key).phase !== 'idle',
|
|
1165
1194
|
control: { owner: this, key },
|
|
1166
1195
|
deferredDelivery: this.#deferred,
|
|
1167
1196
|
enhancement: captureContextEnhancementSource(
|
|
@@ -5,7 +5,7 @@ import { t } from '../shared/i18n.mjs';
|
|
|
5
5
|
export const DINGTALK_MENU_TEMPLATE_ID = '9ca31362-e5b3-409c-a694-6993706a6004.schema';
|
|
6
6
|
export const DINGTALK_CARD_TOPIC = '/v1.0/card/instances/callback';
|
|
7
7
|
export const DINGTALK_MENU_BUTTONS = Object.freeze({
|
|
8
|
-
new: '/new', history: '/history', stop: '/stop', compact: '/compact',
|
|
8
|
+
new: '/new', history: '/history', stop: '/stop', compact: '/compact', clear: '/clear',
|
|
9
9
|
status: '/status', help: '/help',
|
|
10
10
|
});
|
|
11
11
|
|
|
@@ -311,6 +311,7 @@ export class DingtalkRuntime {
|
|
|
311
311
|
} catch {
|
|
312
312
|
// Optional settings failures leave the original message path active.
|
|
313
313
|
}
|
|
314
|
+
const busyMessageMode = this.#harness.currentBusyMessageMode?.() ?? 'queue';
|
|
314
315
|
const task = Promise.resolve().then(async () => {
|
|
315
316
|
if (this.#bridge !== bridge) return;
|
|
316
317
|
let message;
|
|
@@ -330,7 +331,7 @@ export class DingtalkRuntime {
|
|
|
330
331
|
get botId() { return contextEnhancement?.botId; },
|
|
331
332
|
}, message.conversationType === '1' || message.conversationType === 1 ? 'direct'
|
|
332
333
|
: message.conversationType === '2' || message.conversationType === 2 ? 'group' : null);
|
|
333
|
-
await bridge.accept(message, { contextSnapshot });
|
|
334
|
+
await bridge.accept(message, { contextSnapshot, busyMessageMode });
|
|
334
335
|
}).catch(() => {
|
|
335
336
|
if (signal.aborted || this.#bridge !== bridge) return;
|
|
336
337
|
this.#status.lastError = t('钉钉消息处理失败。');
|
|
@@ -876,13 +876,14 @@ export class DiscordRuntime {
|
|
|
876
876
|
this.#contextEnhancement,
|
|
877
877
|
message.guild_id ? 'group' : 'direct',
|
|
878
878
|
);
|
|
879
|
+
const busyMessageMode = this.#harness.currentBusyMessageMode?.() ?? 'queue';
|
|
879
880
|
const pendingRoute = resolveDiscordMessageRoute(message, this.#config.platformId, {
|
|
880
881
|
api: this.#api,
|
|
881
882
|
channel: this.#channels.get(String(message.channel_id)),
|
|
882
883
|
signal: this.#abortController?.signal,
|
|
883
884
|
onChannel: (resolved) => this.#rememberChannel(resolved),
|
|
884
885
|
});
|
|
885
|
-
route = { pendingRoute, contextSnapshot };
|
|
886
|
+
route = { pendingRoute, contextSnapshot, busyMessageMode };
|
|
886
887
|
this.#routing.set(messageId, route);
|
|
887
888
|
void pendingRoute.finally(() => {
|
|
888
889
|
if (this.#routing.get(messageId) === route) this.#routing.delete(messageId);
|
|
@@ -893,6 +894,7 @@ export class DiscordRuntime {
|
|
|
893
894
|
if (normalized) {
|
|
894
895
|
await bridge.accept(normalized, {
|
|
895
896
|
contextSnapshot: route.contextSnapshot,
|
|
897
|
+
busyMessageMode: route.busyMessageMode,
|
|
896
898
|
...(accessDecision ? { accessDecision } : {}),
|
|
897
899
|
});
|
|
898
900
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
|
|
1
2
|
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import QRCode from 'qrcode';
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
isBatchInputCommand,
|
|
44
45
|
} from '../shared/batch-input.mjs';
|
|
45
46
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
47
|
+
import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
|
|
46
48
|
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
|
47
49
|
import {
|
|
48
50
|
isControlCommand,
|
|
@@ -178,7 +180,7 @@ const REPAIR_URL_HOSTS = new Set([
|
|
|
178
180
|
|
|
179
181
|
const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
|
180
182
|
/** Matches fast card commands that should not be queued behind a running task. */
|
|
181
|
-
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|(?:sessionlist|sessions)(?:\s|$)|workspacelist|workspaces|wsl|watchlist|archived(?:\s+(on|off))?)$/i;
|
|
183
|
+
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|clear|(?:sessionlist|sessions)(?:\s|$)|workspacelist|workspaces|wsl|watchlist|archived(?:\s+(on|off))?)$/i;
|
|
182
184
|
|
|
183
185
|
// ── Step push (分步直推) limits ──────────────────────────────────────────────
|
|
184
186
|
/** Feishu allows 5 msg/s per chat; step push stays well below that. */
|
|
@@ -617,6 +619,7 @@ export class FeishuHarnessBridge {
|
|
|
617
619
|
#contextEnhancement;
|
|
618
620
|
#accessPolicy;
|
|
619
621
|
#queues = new Map();
|
|
622
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
620
623
|
#batchInputs = new BatchInputManager();
|
|
621
624
|
#pendingInteractions = new Map();
|
|
622
625
|
#interactionKeys = new Map();
|
|
@@ -1388,7 +1391,27 @@ export class FeishuHarnessBridge {
|
|
|
1388
1391
|
this.#interactionTasks.add(current);
|
|
1389
1392
|
return current;
|
|
1390
1393
|
}
|
|
1391
|
-
return this.#
|
|
1394
|
+
return this.#busyMessages.dispatch({
|
|
1395
|
+
key, messageId, text: commandText,
|
|
1396
|
+
eligible: () => event.message.message_type === 'text' && !hasImages && !hasFiles
|
|
1397
|
+
&& !hasReplyReference(commandMessage) && !isFeishuLocalCommand(commandText)
|
|
1398
|
+
&& !(NUMBER_REPLY.test(commandText) && this.#menus.has(key))
|
|
1399
|
+
&& canAutomaticallySteer(this.#accessPolicy, conversationType, senderOpenId(event)),
|
|
1400
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
1401
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
1402
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
1403
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
1404
|
+
enhancement: {
|
|
1405
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
1406
|
+
source: () => ({ channel: 'feishu', senderId: senderOpenId(event),
|
|
1407
|
+
chatId: event.message.chat_id, threadId: event.message.thread_id }),
|
|
1408
|
+
},
|
|
1409
|
+
isQueued: () => this.#queues.has(key),
|
|
1410
|
+
enqueue: (options) => this.#enqueueMessage(event, messageId, key, processingReaction, options),
|
|
1411
|
+
send: (reply) => this.#send(event.message.chat_id, reply, { replyTo: messageId }),
|
|
1412
|
+
onSteered: () => this.#finishReaction(messageId, processingReaction, 'DONE'),
|
|
1413
|
+
onError: (error) => this.#handleMessageFailure(event, messageId, processingReaction, error),
|
|
1414
|
+
});
|
|
1392
1415
|
}
|
|
1393
1416
|
|
|
1394
1417
|
#finishBatchResult(event, messageId, processingReaction, result) {
|
|
@@ -1545,6 +1568,7 @@ export class FeishuHarnessBridge {
|
|
|
1545
1568
|
}
|
|
1546
1569
|
|
|
1547
1570
|
async waitForIdle() {
|
|
1571
|
+
await this.#busyMessages.whenIdle();
|
|
1548
1572
|
// Drain to a fixed point: awaited work can register compensation or
|
|
1549
1573
|
// another serialized tail before it settles.
|
|
1550
1574
|
for (;;) {
|
|
@@ -1600,6 +1624,7 @@ export class FeishuHarnessBridge {
|
|
|
1600
1624
|
deferredDelivery: this.#deferred,
|
|
1601
1625
|
hasFiles: hasInboundFiles(message),
|
|
1602
1626
|
pendingInteraction: this.#hasPendingInteraction(key),
|
|
1627
|
+
busy: this.#queues.has(key),
|
|
1603
1628
|
control: { owner: this, key },
|
|
1604
1629
|
enhancement: captureContextEnhancementSource(
|
|
1605
1630
|
this.#contextEnhancement,
|
|
@@ -1691,7 +1716,8 @@ export class FeishuHarnessBridge {
|
|
|
1691
1716
|
// 命令识别对 text 与纯文本 post 一视同仁:post 富文本若仅含单个
|
|
1692
1717
|
// 文本段落(如复制粘贴的 /new),同样按命令处理;带图片/文件不认。
|
|
1693
1718
|
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
|
1694
|
-
const
|
|
1719
|
+
const rawCommandText = text ? text.trim() : '';
|
|
1720
|
+
const commandText = !hasImages && !hasFiles && text ? rawCommandText : null;
|
|
1695
1721
|
if (!text && !hasImages && !hasFiles && !hasReply) {
|
|
1696
1722
|
// An "@bot" with nothing else carries no instruction to parse, and the
|
|
1697
1723
|
// menu card is what the reader is reaching for — answer it the way `/m`
|
|
@@ -1751,6 +1777,19 @@ export class FeishuHarnessBridge {
|
|
|
1751
1777
|
}
|
|
1752
1778
|
return;
|
|
1753
1779
|
}
|
|
1780
|
+
if (isClearCommand(rawCommandText)) {
|
|
1781
|
+
const clearCommand = await runClearCommand(rawCommandText, this.#harness, this.#state, key, {
|
|
1782
|
+
signal: this.#signal,
|
|
1783
|
+
hasImages,
|
|
1784
|
+
hasFiles,
|
|
1785
|
+
pendingInteraction: this.#hasPendingInteraction(key),
|
|
1786
|
+
busy: this.#queues.has(key),
|
|
1787
|
+
});
|
|
1788
|
+
if (clearCommand) {
|
|
1789
|
+
await this.#send(event.message.chat_id, clearCommand.message, { replyTo: event.message.message_id });
|
|
1790
|
+
}
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1754
1793
|
if (SESSION_LIST_PREFIX.test(commandText)) {
|
|
1755
1794
|
const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
|
|
1756
1795
|
const request = parseSessionListArgument(argument);
|
|
@@ -2551,6 +2590,7 @@ export class FeishuHarnessBridge {
|
|
|
2551
2590
|
actor = null,
|
|
2552
2591
|
menuContext = null,
|
|
2553
2592
|
newSessionActionId = null,
|
|
2593
|
+
fromMessageQueue = false,
|
|
2554
2594
|
}) {
|
|
2555
2595
|
// Confirmations triggered by a card interaction stay anchored to the
|
|
2556
2596
|
// card's message so they land inside the same Feishu topic.
|
|
@@ -2704,7 +2744,9 @@ export class FeishuHarnessBridge {
|
|
|
2704
2744
|
await this.#handleNewSessionMenu({ key, chatId, messageId, menuContext, newSessionActionId });
|
|
2705
2745
|
return;
|
|
2706
2746
|
}
|
|
2707
|
-
|
|
2747
|
+
// A queued menu pick runs after earlier messages; its own unfinished
|
|
2748
|
+
// queue (including later messages) must not prevent the reset.
|
|
2749
|
+
if ((!fromMessageQueue && this.#queues.has(key)) || this.#hasPendingInteraction(key)) {
|
|
2708
2750
|
await reply(t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'));
|
|
2709
2751
|
return;
|
|
2710
2752
|
}
|
|
@@ -2739,6 +2781,10 @@ export class FeishuHarnessBridge {
|
|
|
2739
2781
|
await this.#handleCompact(key, chatId, messageId);
|
|
2740
2782
|
return;
|
|
2741
2783
|
}
|
|
2784
|
+
if (action === 'clear') {
|
|
2785
|
+
await this.#handleClear(key, chatId, messageId);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2742
2788
|
if (action === 'stop') {
|
|
2743
2789
|
await this.#handleStop(key, chatId, messageId);
|
|
2744
2790
|
return;
|
|
@@ -2927,7 +2973,9 @@ export class FeishuHarnessBridge {
|
|
|
2927
2973
|
await this.#handleRepairCommand(event, '/repair');
|
|
2928
2974
|
return;
|
|
2929
2975
|
}
|
|
2930
|
-
await this.#handleCardAction(action, {
|
|
2976
|
+
await this.#handleCardAction(action, {
|
|
2977
|
+
chatId, key, messageId: replyTo, fromMessageQueue: action === 'new',
|
|
2978
|
+
});
|
|
2931
2979
|
return;
|
|
2932
2980
|
}
|
|
2933
2981
|
if (menu.kind === 'sessions') {
|
|
@@ -3509,6 +3557,22 @@ export class FeishuHarnessBridge {
|
|
|
3509
3557
|
}
|
|
3510
3558
|
}
|
|
3511
3559
|
|
|
3560
|
+
/** Run /clear while retaining the current Session binding. */
|
|
3561
|
+
async #handleClear(key, chatId, replyTo = null) {
|
|
3562
|
+
try {
|
|
3563
|
+
const result = await runClearCommand(
|
|
3564
|
+
'/clear', this.#harness, this.#state, key, {
|
|
3565
|
+
signal: this.#signal,
|
|
3566
|
+
pendingInteraction: this.#hasPendingInteraction(key),
|
|
3567
|
+
busy: this.#queues.has(key),
|
|
3568
|
+
},
|
|
3569
|
+
);
|
|
3570
|
+
await this.#send(chatId, result?.message || t('清空当前会话上下文失败,当前会话未修改,请稍后重试。'), { replyTo });
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
await this.#sendFailure(chatId, error, { logLabel: 'clear', replyTo });
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3512
3576
|
/**
|
|
3513
3577
|
* Stop the running task in the bound session (mirrors `/stop`).
|
|
3514
3578
|
*/
|
|
@@ -353,6 +353,7 @@ export function menuCard(ctx) {
|
|
|
353
353
|
// ── 任务控制(对运行中任务的操作)────────────────────────
|
|
354
354
|
elements.push({ tag: 'div', text: markdown(t('**任务控制**')) });
|
|
355
355
|
elements.push(buttonPair(t('⏹ 停止'), 'stop', t('📐 压缩'), 'compact'));
|
|
356
|
+
elements.push(button(t('🧹 清空上下文'), 'clear'));
|
|
356
357
|
elements.push({ tag: 'hr' });
|
|
357
358
|
|
|
358
359
|
// ── 补充指令 + 归档切换(并列)────────────────────────────
|
|
@@ -573,6 +574,7 @@ export function menuHelpText() {
|
|
|
573
574
|
'/status 连接状态',
|
|
574
575
|
'/version 查看插件版本',
|
|
575
576
|
'/compact 压缩当前会话上下文',
|
|
577
|
+
'/clear 清空当前会话上下文(保留 Session 绑定和历史记录)',
|
|
576
578
|
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
|
577
579
|
'/archived on/off 会话列表显示/隐藏归档',
|
|
578
580
|
'',
|
|
@@ -620,10 +622,11 @@ const HELP_CARD_FEATURES = [
|
|
|
620
622
|
'6. 📋 会话/关注 — 查看/绑定会话,管理关注',
|
|
621
623
|
'7. ⏹ 停止 — 停止当前任务',
|
|
622
624
|
'8. 📐 压缩 — 压缩当前会话上下文',
|
|
623
|
-
'9.
|
|
624
|
-
'10.
|
|
625
|
-
'11.
|
|
626
|
-
'12.
|
|
625
|
+
'9. 🧹 清空上下文 — 清空上下文并保留绑定和历史记录',
|
|
626
|
+
'10. 补充指令 — 给 Agent 发送指令',
|
|
627
|
+
'11. 🗄 归档切换 — 显示/隐藏归档会话',
|
|
628
|
+
'12. 📊 状态 — 查看系统连接状态',
|
|
629
|
+
'13. 📖 帮助 — 查看本帮助',
|
|
627
630
|
].join('\n');
|
|
628
631
|
|
|
629
632
|
const HELP_TEXT_COMMANDS = [
|
|
@@ -638,6 +641,7 @@ const HELP_TEXT_COMMANDS = [
|
|
|
638
641
|
'`/workspacelist` — 列出工作区',
|
|
639
642
|
'`/status` — 查看连接状态',
|
|
640
643
|
'`/compact` — 压缩上下文',
|
|
644
|
+
'`/clear` — 清空上下文并保留 Session 绑定和历史记录',
|
|
641
645
|
'`/stop` — 停止当前任务',
|
|
642
646
|
'`/steer 指令` — 补充指令',
|
|
643
647
|
'`/watch ID` — 关注会话',
|
|
@@ -30,6 +30,7 @@ export const SLASH_COMMAND_MANIFEST = Object.freeze([
|
|
|
30
30
|
{ command: 'help', icon: 'promptword_outlined', default: '查看帮助', en_us: 'Show help' },
|
|
31
31
|
{ command: 'status', icon: 'ai-functions_outlined', default: '查看机器人状态', en_us: 'Show bot status' },
|
|
32
32
|
{ command: 'compact', icon: 'ai-block_outlined', default: '压缩当前会话上下文', en_us: 'Compact the current session' },
|
|
33
|
+
{ command: 'clear', icon: 'clear_outlined', default: '清空当前会话上下文', en_us: 'Clear the current session context' },
|
|
33
34
|
{ command: 'sessionlist', icon: 'chat-ai_outlined', default: '列出会话', en_us: 'List sessions' },
|
|
34
35
|
{ command: 'workspacelist', icon: 'folder_outlined', default: '列出工作区', en_us: 'List workspaces' },
|
|
35
36
|
{ command: 'workspaces', icon: 'folder_outlined', default: '列出工作区', en_us: 'List workspaces' },
|
|
@@ -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('回答问题、审批和批量输入时,数字优先用于当前交互。'),
|