@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
|
@@ -19,6 +19,11 @@ const APPROVAL_UNAVAILABLE_TEXT = '机器人端无法处理这次审批,请到
|
|
|
19
19
|
const APPROVAL_PROMPT = '请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。';
|
|
20
20
|
const APPROVAL_AFTER_QUESTION_PROMPT = '请先完成当前问题,再精准回复「批准」或「拒绝」。';
|
|
21
21
|
const APPROVAL_RESOLVED_TEXT = '该审批已处理,无需再次回复。';
|
|
22
|
+
const APPROVAL_TIMEOUT_TEXT = '审批已超时,已自动拒绝此次操作。';
|
|
23
|
+
const APPROVAL_TIMEOUT_UNCONFIRMED_TEXT = '审批已超时,正在自动拒绝,但暂未得到确认,稍后会自动重试。';
|
|
24
|
+
const DEFAULT_APPROVAL_TIMEOUT_MS = 60 * 60_000;
|
|
25
|
+
const EXPIRY_RETRY_BASE_MS = 30_000;
|
|
26
|
+
const EXPIRY_RETRY_MAX_MS = 5 * 60_000;
|
|
22
27
|
const RESOLVED_ROUTE_TTL_MS = 5 * 60_000;
|
|
23
28
|
const MAX_RESOLVED_ROUTES = 2_048;
|
|
24
29
|
|
|
@@ -102,6 +107,7 @@ function approvalResult(pending, outcome) {
|
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
function approvalOutcomeText(outcome) {
|
|
110
|
+
if (outcome === 'expired') return t(APPROVAL_TIMEOUT_TEXT);
|
|
105
111
|
if (outcome === 'unavailable') return t(APPROVAL_UNAVAILABLE_TEXT);
|
|
106
112
|
if (outcome === 'allowed-once') return t('已批准,仅对本次操作有效。');
|
|
107
113
|
if (outcome === 'rejected') return t('已拒绝此次操作。');
|
|
@@ -114,10 +120,14 @@ export class HarnessApprovalQueue {
|
|
|
114
120
|
#byId = new Map();
|
|
115
121
|
#routes = new Map();
|
|
116
122
|
#resolvedRoutes = new Map();
|
|
123
|
+
#approvalTimeoutMs;
|
|
117
124
|
|
|
118
|
-
constructor({ label = 'IM', logger = console } = {}) {
|
|
125
|
+
constructor({ label = 'IM', logger = console, approvalTimeoutMs = DEFAULT_APPROVAL_TIMEOUT_MS } = {}) {
|
|
119
126
|
this.#label = label;
|
|
120
127
|
this.#logger = logger;
|
|
128
|
+
this.#approvalTimeoutMs = Number.isFinite(approvalTimeoutMs) && approvalTimeoutMs > 0
|
|
129
|
+
? approvalTimeoutMs
|
|
130
|
+
: DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
hasPending(key) {
|
|
@@ -184,6 +194,10 @@ export class HarnessApprovalQueue {
|
|
|
184
194
|
}
|
|
185
195
|
await pending.activationTask?.catch(() => undefined);
|
|
186
196
|
await pending.presentationTask?.catch(() => undefined);
|
|
197
|
+
if (!pending.inactive && pending.expiring) {
|
|
198
|
+
await send(t(APPROVAL_TIMEOUT_UNCONFIRMED_TEXT));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
187
201
|
if (pending.inactive || pending.resolving) {
|
|
188
202
|
await send(t(APPROVAL_RESOLVED_TEXT));
|
|
189
203
|
return;
|
|
@@ -338,11 +352,22 @@ export class HarnessApprovalQueue {
|
|
|
338
352
|
closedOutcome: null,
|
|
339
353
|
resolutionNotified: false,
|
|
340
354
|
activationTask: null,
|
|
355
|
+
timeoutTimer: null,
|
|
356
|
+
expired: false,
|
|
357
|
+
expiring: false,
|
|
358
|
+
expiryAttempts: 0,
|
|
359
|
+
expiryNoticeSent: false,
|
|
341
360
|
};
|
|
342
361
|
this.#byId.set(approvalId, pending);
|
|
343
362
|
const route = this.#routes.get(key) ?? { items: [] };
|
|
344
363
|
route.items.push(pending);
|
|
345
364
|
this.#routes.set(key, route);
|
|
365
|
+
pending.timeoutTimer = setTimeout(() => {
|
|
366
|
+
void this.#expire(pending).catch((error) => {
|
|
367
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to expire an approval:`, error);
|
|
368
|
+
});
|
|
369
|
+
}, this.#approvalTimeoutMs);
|
|
370
|
+
pending.timeoutTimer.unref?.();
|
|
346
371
|
if (route.items[0] === pending) await this.#present(pending);
|
|
347
372
|
return true;
|
|
348
373
|
}
|
|
@@ -377,13 +402,15 @@ export class HarnessApprovalQueue {
|
|
|
377
402
|
const shouldNotify = pending.presented || presentationTask;
|
|
378
403
|
const send = pending.send;
|
|
379
404
|
const next = this.#remove(pending);
|
|
405
|
+
// The host reports our own timeout rejection as a plain rejection.
|
|
406
|
+
const outcome = pending.expiring && resolution.outcome === 'rejected' ? 'expired' : resolution.outcome;
|
|
380
407
|
await this.#transition(next, async () => {
|
|
381
408
|
let delivered = pending.presented;
|
|
382
409
|
if (presentationTask) {
|
|
383
410
|
delivered = await presentationTask.then(() => true, () => false);
|
|
384
411
|
}
|
|
385
412
|
if (shouldNotify && delivered) {
|
|
386
|
-
await this.#notifyResolved(pending,
|
|
413
|
+
await this.#notifyResolved(pending, outcome, send);
|
|
387
414
|
}
|
|
388
415
|
});
|
|
389
416
|
return true;
|
|
@@ -453,6 +480,70 @@ export class HarnessApprovalQueue {
|
|
|
453
480
|
}
|
|
454
481
|
}
|
|
455
482
|
|
|
483
|
+
async #expire(pending) {
|
|
484
|
+
pending.timeoutTimer = null;
|
|
485
|
+
pending.expired = true;
|
|
486
|
+
// An in-flight decision wins; #submit expires the approval if it fails.
|
|
487
|
+
if (pending.inactive || pending.resolving || pending.submitting) return;
|
|
488
|
+
pending.resolving = true;
|
|
489
|
+
pending.expiring = true;
|
|
490
|
+
// Keep the route barrier: an earlier confirmation must finish before a
|
|
491
|
+
// later approval can be presented.
|
|
492
|
+
await pending.activationTask?.catch(() => undefined);
|
|
493
|
+
if (pending.inactive || this.#byId.get(pending.approvalId) !== pending) return;
|
|
494
|
+
await this.#releaseExpired(pending);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async #releaseExpired(pending) {
|
|
498
|
+
pending.timeoutTimer = null;
|
|
499
|
+
if (pending.inactive) return;
|
|
500
|
+
let outcome = 'expired';
|
|
501
|
+
try {
|
|
502
|
+
// Reject rather than withdraw: a withdrawal only retires the IM side and
|
|
503
|
+
// would leave a shared Web approval waiting without a bound.
|
|
504
|
+
await pending.interaction.respond(
|
|
505
|
+
approvalResult(pending, 'rejected'),
|
|
506
|
+
{ signal: AbortSignal.timeout(5_000) },
|
|
507
|
+
);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
510
|
+
if (pending.inactive) return;
|
|
511
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to release an expired approval:`, error);
|
|
512
|
+
this.#retryExpiry(pending);
|
|
513
|
+
if (!pending.expiryNoticeSent && pending.presented) {
|
|
514
|
+
pending.expiryNoticeSent = true;
|
|
515
|
+
await pending.send(t(APPROVAL_TIMEOUT_UNCONFIRMED_TEXT)).catch(() => undefined);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
outcome = 'resolved';
|
|
520
|
+
}
|
|
521
|
+
if (pending.inactive) return;
|
|
522
|
+
const presentationTask = pending.presentationTask;
|
|
523
|
+
const shouldNotify = pending.presented || presentationTask;
|
|
524
|
+
const send = pending.send;
|
|
525
|
+
const next = this.#remove(pending);
|
|
526
|
+
pending.closedOutcome = outcome;
|
|
527
|
+
await this.#transition(next, async () => {
|
|
528
|
+
let delivered = pending.presented;
|
|
529
|
+
if (presentationTask) {
|
|
530
|
+
delivered = await presentationTask.then(() => true, () => false);
|
|
531
|
+
}
|
|
532
|
+
if (shouldNotify && delivered) await this.#notifyResolved(pending, outcome, send);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#retryExpiry(pending) {
|
|
537
|
+
const delay = Math.min(EXPIRY_RETRY_BASE_MS * 2 ** pending.expiryAttempts, EXPIRY_RETRY_MAX_MS);
|
|
538
|
+
pending.expiryAttempts += 1;
|
|
539
|
+
pending.timeoutTimer = setTimeout(() => {
|
|
540
|
+
void this.#releaseExpired(pending).catch((error) => {
|
|
541
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to expire an approval:`, error);
|
|
542
|
+
});
|
|
543
|
+
}, delay);
|
|
544
|
+
pending.timeoutTimer.unref?.();
|
|
545
|
+
}
|
|
546
|
+
|
|
456
547
|
async #submit(pending, outcome) {
|
|
457
548
|
pending.submitting = true;
|
|
458
549
|
try {
|
|
@@ -478,6 +569,11 @@ export class HarnessApprovalQueue {
|
|
|
478
569
|
if (pending.inactive) return;
|
|
479
570
|
pending.submitting = false;
|
|
480
571
|
this.#logger.error?.(`[dsh-im:${this.#label}] failed to submit an approval:`, error);
|
|
572
|
+
// The deadline passed while this decision was in flight.
|
|
573
|
+
if (pending.expired) {
|
|
574
|
+
await this.#expire(pending);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
481
577
|
await pending.send(t('审批提交失败,请重新回复「批准」或「拒绝」。')).catch(() => undefined);
|
|
482
578
|
return;
|
|
483
579
|
}
|
|
@@ -536,6 +632,10 @@ export class HarnessApprovalQueue {
|
|
|
536
632
|
#remove(pending) {
|
|
537
633
|
if (pending.inactive) return null;
|
|
538
634
|
pending.inactive = true;
|
|
635
|
+
if (pending.timeoutTimer) {
|
|
636
|
+
clearTimeout(pending.timeoutTimer);
|
|
637
|
+
pending.timeoutTimer = null;
|
|
638
|
+
}
|
|
539
639
|
this.#rememberResolvedRoute(pending.key);
|
|
540
640
|
this.#byId.delete(pending.approvalId);
|
|
541
641
|
const route = this.#routes.get(pending.key);
|
|
@@ -259,10 +259,10 @@ export default {
|
|
|
259
259
|
'/presetlist or /presets List available Agent Presets',
|
|
260
260
|
'🎮 任务控制': '🎮 Task controls',
|
|
261
261
|
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
|
262
|
-
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\
|
|
263
|
-
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\
|
|
264
|
-
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话\n`/workspace 工作区序号或绝对路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` 或 `/presets` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
|
|
265
|
-
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/sessionlist --limit N` or `/sessions --limit N` — list only the first N sessions in the current workspace\n`/workspace <workspace index or absolute path>` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` or `/presets` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
|
|
262
|
+
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 🧹 清空上下文 — 清空上下文并保留绑定和历史记录\n10. 补充指令 — 给 Agent 发送指令\n11. 🗄 归档切换 — 显示/隐藏归档会话\n12. 📊 状态 — 查看系统连接状态\n13. 📖 帮助 — 查看本帮助':
|
|
263
|
+
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. 🧹 Clear context — clear context while keeping the binding and history\n10. Steer task — send an instruction to the Agent\n11. 🗄 Archived toggle — show or hide archived sessions\n12. 📊 Status — view connection status\n13. 📖 Help — view this help',
|
|
264
|
+
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/sessionlist --limit N` 或 `/sessions --limit N` — 仅列出当前工作区前 N 个会话\n`/workspace 工作区序号或绝对路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/clear` — 清空上下文并保留 Session 绑定和历史记录\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` 或 `/presets` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
|
|
265
|
+
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/sessionlist --limit N` or `/sessions --limit N` — list only the first N sessions in the current workspace\n`/workspace <workspace index or absolute path>` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/clear` — clear context while keeping the Session binding and history\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` or `/presets` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
|
|
266
266
|
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
|
|
267
267
|
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Complete permissions · **6** Help',
|
|
268
268
|
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
|
@@ -319,6 +319,8 @@ export default {
|
|
|
319
319
|
'/watch ID 或序号 关注会话(完成后推送)':
|
|
320
320
|
'/watch ID or index Watch a session (push when done)',
|
|
321
321
|
'/compact 压缩上下文': '/compact Compact the context',
|
|
322
|
+
'🧹 清空上下文': '🧹 Clear context',
|
|
323
|
+
'清空当前会话上下文并保留绑定和历史记录': 'Clear the current session context while keeping the binding and history',
|
|
322
324
|
'/workspace 绝对路径 切换工作区': '/workspace <absolute path> Switch workspace',
|
|
323
325
|
'/presetlist 列出可用 Agent Preset': '/presetlist List available Agent Presets',
|
|
324
326
|
'任务完成会自动推送,回复数字或点按钮取消关注:':
|
|
@@ -29,7 +29,8 @@ export default {
|
|
|
29
29
|
'发送 /m 打开助手中心,点击按钮或回复当前列表的数字选择。': 'Send /m to open the assistant menu. Click a button or reply with a number from the current list.',
|
|
30
30
|
'设置:会话、工作区、模式/预设、模型。': 'Settings: session, workspace, mode / preset, model.',
|
|
31
31
|
'会话:新会话、会话列表、归档显示切换。': 'Sessions: new session, session list, show or hide archived sessions.',
|
|
32
|
-
'
|
|
32
|
+
'任务:停止、压缩、清空上下文、快捷或自定义补充指令。':
|
|
33
|
+
'Tasks: stop, compact, clear context, quick or custom steering instructions.',
|
|
33
34
|
'信息:状态、帮助。': 'Information: status, help.',
|
|
34
35
|
'菜单有效期为 15 分钟;发送普通消息会退出数字选择。': 'Menus expire after 15 minutes. Sending an ordinary message exits number selection.',
|
|
35
36
|
'回答问题、审批和批量输入时,数字优先用于当前交互。': 'During questions, approvals and batch input, numbers belong to that interaction.',
|
|
@@ -244,6 +244,32 @@ export default {
|
|
|
244
244
|
'当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。':
|
|
245
245
|
'The current Harness has not registered the /compact command; please make sure the context compaction component is enabled.',
|
|
246
246
|
|
|
247
|
+
// clear-command.mjs
|
|
248
|
+
'/clear 清空当前会话上下文(保留 Session 绑定和历史记录)':
|
|
249
|
+
'/clear Clear the current session context (keep the Session binding and history)',
|
|
250
|
+
'清空当前会话上下文并保留会话绑定':
|
|
251
|
+
'Clear the current session context and keep the Session binding',
|
|
252
|
+
'用法:/clear(不带参数且不可附带图片或文件)':
|
|
253
|
+
'Usage: /clear (no arguments, images or files)',
|
|
254
|
+
'当前聊天绑定的会话已不存在,未清空上下文;请先发送新消息开启会话。':
|
|
255
|
+
'The Session bound to this chat no longer exists; the context was not cleared. Send a new message to start a Session first.',
|
|
256
|
+
'当前会话正在生成回复或等待交互,请先完成交互或发送 /stop,再执行 /clear。':
|
|
257
|
+
'The current Session is generating a reply or waiting for an interaction. Finish it or send /stop before running /clear.',
|
|
258
|
+
'上下文清理已取消,当前会话未修改。':
|
|
259
|
+
'Context clearing was cancelled; the current Session was not changed.',
|
|
260
|
+
'工作区或机器人状态已发生变化,未清空当前会话,请重试。':
|
|
261
|
+
'The workspace or bot state changed, so the current Session was not cleared. Please try again.',
|
|
262
|
+
'当前 Host 不支持 /clear,当前会话未修改。':
|
|
263
|
+
'The current Host does not support /clear; the current Session was not changed.',
|
|
264
|
+
'清空当前会话上下文失败,当前会话未修改,请稍后重试。':
|
|
265
|
+
'Failed to clear the current Session context; the Session was not changed. Please try again later.',
|
|
266
|
+
'当前机器人没有可用的会话状态,未清空当前会话。':
|
|
267
|
+
'This bot has no available Session state; the current Session was not cleared.',
|
|
268
|
+
'当前聊天还没有可清空的会话,请先发送一条消息。':
|
|
269
|
+
'This chat has no Session to clear yet; please send a message first.',
|
|
270
|
+
'当前会话上下文已清空;Session 绑定和历史记录仍保留。':
|
|
271
|
+
'The current Session context was cleared; its binding and history were retained.',
|
|
272
|
+
|
|
247
273
|
// control-command.mjs
|
|
248
274
|
'用法:/stop(不带参数)': 'Usage: /stop (no arguments)',
|
|
249
275
|
'用法:/version(不带参数)': 'Usage: /version (no arguments)',
|
|
@@ -258,6 +284,7 @@ export default {
|
|
|
258
284
|
'No session is bound to this chat, so an additional instruction cannot be given. Bind a session first.',
|
|
259
285
|
'任务已结束,没有正在运行的任务,无法补充指令。请直接发送消息开始新任务。':
|
|
260
286
|
'The task has ended and there is no running task to steer. Send a message to start a new task.',
|
|
287
|
+
'已排队,等待前序消息处理。': 'Queued. Waiting for the preceding messages to finish.',
|
|
261
288
|
'已提交补充指令,Agent 会在下一步读取。':
|
|
262
289
|
'Additional instruction submitted; the Agent will read it at the next step.',
|
|
263
290
|
};
|
|
@@ -56,6 +56,10 @@ export default {
|
|
|
56
56
|
'Please finish the current question first, then reply exactly with 「批准」 (approve) or 「拒绝」 (reject).',
|
|
57
57
|
'该审批已处理,无需再次回复。':
|
|
58
58
|
'This approval has already been handled; no need to reply again.',
|
|
59
|
+
'审批已超时,已自动拒绝此次操作。':
|
|
60
|
+
'This approval timed out and the operation was rejected automatically.',
|
|
61
|
+
'审批已超时,正在自动拒绝,但暂未得到确认,稍后会自动重试。':
|
|
62
|
+
'This approval timed out. Automatic rejection has not been confirmed yet and will be retried shortly.',
|
|
59
63
|
'DeepSeek Harness 需要你的审批:':
|
|
60
64
|
'DeepSeek Harness needs your approval:',
|
|
61
65
|
'工具:{tool}': 'Tool: {tool}',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const SET_MESSAGE_MODE_ENDPOINT = 'bot.message-mode.set';
|
|
2
|
+
|
|
3
|
+
export function normalizeBusyMessageMode(value) {
|
|
4
|
+
return value === 'steer' ? 'steer' : 'queue';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function validateBusyMessageMode(value) {
|
|
8
|
+
if (value !== 'queue' && value !== 'steer') {
|
|
9
|
+
throw new TypeError('Message mode must be queue or steer');
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validMessageModePayload(payload) {
|
|
15
|
+
return payload !== null && typeof payload === 'object' && !Array.isArray(payload)
|
|
16
|
+
&& Object.keys(payload).length === 2
|
|
17
|
+
&& typeof payload.botId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)
|
|
18
|
+
&& (payload.busyMessageMode === 'queue' || payload.busyMessageMode === 'steer');
|
|
19
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BusyMessageDispatcher, canAutomaticallySteer } from './busy-message-dispatcher.mjs';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
|
|
3
4
|
import { createDeferredDeliveryCoordinator, deferredOutcomeText } from './deferred-delivery-coordinator.mjs';
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from './context-enhancement.mjs';
|
|
15
16
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
16
17
|
import { runCompactCommand } from './compact-command.mjs';
|
|
18
|
+
import { isClearCommand, runClearCommand } from './clear-command.mjs';
|
|
17
19
|
import { isHistoryCommand, runHistoryCommand } from './history-command.mjs';
|
|
18
20
|
import {
|
|
19
21
|
isControlCommand,
|
|
@@ -178,6 +180,7 @@ export class TextHarnessBridge {
|
|
|
178
180
|
#signal;
|
|
179
181
|
#keepaliveIntervalMs;
|
|
180
182
|
#queues = new Map();
|
|
183
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
181
184
|
#pendingInteractions = new Map();
|
|
182
185
|
#interactionKeys = new Map();
|
|
183
186
|
// Keep the accepted configuration through the existing queue/reply lifecycle.
|
|
@@ -236,7 +239,7 @@ export class TextHarnessBridge {
|
|
|
236
239
|
return structuredClone(this.#status);
|
|
237
240
|
}
|
|
238
241
|
|
|
239
|
-
accept(message, { contextSnapshot, accessDecision } = {}) {
|
|
242
|
+
accept(message, { contextSnapshot, accessDecision, busyMessageMode } = {}) {
|
|
240
243
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
241
244
|
const conversationId = cleanText(message?.conversationId);
|
|
242
245
|
const kind = message?.kind === 'group' ? 'group' : 'direct';
|
|
@@ -300,7 +303,7 @@ export class TextHarnessBridge {
|
|
|
300
303
|
});
|
|
301
304
|
normalized.statusReaction = statusReaction;
|
|
302
305
|
|
|
303
|
-
const processing = this.#acceptAcceptedMessage(normalized, messageId, senderId);
|
|
306
|
+
const processing = this.#acceptAcceptedMessage(normalized, messageId, senderId, busyMessageMode);
|
|
304
307
|
void processing.then(
|
|
305
308
|
() => statusReaction.success(),
|
|
306
309
|
() => statusReaction.error(),
|
|
@@ -308,7 +311,7 @@ export class TextHarnessBridge {
|
|
|
308
311
|
return processing;
|
|
309
312
|
}
|
|
310
313
|
|
|
311
|
-
#acceptAcceptedMessage(normalized, messageId, senderId) {
|
|
314
|
+
#acceptAcceptedMessage(normalized, messageId, senderId, busyMessageMode) {
|
|
312
315
|
if (normalized.kind === 'direct') {
|
|
313
316
|
rememberConnectionTestTarget(
|
|
314
317
|
this.#state,
|
|
@@ -319,6 +322,18 @@ export class TextHarnessBridge {
|
|
|
319
322
|
const key = `${normalized.kind}:${normalized.conversationId}`;
|
|
320
323
|
const pending = this.#pendingInteractions.get(key);
|
|
321
324
|
const text = controlTextOf(normalized);
|
|
325
|
+
if (isClearCommand(text) && (
|
|
326
|
+
this.#queues.has(key)
|
|
327
|
+
|| pending
|
|
328
|
+
|| this.#approvals.hasPending(key)
|
|
329
|
+
|| this.#batches.status(key).phase !== 'idle'
|
|
330
|
+
)) {
|
|
331
|
+
return this.#finishLocalMessage(
|
|
332
|
+
normalized,
|
|
333
|
+
messageId,
|
|
334
|
+
t('当前会话正在生成回复或等待交互,请先完成交互或发送 /stop,再执行 /clear。'),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
322
337
|
const batchCommand = isBatchInputCommand(text);
|
|
323
338
|
if (batchCommand && normalized.kind === 'group' && normalized.addressed === true) {
|
|
324
339
|
return this.#finishLocalMessage(
|
|
@@ -373,7 +388,8 @@ export class TextHarnessBridge {
|
|
|
373
388
|
}
|
|
374
389
|
const collectingBatch = normalized.kind === 'direct'
|
|
375
390
|
&& this.#batches.status(key).phase === 'collecting';
|
|
376
|
-
const commandRunner = collectingBatch ? null :
|
|
391
|
+
const commandRunner = collectingBatch ? null : isClearCommand(text) ? runClearCommand
|
|
392
|
+
: isHistoryCommand(text) ? runHistoryCommand
|
|
377
393
|
: hasInboundFiles(normalized) ? null : isControlCommand(text)
|
|
378
394
|
? runControlCommand
|
|
379
395
|
: (isModelCommand(text)
|
|
@@ -458,7 +474,33 @@ export class TextHarnessBridge {
|
|
|
458
474
|
pending.queue = current;
|
|
459
475
|
return current;
|
|
460
476
|
}
|
|
461
|
-
return this.#
|
|
477
|
+
return this.#busyMessages.dispatch({
|
|
478
|
+
key, messageId, text: normalized.content,
|
|
479
|
+
commandText: text, mode: busyMessageMode,
|
|
480
|
+
onFailure: (error) => this.#signal?.aborted || error?.code === 'turn-stopped'
|
|
481
|
+
? normalized.statusReaction?.clear() : normalized.statusReaction?.error(),
|
|
482
|
+
eligible: () => normalized.plainText !== false
|
|
483
|
+
&& !hasInboundImages(normalized) && !hasInboundFiles(normalized)
|
|
484
|
+
&& !hasReplyReference(normalized)
|
|
485
|
+
&& canAutomaticallySteer(this.#accessPolicy, normalized.kind,
|
|
486
|
+
[senderId, normalized.senderAlternateId].filter(Boolean)),
|
|
487
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
488
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
489
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
490
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
491
|
+
enhancement: {
|
|
492
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
493
|
+
source: () => {
|
|
494
|
+
const source = normalized.contextSource?.();
|
|
495
|
+
return { channel: this.#descriptor.key, senderId, senderName: source?.senderName,
|
|
496
|
+
conversationTitle: source?.conversationTitle,
|
|
497
|
+
chatId: source?.chatId ?? normalized.conversationId, threadId: source?.threadId };
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
isQueued: () => this.#queues.has(key),
|
|
501
|
+
enqueue: (options) => this.#enqueueMessage(normalized, messageId, senderId, key, options),
|
|
502
|
+
send: (reply) => this.#bot.sendText(normalized.replyTarget, reply),
|
|
503
|
+
});
|
|
462
504
|
}
|
|
463
505
|
|
|
464
506
|
#finishLocalMessage(message, messageId, reply, { recordReceived = true } = {}) {
|
|
@@ -522,6 +564,7 @@ export class TextHarnessBridge {
|
|
|
522
564
|
}
|
|
523
565
|
|
|
524
566
|
async waitForIdle() {
|
|
567
|
+
await this.#busyMessages.whenIdle();
|
|
525
568
|
await Promise.allSettled([
|
|
526
569
|
...this.#queues.values(),
|
|
527
570
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
|
|
1
2
|
import { imageInputLimits } from '../shared/image-input-policy.mjs';
|
|
2
3
|
import { imageDownloadLimitMessage } from '../shared/image-prompt.mjs';
|
|
3
4
|
import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
isBatchInputCommand,
|
|
21
22
|
} from '../shared/batch-input.mjs';
|
|
22
23
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
24
|
+
import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
|
|
23
25
|
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
|
24
26
|
import {
|
|
25
27
|
isControlCommand,
|
|
@@ -82,6 +84,7 @@ function helpText() {
|
|
|
82
84
|
t('/menu 或 /m 打开可点击的功能菜单'),
|
|
83
85
|
t('/new 开启一个全新会话'),
|
|
84
86
|
t('/compact 压缩当前会话的较早上下文'),
|
|
87
|
+
t('/clear 清空当前会话上下文(保留 Session 绑定和历史记录)'),
|
|
85
88
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
86
89
|
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
87
90
|
t('/workspacelist 列出工作区绝对路径'),
|
|
@@ -565,6 +568,7 @@ export class WecomHarnessBridge {
|
|
|
565
568
|
#signal;
|
|
566
569
|
#fileUploadTimeoutMs;
|
|
567
570
|
#queues = new Map();
|
|
571
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
568
572
|
#pendingInteractions = new Map();
|
|
569
573
|
#interactionKeys = new Map();
|
|
570
574
|
// Keep the accepted configuration through the existing queue/reply lifecycle.
|
|
@@ -870,6 +874,7 @@ export class WecomHarnessBridge {
|
|
|
870
874
|
return { message: t('已开启新会话。请发送你的问题。') };
|
|
871
875
|
}
|
|
872
876
|
return await runWorkspaceCommand(text, this.#harness, key)
|
|
877
|
+
?? await runClearCommand(text, this.#harness, this.#state, key, options)
|
|
873
878
|
?? await runCompactCommand(text, this.#harness, this.#state, key, options);
|
|
874
879
|
}
|
|
875
880
|
|
|
@@ -951,7 +956,8 @@ export class WecomHarnessBridge {
|
|
|
951
956
|
const menuCommand = !hasInboundImages(commandMessage) && !hasInboundFiles(commandMessage)
|
|
952
957
|
&& (parseWecomMenu(commandText) || ['/help', '/status', '/new'].includes(commandText.toLowerCase())
|
|
953
958
|
|| this.#cardFrames.has(frame));
|
|
954
|
-
const commandRunner =
|
|
959
|
+
const commandRunner = isClearCommand(commandText) ? runClearCommand
|
|
960
|
+
: isHistoryCommand(commandText) ? runHistoryCommand
|
|
955
961
|
: hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
|
956
962
|
? runControlCommand
|
|
957
963
|
: (isModelCommand(commandText)
|
|
@@ -1048,7 +1054,23 @@ export class WecomHarnessBridge {
|
|
|
1048
1054
|
pending.queue = current;
|
|
1049
1055
|
return current;
|
|
1050
1056
|
}
|
|
1051
|
-
return this.#
|
|
1057
|
+
return this.#busyMessages.dispatch({
|
|
1058
|
+
key, messageId, text: commandText,
|
|
1059
|
+
eligible: () => isNativeWecomText(frame) && !hasReplyReference(commandMessage)
|
|
1060
|
+
&& !parseWecomMenu(commandText) && !this.#cardFrames.has(frame)
|
|
1061
|
+
&& canAutomaticallySteer(this.#accessPolicy, conversationType, senderId),
|
|
1062
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
1063
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
1064
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
1065
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
1066
|
+
enhancement: {
|
|
1067
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
1068
|
+
source: () => ({ channel: 'wecom', senderId, chatId }),
|
|
1069
|
+
},
|
|
1070
|
+
isQueued: () => this.#queues.has(key),
|
|
1071
|
+
enqueue: (options) => this.#enqueueMessage(frame, messageId, key, options),
|
|
1072
|
+
send: (reply) => this.#sendImmediate(frame, chatId, reply),
|
|
1073
|
+
});
|
|
1052
1074
|
}
|
|
1053
1075
|
|
|
1054
1076
|
#enqueueMessage(frame, messageId, key, {
|
|
@@ -1151,6 +1173,7 @@ export class WecomHarnessBridge {
|
|
|
1151
1173
|
}
|
|
1152
1174
|
|
|
1153
1175
|
async waitForIdle() {
|
|
1176
|
+
await this.#busyMessages.whenIdle();
|
|
1154
1177
|
await Promise.allSettled([
|
|
1155
1178
|
...this.#queues.values(),
|
|
1156
1179
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
@@ -1176,6 +1199,7 @@ export class WecomHarnessBridge {
|
|
|
1176
1199
|
hasFiles: hasInboundFiles(message),
|
|
1177
1200
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
1178
1201
|
|| this.#approvals.hasPending(key),
|
|
1202
|
+
busy: this.#queues.has(key) || this.#batchInputs.status(key).phase !== 'idle',
|
|
1179
1203
|
control: { owner: this, key },
|
|
1180
1204
|
deferredDelivery: this.#deferred,
|
|
1181
1205
|
enhancement: captureContextEnhancementSource(
|
|
@@ -48,7 +48,7 @@ export function wecomMenu({ workspace, workspaces = [] } = {}) {
|
|
|
48
48
|
entries: [
|
|
49
49
|
[t('切换工作区'), 'select:workspace'],
|
|
50
50
|
[t('🆕 新会话'), '/new'],
|
|
51
|
-
[t('⏹ 停止'), '/stop'], [t('📐 压缩'), '/compact'],
|
|
51
|
+
[t('⏹ 停止'), '/stop'], [t('📐 压缩'), '/compact'], [t('🧹 清空上下文'), '/clear'],
|
|
52
52
|
[t('📊 状态'), '/status'], [t('📖 帮助'), '/help'],
|
|
53
53
|
],
|
|
54
54
|
};
|
|
@@ -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
|
harnessAnswerForQuestion,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
isBatchInputCommand,
|
|
12
13
|
} from '../shared/batch-input.mjs';
|
|
13
14
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
15
|
+
import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
|
|
14
16
|
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
|
15
17
|
import {
|
|
16
18
|
isControlCommand,
|
|
@@ -75,6 +77,7 @@ const HELP_TEXT = () => [
|
|
|
75
77
|
t('直接发送文字或图片即可继续当前会话。'),
|
|
76
78
|
t('/new 开启一个全新会话'),
|
|
77
79
|
t('/compact 压缩当前会话的较早上下文'),
|
|
80
|
+
t('/clear 清空当前会话上下文(保留 Session 绑定和历史记录)'),
|
|
78
81
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
79
82
|
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
80
83
|
t('/workspacelist 列出工作区绝对路径'),
|
|
@@ -202,6 +205,7 @@ export class WecomAppBridge {
|
|
|
202
205
|
#streamRegistry;
|
|
203
206
|
#maxChunkBytes;
|
|
204
207
|
#queues = new Map();
|
|
208
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
205
209
|
#acceptedMessageIds = new Map();
|
|
206
210
|
#commandTasks = new Set();
|
|
207
211
|
#approvalTasks = new Set();
|
|
@@ -311,7 +315,8 @@ export class WecomAppBridge {
|
|
|
311
315
|
return this.#finishBatchResult(messageId, key, sender, result);
|
|
312
316
|
}
|
|
313
317
|
}
|
|
314
|
-
const commandRunner =
|
|
318
|
+
const commandRunner = isClearCommand(commandText) ? runClearCommand
|
|
319
|
+
: isHistoryCommand(commandText) ? runHistoryCommand
|
|
315
320
|
: isControlCommand(commandText)
|
|
316
321
|
? runControlCommand
|
|
317
322
|
: (isModelCommand(commandText)
|
|
@@ -385,7 +390,24 @@ export class WecomAppBridge {
|
|
|
385
390
|
pending.queue = current;
|
|
386
391
|
return current;
|
|
387
392
|
}
|
|
388
|
-
return this.#
|
|
393
|
+
return this.#busyMessages.dispatch({
|
|
394
|
+
key, messageId, text: commandText,
|
|
395
|
+
eligible: () => message.msgtype === 'text' && !hasImages
|
|
396
|
+
&& !hasReplyReference(wecomAppInboundMessage(message, this.#api))
|
|
397
|
+
&& canAutomaticallySteer(this.#accessPolicy, 'direct', sender),
|
|
398
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
399
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
400
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
401
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
402
|
+
enhancement: {
|
|
403
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
404
|
+
source: () => ({ channel: 'wecom-app', senderId: sender, chatId: sender }),
|
|
405
|
+
},
|
|
406
|
+
isQueued: () => this.#queues.has(key),
|
|
407
|
+
enqueue: (options) => this.#enqueueMessage(message, messageId, key, { ...options, sink }),
|
|
408
|
+
send: (reply) => this.#send(sender, reply),
|
|
409
|
+
onError: (error) => this.#handleInteractionFailure(message, messageId, error),
|
|
410
|
+
});
|
|
389
411
|
}
|
|
390
412
|
|
|
391
413
|
#enqueueMessage(message, messageId, key, {
|
|
@@ -472,6 +494,7 @@ export class WecomAppBridge {
|
|
|
472
494
|
}
|
|
473
495
|
|
|
474
496
|
async waitForIdle() {
|
|
497
|
+
await this.#busyMessages.whenIdle();
|
|
475
498
|
await Promise.allSettled([
|
|
476
499
|
...this.#queues.values(),
|
|
477
500
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
@@ -501,6 +524,7 @@ export class WecomAppBridge {
|
|
|
501
524
|
hasFiles: false,
|
|
502
525
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
503
526
|
|| this.#approvals.hasPending(key),
|
|
527
|
+
busy: this.#queues.has(key) || this.#batchInputs.status(key).phase !== 'idle',
|
|
504
528
|
control: { owner: this, key },
|
|
505
529
|
deferredDelivery: this.#deferred,
|
|
506
530
|
enhancement: captureContextEnhancementSource(
|