@jeik/dingtalk-connector 0.8.37 → 0.8.38
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/CHANGELOG.md +10 -0
- package/dist/{channel-B56QFS9m.mjs → channel-BbpJFyga.mjs} +5 -5
- package/dist/{connection-Bh3U9I4x.mjs → connection-C7-3gzQW.mjs} +1 -1
- package/dist/{dws-delivery-context-ZJalcxYA.mjs → dws-delivery-context-BsTrxK68.mjs} +1 -1
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-DBRRhEk5.mjs +2 -0
- package/dist/{gateway-methods-Ce1FXmCx.mjs → gateway-methods-lTdxcCpO.mjs} +5 -5
- package/dist/index.mjs +2 -2
- package/dist/{media-g8tJtpfU.mjs → media-3G_kHjld.mjs} +8 -8
- package/dist/{media-DDtX37T2.mjs → media-o_kBcTXb.mjs} +1 -1
- package/dist/{message-handler-0LEoMD85.mjs → message-handler-DNgfzwqR.mjs} +57 -14
- package/dist/{messaging-CwQLykl6.mjs → messaging-DvW-2HJJ.mjs} +6 -6
- package/dist/{session-DJ4jYqPv.mjs → session-DsDDfhMw.mjs} +49 -3
- package/dist/{utils-Bwz2Fls_.mjs → utils-CNXrWqt1.mjs} +2 -2
- package/dist/{utils-CIfI_3Jh.mjs → utils-DXwaa6O-.mjs} +1 -0
- package/dist/{utils-legacy-CGEa8tg2.mjs → utils-legacy-D9TCnrMR.mjs} +1 -0
- package/dist/utils-legacy-vhmn24_Z.mjs +3 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/message-handler.ts +67 -5
- package/src/reply-dispatcher.ts +6 -0
- package/src/utils/constants.ts +15 -0
- package/src/utils/session.ts +51 -2
- package/dist/gateway-methods-CV8y0xGw.mjs +0 -2
- package/dist/utils-legacy-Bx5izlVF.mjs +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.38] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Features & Fixes
|
|
11
|
+
|
|
12
|
+
- **/stop 中止命令穿透入站队列与即时停止** — 当会话正在执行长任务时,发送 `/stop`、`/abort`、`停止` 等命令不再被当成普通消息排队(不再误触发“上一条还没结束”排队 ACK),而是实现优先级穿透:
|
|
13
|
+
- 立即调用网关 `chat.abort`,毫秒级终止底层 Agent 与运行进程(对齐 WebUI Stop);
|
|
14
|
+
- 自动作废当前会话已排队的其他后续任务,防止任务中止后排队的旧消息被幽灵唤醒;
|
|
15
|
+
- 穿透执行 `dispatchReplyFromConfig` fastAbort,即时向钉钉回执任务中止;
|
|
16
|
+
- 优化中止卡片文案为 `🛑 正在停止当前任务…`,原任务卡片因中止退出时定格为 `🛑 任务已中止`(不再报模型请求异常)。
|
|
17
|
+
|
|
8
18
|
## [0.8.35] - 2026-07-31
|
|
9
19
|
|
|
10
20
|
### Fixes
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { b as __exportAll } from "./media-
|
|
1
|
+
import { b as __exportAll } from "./media-3G_kHjld.mjs";
|
|
2
2
|
import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfiguredSecretInput, f as normalizeAccountId, l as createDefaultChannelRuntimeState, m as resolveDefaultGroupPolicy, o as resolveDingtalkCredentials, p as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultDingtalkAccountId, s as DEFAULT_ACCOUNT_ID, t as listDingtalkAccountIds, u as formatDocsLink } from "./accounts-BAzdqkAV.mjs";
|
|
3
3
|
import { t as createLogger } from "./logger-CnBTcwyq.mjs";
|
|
4
4
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
5
|
-
import "./utils-
|
|
6
|
-
import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-
|
|
5
|
+
import "./utils-DXwaa6O-.mjs";
|
|
6
|
+
import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-DvW-2HJJ.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { z, z as z$1 } from "zod";
|
|
9
9
|
//#region src/secret-input.ts
|
|
@@ -970,8 +970,8 @@ async function monitorDingtalkProvider(opts = {}) {
|
|
|
970
970
|
const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
|
|
971
971
|
const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
|
|
972
972
|
import("./accounts-BQptOmgB.mjs"),
|
|
973
|
-
import("./message-handler-
|
|
974
|
-
import("./connection-
|
|
973
|
+
import("./message-handler-DNgfzwqR.mjs"),
|
|
974
|
+
import("./connection-C7-3gzQW.mjs")
|
|
975
975
|
]);
|
|
976
976
|
const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
|
|
977
977
|
const { handleDingTalkMessage } = monitorAccountModule;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as checkAndMarkDingtalkMessage } from "./utils-legacy-
|
|
1
|
+
import { i as checkAndMarkDingtalkMessage } from "./utils-legacy-D9TCnrMR.mjs";
|
|
2
2
|
import { n as onBackgroundWorkCountChange, t as getActiveBackgroundWorkCount } from "./background-work-874-aK4-.mjs";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
//#region src/core/connection.ts
|
package/dist/entry-bundled.mjs
CHANGED
|
@@ -23,7 +23,7 @@ var entry_bundled_default = defineBundledChannelEntry({
|
|
|
23
23
|
exportName: "setDingtalkRuntime"
|
|
24
24
|
},
|
|
25
25
|
async registerFull(api) {
|
|
26
|
-
const { registerGatewayMethods } = await import("./gateway-methods-
|
|
26
|
+
const { registerGatewayMethods } = await import("./gateway-methods-DBRRhEk5.mjs");
|
|
27
27
|
registerGatewayMethods(api);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount, t as listDingtalkAccountIds } from "./accounts-BAzdqkAV.mjs";
|
|
2
2
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
|
-
import { r as getAccessToken, t as DINGTALK_API } from "./utils-
|
|
4
|
-
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-
|
|
5
|
-
import { d as getUnionId, h as recallEmotionReply } from "./utils-legacy-
|
|
3
|
+
import { r as getAccessToken, t as DINGTALK_API } from "./utils-DXwaa6O-.mjs";
|
|
4
|
+
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-DvW-2HJJ.mjs";
|
|
5
|
+
import { d as getUnionId, h as recallEmotionReply } from "./utils-legacy-D9TCnrMR.mjs";
|
|
6
6
|
//#region src/docs.ts
|
|
7
7
|
var DingtalkDocsClient = class {
|
|
8
8
|
config;
|
|
@@ -592,7 +592,7 @@ function registerGatewayMethods(api) {
|
|
|
592
592
|
});
|
|
593
593
|
const results = {};
|
|
594
594
|
if (cardInstanceId) try {
|
|
595
|
-
const { getAccessToken } = await import("./utils-legacy-
|
|
595
|
+
const { getAccessToken } = await import("./utils-legacy-vhmn24_Z.mjs");
|
|
596
596
|
const token = await getAccessToken(account.config);
|
|
597
597
|
await finishAICard({
|
|
598
598
|
cardInstanceId: String(cardInstanceId),
|
|
@@ -752,7 +752,7 @@ function registerGatewayMethods(api) {
|
|
|
752
752
|
try {
|
|
753
753
|
const account = resolveDingtalkAccount({ cfg });
|
|
754
754
|
if (!account.config?.clientId || !account.config?.clientSecret) return respond(false, { error: "Not configured" });
|
|
755
|
-
const { getAccessToken } = await import("./utils-legacy-
|
|
755
|
+
const { getAccessToken } = await import("./utils-legacy-vhmn24_Z.mjs");
|
|
756
756
|
await getAccessToken(account.config);
|
|
757
757
|
respond(true, {
|
|
758
758
|
ok: true,
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as dingtalkPlugin, r as initDingtalkPluginConfigSchema } from "./channel-
|
|
1
|
+
import { n as dingtalkPlugin, r as initDingtalkPluginConfigSchema } from "./channel-BbpJFyga.mjs";
|
|
2
2
|
import { n as setDingtalkRuntime } from "./runtime-DDMzayvA.mjs";
|
|
3
|
-
import { t as registerGatewayMethods } from "./gateway-methods-
|
|
3
|
+
import { t as registerGatewayMethods } from "./gateway-methods-lTdxcCpO.mjs";
|
|
4
4
|
//#region index.ts
|
|
5
5
|
/**
|
|
6
6
|
* 检测同一 plugin id 在多个路径被加载的情况。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
|
|
2
2
|
import { n as dingtalkOapiHttp, t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
|
-
import { n as DINGTALK_OAPI } from "./utils-
|
|
3
|
+
import { n as DINGTALK_OAPI } from "./utils-DXwaa6O-.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as os from "os";
|
|
@@ -667,7 +667,7 @@ async function extractAudioDuration(filePath, log) {
|
|
|
667
667
|
*/
|
|
668
668
|
async function sendVideoMessage(config, sessionWebhook, fileName, mediaId, log, metadata) {
|
|
669
669
|
try {
|
|
670
|
-
const token = await (await import("./utils-
|
|
670
|
+
const token = await (await import("./utils-CNXrWqt1.mjs")).getAccessToken(config);
|
|
671
671
|
const videoMessage = {
|
|
672
672
|
msgtype: "video",
|
|
673
673
|
video: {
|
|
@@ -695,8 +695,8 @@ async function sendVideoMessage(config, sessionWebhook, fileName, mediaId, log,
|
|
|
695
695
|
*/
|
|
696
696
|
async function sendVideoProactive(config, target, videoMediaId, picMediaId, metadata, log) {
|
|
697
697
|
try {
|
|
698
|
-
const token = await (await import("./utils-
|
|
699
|
-
const { DINGTALK_API } = await import("./utils-
|
|
698
|
+
const token = await (await import("./utils-CNXrWqt1.mjs")).getAccessToken(config);
|
|
699
|
+
const { DINGTALK_API } = await import("./utils-CNXrWqt1.mjs");
|
|
700
700
|
const msgParam = {
|
|
701
701
|
duration: metadata?.duration.toString() || "60000",
|
|
702
702
|
videoMediaId,
|
|
@@ -741,8 +741,8 @@ async function sendVideoProactive(config, target, videoMediaId, picMediaId, meta
|
|
|
741
741
|
*/
|
|
742
742
|
async function sendAudioProactive(config, target, fileName, mediaId, log, durationMs) {
|
|
743
743
|
try {
|
|
744
|
-
const token = await (await import("./utils-
|
|
745
|
-
const { DINGTALK_API } = await import("./utils-
|
|
744
|
+
const token = await (await import("./utils-CNXrWqt1.mjs")).getAccessToken(config);
|
|
745
|
+
const { DINGTALK_API } = await import("./utils-CNXrWqt1.mjs");
|
|
746
746
|
const msgParam = {
|
|
747
747
|
mediaId,
|
|
748
748
|
duration: durationMs && durationMs > 0 ? durationMs.toString() : "60000"
|
|
@@ -779,8 +779,8 @@ async function sendAudioProactive(config, target, fileName, mediaId, log, durati
|
|
|
779
779
|
*/
|
|
780
780
|
async function sendFileProactive(config, target, fileInfo, mediaId, log) {
|
|
781
781
|
try {
|
|
782
|
-
const token = await (await import("./utils-
|
|
783
|
-
const { DINGTALK_API } = await import("./utils-
|
|
782
|
+
const token = await (await import("./utils-CNXrWqt1.mjs")).getAccessToken(config);
|
|
783
|
+
const { DINGTALK_API } = await import("./utils-CNXrWqt1.mjs");
|
|
784
784
|
const resolvedFileName = fileInfo.fileName || path.basename(fileInfo.path);
|
|
785
785
|
const msgParam = {
|
|
786
786
|
mediaId,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-
|
|
1
|
+
import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-3G_kHjld.mjs";
|
|
2
2
|
export { hasResidualLocalMdImagesOutsideCode, processLocalImages, processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
|
|
2
|
-
import { t as CHANNEL_ID } from "./channel-
|
|
2
|
+
import { t as CHANNEL_ID } from "./channel-BbpJFyga.mjs";
|
|
3
3
|
import { n as createLoggerFromConfig, r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
|
|
4
4
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
5
|
-
import { i as getOapiAccessToken } from "./utils-
|
|
6
|
-
import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-
|
|
7
|
-
import {
|
|
5
|
+
import { i as getOapiAccessToken } from "./utils-DXwaa6O-.mjs";
|
|
6
|
+
import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-DvW-2HJJ.mjs";
|
|
7
|
+
import { n as isAbortCommand, o as QUEUE_BUSY_ACK_PHRASES, r as normalizeSlashCommand, t as buildSessionContext } from "./session-DsDDfhMw.mjs";
|
|
8
8
|
import { t as getDingtalkRuntime } from "./runtime-DDMzayvA.mjs";
|
|
9
|
-
import { c as formatSenderRoles, f as getUserProfile, h as recallEmotionReply, l as getAccessToken, o as formatHiredDateShanghai, r as addEmotionReply, s as formatSenderDisplayLabel, t as DINGTALK_API, u as getOapiAccessToken$1 } from "./utils-legacy-
|
|
9
|
+
import { c as formatSenderRoles, f as getUserProfile, h as recallEmotionReply, l as getAccessToken, o as formatHiredDateShanghai, r as addEmotionReply, s as formatSenderDisplayLabel, t as DINGTALK_API, u as getOapiAccessToken$1 } from "./utils-legacy-D9TCnrMR.mjs";
|
|
10
10
|
import { r as trackBackgroundWork } from "./background-work-874-aK4-.mjs";
|
|
11
11
|
import "./chunk-upload-z185bTqH.mjs";
|
|
12
12
|
import { a as toLocalPath, i as VIDEO_MARKER_PATTERN, o as uploadMediaToDingTalk, r as FILE_MARKER_PATTERN, t as AUDIO_MARKER_PATTERN } from "./common-1-GDZ2h3.mjs";
|
|
@@ -64,7 +64,7 @@ function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
|
64
64
|
* 直接复用 media.ts 权威实现,避免 reply 路径与 message 路径两套逻辑分叉。
|
|
65
65
|
*/
|
|
66
66
|
async function processLocalImages(content, oapiToken, log) {
|
|
67
|
-
const { processLocalImages: impl } = await import("./media-
|
|
67
|
+
const { processLocalImages: impl } = await import("./media-o_kBcTXb.mjs");
|
|
68
68
|
return impl(content, oapiToken, log);
|
|
69
69
|
}
|
|
70
70
|
//#endregion
|
|
@@ -225,6 +225,7 @@ const MODEL_ERROR_RULES = [
|
|
|
225
225
|
[/(?:reached|hit|breached).{0,48}(?:usage|token|tokens|rate|quota|credit).{0,16}limit|(?:usage|token|tokens|rate|quota|credit).{0,16}limit.{0,16}(?:reached|hit|exceeded|exhausted)/i, "⚠️ 模型 token 用量上限或限流,请充钱加模型或等待模型用量重置"],
|
|
226
226
|
[/\bcontext overflow\b|\bprompt too large\b|\bcontext length exceeded\b|\bmaximum context length\b|\btoo many tokens per request\b|\brequest_too_large\b|上下文过长/i, "⚠️ 上下文过长,超出模型处理限制,请发送 /clear 清空对话后重试"],
|
|
227
227
|
[/\bmodel_not_found\b|\bselected model was not found\b|\binvalid model\b|\bmodel\b.{0,24}\bnot found\b/i, "⚠️ 指定的模型不存在,请检查模型 ID 是否正确"],
|
|
228
|
+
[/\b(?:aborted|abort|cancelled|canceled|user_abort)\b|The operation was aborted|Dispatch reply operation aborted|Agent was aborted/i, "🛑 任务已中止"],
|
|
228
229
|
[/\boverloaded(?:_error)?\b|\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b|\bhigh (?:demand|load)\b|服务过载|当前负载过高|访问量过大/i, "⚠️ 当前模型负载过高,请切换其他模型或稍后重试"],
|
|
229
230
|
[/\brejected the request schema\b|\binvalid request format\b|\bunknown tool\b|\btool[_ ]?use[_ ]?id\b|\btool_use\.id\b|\btool\b.{0,40}(?:not found|is not available)|\bdoes not support assistant message prefill\b|\bconversation must end with a user message\b/i, "⚠️ 模型请求被拒绝,可能是工具参数格式不正确或会话被工具历史污染,可尝试更换模型或发送 /clear 清空对话后重试"],
|
|
230
231
|
[/\binvalid[_ ]?api[_ ]?key\b|\bincorrect api key\b|\bapi[_ ]?key[_ ]?(?:revoked|deactivated|deleted)\b|\bauthentication failed\b|\bunauthorized\b|\bHTTP\s*401\b|\bpermission_error\b|\baccess denied\b|\bforbidden\b|\bHTTP\s*403\b|\boauth token refresh failed\b|\btoken has expired\b|\bno (?:credentials|api key) found\b|无权访问|认证失败|鉴权失败|密钥无效/i, "⚠️ 认证失败,请检查模型密钥是否正确或已过期"],
|
|
@@ -614,13 +615,13 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
614
615
|
finalText = await processLocalImages(finalText, oapiToken, log);
|
|
615
616
|
if (beforeImg !== finalText) log.info(`[DingTalk][LocalImage] closeStreaming 图片已处理 | ${beforeImg.length}→${finalText.length}`);
|
|
616
617
|
try {
|
|
617
|
-
const { hasResidualLocalMdImagesOutsideCode } = await import("./media-
|
|
618
|
+
const { hasResidualLocalMdImagesOutsideCode } = await import("./media-o_kBcTXb.mjs");
|
|
618
619
|
if (hasResidualLocalMdImagesOutsideCode(finalText)) log.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
|
|
619
620
|
} catch {}
|
|
620
621
|
finalText = await processVideoMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
621
622
|
finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
622
623
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
623
|
-
const { processRawMediaPaths } = await import("./media-
|
|
624
|
+
const { processRawMediaPaths } = await import("./media-o_kBcTXb.mjs");
|
|
624
625
|
finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
|
|
625
626
|
log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
|
|
626
627
|
} else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
|
|
@@ -849,7 +850,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
849
850
|
const oapiToken = await getOapiAccessToken(account.config);
|
|
850
851
|
if (oapiToken) {
|
|
851
852
|
log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
|
|
852
|
-
const { processRawMediaPaths } = await import("./media-
|
|
853
|
+
const { processRawMediaPaths } = await import("./media-o_kBcTXb.mjs");
|
|
853
854
|
text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
|
|
854
855
|
log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
|
|
855
856
|
}
|
|
@@ -1036,7 +1037,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
1036
1037
|
payload.output,
|
|
1037
1038
|
commandText
|
|
1038
1039
|
].filter(Boolean).join("\n") || "";
|
|
1039
|
-
if (/\bdws\b/i.test(fullCmd)) import("./dws-delivery-context-
|
|
1040
|
+
if (/\bdws\b/i.test(fullCmd)) import("./dws-delivery-context-BsTrxK68.mjs").then(({ maybeInjectDwsOutboundContext }) => maybeInjectDwsOutboundContext({
|
|
1040
1041
|
cfg,
|
|
1041
1042
|
accountConfig: account.config,
|
|
1042
1043
|
agentId,
|
|
@@ -1077,6 +1078,7 @@ const SESSION_QUEUE_TASK_TIMEOUT_MS = 180 * 1e3;
|
|
|
1077
1078
|
* 这样不同 agent 可以并发处理,同一 agent 的同一会话串行处理
|
|
1078
1079
|
*/
|
|
1079
1080
|
const sessionQueues = /* @__PURE__ */ new Map();
|
|
1081
|
+
const sessionQueueEpoch = /* @__PURE__ */ new Map();
|
|
1080
1082
|
/**
|
|
1081
1083
|
* 清理过期的会话队列(超过5分钟没有新消息的会话+agent)
|
|
1082
1084
|
*/
|
|
@@ -1086,6 +1088,7 @@ function cleanupExpiredSessionQueues() {
|
|
|
1086
1088
|
const now = Date.now();
|
|
1087
1089
|
for (const [queueKey, lastActivity] of sessionLastActivity.entries()) if (now - lastActivity > SESSION_QUEUE_TTL) {
|
|
1088
1090
|
sessionQueues.delete(queueKey);
|
|
1091
|
+
sessionQueueEpoch.delete(queueKey);
|
|
1089
1092
|
sessionLastActivity.delete(queueKey);
|
|
1090
1093
|
}
|
|
1091
1094
|
}
|
|
@@ -2189,7 +2192,7 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
2189
2192
|
userContent = userContent ? `${userContent}\n\n${fileText}` : fileText;
|
|
2190
2193
|
}
|
|
2191
2194
|
if (!userContent && imageLocalPaths.length === 0) return;
|
|
2192
|
-
if (!params.emotionAlreadyAdded) addEmotionReply(config, data, log).catch((err) => {
|
|
2195
|
+
if (!params.emotionAlreadyAdded && !isAbortCommand(rawText)) addEmotionReply(config, data, log).catch((err) => {
|
|
2193
2196
|
log?.warn?.(`贴表情失败: ${err.message}`);
|
|
2194
2197
|
});
|
|
2195
2198
|
const asyncMode = config.asyncMode === true;
|
|
@@ -2276,6 +2279,7 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
2276
2279
|
});
|
|
2277
2280
|
const groupReplyModeForCard = config.groupReplyMode || "aicard";
|
|
2278
2281
|
const streamingEnabledForCard = !(!isDirect && (groupReplyModeForCard === "text" || groupReplyModeForCard === "markdown")) && config.streaming !== false;
|
|
2282
|
+
const isAbort = isAbortCommand(rawText);
|
|
2279
2283
|
if (!earlyCard && streamingEnabledForCard && !asyncMode) try {
|
|
2280
2284
|
earlyCard = await createAICardForTarget(config, isDirect ? {
|
|
2281
2285
|
type: "user",
|
|
@@ -2285,9 +2289,9 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
2285
2289
|
openConversationId: data.conversationId
|
|
2286
2290
|
}, log);
|
|
2287
2291
|
if (earlyCard) {
|
|
2288
|
-
await streamAICard(earlyCard, "🦸 正在召唤大模型…", false, config, log);
|
|
2292
|
+
await streamAICard(earlyCard, isAbort ? "🛑 正在停止当前任务…" : "🦸 正在召唤大模型…", false, config, log);
|
|
2289
2293
|
if (!isDirect) registerActiveCard(data.conversationId, earlyCard);
|
|
2290
|
-
log.info(
|
|
2294
|
+
log.info(`[DingTalk][earlyCard] ✅ 即时建卡成功 (${isAbort ? "中止任务" : "召唤模型"})`);
|
|
2291
2295
|
}
|
|
2292
2296
|
} catch (e) {
|
|
2293
2297
|
log.warn(`[DingTalk][earlyCard] 即时建卡失败(降级延迟建卡):${e?.message}`);
|
|
@@ -2352,7 +2356,7 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
2352
2356
|
finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2353
2357
|
finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2354
2358
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2355
|
-
const { processRawMediaPaths } = await import("./media-
|
|
2359
|
+
const { processRawMediaPaths } = await import("./media-o_kBcTXb.mjs");
|
|
2356
2360
|
finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
|
|
2357
2361
|
}
|
|
2358
2362
|
const textToSend = finalText.trim() || "✅ 任务执行完成(无文本输出)";
|
|
@@ -2459,15 +2463,54 @@ async function handleDingTalkMessage(params) {
|
|
|
2459
2463
|
}
|
|
2460
2464
|
if (!matchedAgentId) matchedAgentId = cfg.defaultAgent || "main";
|
|
2461
2465
|
const queueKey = `${baseSessionId}:${matchedAgentId}`;
|
|
2466
|
+
const rawText = extractMessageContent(data).text || "";
|
|
2467
|
+
if (isAbortCommand(rawText)) {
|
|
2468
|
+
log?.info?.(`[队列][Stop] 收到中止命令,穿透队列直接执行: queueKey=${queueKey}, rawText=${rawText}`);
|
|
2469
|
+
sessionQueueEpoch.set(queueKey, (sessionQueueEpoch.get(queueKey) || 0) + 1);
|
|
2470
|
+
sessionQueues.delete(queueKey);
|
|
2471
|
+
try {
|
|
2472
|
+
const core = getDingtalkRuntime();
|
|
2473
|
+
const dmScope = cfg.session?.dmScope || "per-channel-peer";
|
|
2474
|
+
const abortSessionKey = core.channel.routing.buildAgentSessionKey({
|
|
2475
|
+
agentId: matchedAgentId,
|
|
2476
|
+
channel: "dingtalk-connector",
|
|
2477
|
+
accountId,
|
|
2478
|
+
peer: {
|
|
2479
|
+
kind: queueSessionContext.chatType,
|
|
2480
|
+
id: queueSessionContext.sessionPeerId
|
|
2481
|
+
},
|
|
2482
|
+
dmScope
|
|
2483
|
+
});
|
|
2484
|
+
if (core.gateway?.request) core.gateway.request("chat.abort", {
|
|
2485
|
+
sessionKey: abortSessionKey,
|
|
2486
|
+
agentId: matchedAgentId
|
|
2487
|
+
}).then((res) => {
|
|
2488
|
+
log?.info?.(`[队列][Stop] Gateway chat.abort 成功: ${JSON.stringify(res)}`);
|
|
2489
|
+
}).catch((err) => {
|
|
2490
|
+
log?.warn?.(`[队列][Stop] Gateway chat.abort 异常: ${err?.message || err}`);
|
|
2491
|
+
});
|
|
2492
|
+
} catch (abortErr) {
|
|
2493
|
+
log?.warn?.(`[队列][Stop] 快速触发 Gateway abort 失败: ${abortErr?.message || abortErr}`);
|
|
2494
|
+
}
|
|
2495
|
+
trackBackgroundWork(handleDingTalkMessageInternal(params).catch((err) => {
|
|
2496
|
+
log?.error?.(`[队列][Stop] 终止消息处理异常: ${err?.message || err}`);
|
|
2497
|
+
}), `stop-${queueKey}`);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2462
2500
|
try {
|
|
2463
2501
|
sessionLastActivity.set(queueKey, Date.now());
|
|
2464
2502
|
const previousTask = sessionQueues.get(queueKey) || Promise.resolve();
|
|
2465
2503
|
const isQueueBusy = sessionQueues.has(queueKey);
|
|
2504
|
+
const myEpoch = sessionQueueEpoch.get(queueKey) || 0;
|
|
2466
2505
|
const cardHolder = {
|
|
2467
2506
|
card: void 0,
|
|
2468
2507
|
ackDone: !isQueueBusy
|
|
2469
2508
|
};
|
|
2470
2509
|
const currentTask = previousTask.then(async () => {
|
|
2510
|
+
if ((sessionQueueEpoch.get(queueKey) || 0) !== myEpoch) {
|
|
2511
|
+
log?.info?.(`[队列] 任务已被后续 /stop 命令取消,跳过执行: queueKey=${queueKey}`);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2471
2514
|
log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`);
|
|
2472
2515
|
if (isQueueBusy && !cardHolder.ackDone) {
|
|
2473
2516
|
const waitStart = Date.now();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-
|
|
1
|
+
import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-3G_kHjld.mjs";
|
|
2
2
|
import { n as createLoggerFromConfig, r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
|
|
3
3
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
4
|
-
import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-
|
|
5
|
-
import {
|
|
4
|
+
import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-DXwaa6O-.mjs";
|
|
5
|
+
import { i as MEDIA_MSG_TYPES } from "./session-DsDDfhMw.mjs";
|
|
6
6
|
//#region src/services/reply-markers.ts
|
|
7
7
|
const PROCESS_TAG = "[-process-]";
|
|
8
8
|
const FINAL_TAG = "[-final-]";
|
|
@@ -876,7 +876,7 @@ async function resolveMediaSourceToLocalFile(mediaUrl, mediaLocalRoots, log) {
|
|
|
876
876
|
cleanup: tmp
|
|
877
877
|
};
|
|
878
878
|
}
|
|
879
|
-
const { toLocalPath } = await import("./media-
|
|
879
|
+
const { toLocalPath } = await import("./media-o_kBcTXb.mjs");
|
|
880
880
|
const _fs = await import("fs");
|
|
881
881
|
const _path = await import("path");
|
|
882
882
|
let resolved = toLocalPath(mediaUrl);
|
|
@@ -1143,7 +1143,7 @@ async function sendMediaToDingTalk(params) {
|
|
|
1143
1143
|
const fileName = mediaUrl.split("/").filter(Boolean).pop()?.split("?")[0] || "file";
|
|
1144
1144
|
if (mediaType === "video") {
|
|
1145
1145
|
const videoMarker = `[DINGTALK_VIDEO]{"path":"${resolvedMediaUrl}"}[/DINGTALK_VIDEO]`;
|
|
1146
|
-
const { processVideoMarkers } = await import("./media-
|
|
1146
|
+
const { processVideoMarkers } = await import("./media-o_kBcTXb.mjs");
|
|
1147
1147
|
await processVideoMarkers(videoMarker, "", config, oapiToken, console, true, targetParam);
|
|
1148
1148
|
return {
|
|
1149
1149
|
ok: true,
|
|
@@ -1162,7 +1162,7 @@ async function sendMediaToDingTalk(params) {
|
|
|
1162
1162
|
fileName,
|
|
1163
1163
|
fileType: ext && ext.length < 12 ? ext : "file"
|
|
1164
1164
|
};
|
|
1165
|
-
const { sendFileProactive } = await import("./media-
|
|
1165
|
+
const { sendFileProactive } = await import("./media-o_kBcTXb.mjs");
|
|
1166
1166
|
await sendFileProactive(config, targetParam, fileInfo, uploadResult.mediaId, log);
|
|
1167
1167
|
return {
|
|
1168
1168
|
ok: true,
|
|
@@ -11,6 +11,20 @@ const NEW_SESSION_COMMANDS = [
|
|
|
11
11
|
"重新开始",
|
|
12
12
|
"清空对话"
|
|
13
13
|
];
|
|
14
|
+
/** 中止/停止触发命令 */
|
|
15
|
+
const STOP_COMMANDS = [
|
|
16
|
+
"/stop",
|
|
17
|
+
"/abort",
|
|
18
|
+
"stop",
|
|
19
|
+
"abort",
|
|
20
|
+
"停止",
|
|
21
|
+
"停下来",
|
|
22
|
+
"暂停",
|
|
23
|
+
"取消",
|
|
24
|
+
"别说了",
|
|
25
|
+
"闭嘴",
|
|
26
|
+
"halt"
|
|
27
|
+
];
|
|
14
28
|
/**
|
|
15
29
|
* 媒体类消息类型集合。
|
|
16
30
|
*
|
|
@@ -103,12 +117,44 @@ function buildSessionContext(params) {
|
|
|
103
117
|
};
|
|
104
118
|
}
|
|
105
119
|
/**
|
|
106
|
-
*
|
|
120
|
+
* 尝试动态加载 SDK 中的 isAbortRequestText
|
|
121
|
+
*/
|
|
122
|
+
let sdkIsAbortRequestText = null;
|
|
123
|
+
try {
|
|
124
|
+
const mod = await import("openclaw/plugin-sdk/command-primitives-runtime");
|
|
125
|
+
if (typeof mod.isAbortRequestText === "function") sdkIsAbortRequestText = mod.isAbortRequestText;
|
|
126
|
+
} catch {}
|
|
127
|
+
/**
|
|
128
|
+
* 检查消息是否是中止/停止任务命令
|
|
129
|
+
* 支持 /stop, stop, 停止, 暂停 等,以及群聊中带 @机器人 /stop 的情况
|
|
130
|
+
*/
|
|
131
|
+
function isAbortCommand(text) {
|
|
132
|
+
if (!text) return false;
|
|
133
|
+
const trimmed = text.trim();
|
|
134
|
+
if (!trimmed) return false;
|
|
135
|
+
try {
|
|
136
|
+
if (sdkIsAbortRequestText?.(trimmed)) return true;
|
|
137
|
+
} catch {}
|
|
138
|
+
const stripped = trimmed.replace(/@[^\s]+/g, "").trim();
|
|
139
|
+
if (stripped) {
|
|
140
|
+
try {
|
|
141
|
+
if (sdkIsAbortRequestText?.(stripped)) return true;
|
|
142
|
+
} catch {}
|
|
143
|
+
const lower = stripped.toLowerCase().replace(/^[!/]/, "");
|
|
144
|
+
if (STOP_COMMANDS.some((cmd) => cmd.toLowerCase().replace(/^[!/]/, "") === lower)) return true;
|
|
145
|
+
if (STOP_COMMANDS.some((cmd) => stripped === cmd)) return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 归一化斜杠命令
|
|
107
151
|
*/
|
|
108
152
|
function normalizeSlashCommand(text) {
|
|
109
|
-
const
|
|
153
|
+
const trimmed = text.trim();
|
|
154
|
+
if (isAbortCommand(trimmed)) return "/stop";
|
|
155
|
+
const lower = trimmed.toLowerCase();
|
|
110
156
|
if (NEW_SESSION_COMMANDS.some((cmd) => lower === cmd.toLowerCase())) return "/new";
|
|
111
157
|
return text;
|
|
112
158
|
}
|
|
113
159
|
//#endregion
|
|
114
|
-
export {
|
|
160
|
+
export { NEW_SESSION_COMMANDS as a, MEDIA_MSG_TYPES as i, isAbortCommand as n, QUEUE_BUSY_ACK_PHRASES as o, normalizeSlashCommand as r, STOP_COMMANDS as s, buildSessionContext as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./logger-CnBTcwyq.mjs";
|
|
2
|
-
import { r as getAccessToken, t as DINGTALK_API } from "./utils-
|
|
3
|
-
import "./session-
|
|
2
|
+
import { r as getAccessToken, t as DINGTALK_API } from "./utils-DXwaa6O-.mjs";
|
|
3
|
+
import "./session-DsDDfhMw.mjs";
|
|
4
4
|
export { DINGTALK_API, getAccessToken };
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
} from "../services/messaging/card-content-cache.ts";
|
|
64
64
|
import { QUEUE_BUSY_ACK_PHRASES } from "../utils/constants.ts";
|
|
65
65
|
import { createDingtalkReplyDispatcher, matchModelErrorText } from "../reply-dispatcher.ts";
|
|
66
|
-
import { normalizeSlashCommand } from "../utils/session.ts";
|
|
66
|
+
import { normalizeSlashCommand, isAbortCommand } from "../utils/session.ts";
|
|
67
67
|
import { getDingtalkRuntime } from "../runtime.ts";
|
|
68
68
|
import { dingtalkHttp } from '../utils/http-client.ts';
|
|
69
69
|
import { createLoggerFromConfig, isDingtalkDebug } from '../utils/index.ts';
|
|
@@ -100,6 +100,7 @@ const AICardStatus = {
|
|
|
100
100
|
* 这样不同 agent 可以并发处理,同一 agent 的同一会话串行处理
|
|
101
101
|
*/
|
|
102
102
|
const sessionQueues = new Map<string, Promise<void>>();
|
|
103
|
+
const sessionQueueEpoch = new Map<string, number>();
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
106
|
* 清理过期的会话队列(超过5分钟没有新消息的会话+agent)
|
|
@@ -112,6 +113,7 @@ function cleanupExpiredSessionQueues(): void {
|
|
|
112
113
|
for (const [queueKey, lastActivity] of sessionLastActivity.entries()) {
|
|
113
114
|
if (now - lastActivity > SESSION_QUEUE_TTL) {
|
|
114
115
|
sessionQueues.delete(queueKey);
|
|
116
|
+
sessionQueueEpoch.delete(queueKey);
|
|
115
117
|
sessionLastActivity.delete(queueKey);
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -1630,8 +1632,8 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1630
1632
|
if (!userContent && imageLocalPaths.length === 0) return;
|
|
1631
1633
|
|
|
1632
1634
|
// ===== 贴处理中表情 =====
|
|
1633
|
-
//
|
|
1634
|
-
if (!params.emotionAlreadyAdded) {
|
|
1635
|
+
// 若队列繁忙时已在入队阶段提前贴过表情,或为终止命令,此处跳过
|
|
1636
|
+
if (!params.emotionAlreadyAdded && !isAbortCommand(rawText)) {
|
|
1635
1637
|
addEmotionReply(config, data, log).catch(err => {
|
|
1636
1638
|
log?.warn?.(`贴表情失败: ${err.message}`);
|
|
1637
1639
|
});
|
|
@@ -1756,6 +1758,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1756
1758
|
const skipAICardForCard = !isDirect && (groupReplyModeForCard === 'text' || groupReplyModeForCard === 'markdown');
|
|
1757
1759
|
const streamingEnabledForCard = !skipAICardForCard && (config as any).streaming !== false;
|
|
1758
1760
|
|
|
1761
|
+
const isAbort = isAbortCommand(rawText);
|
|
1759
1762
|
if (!earlyCard && streamingEnabledForCard && !asyncMode) {
|
|
1760
1763
|
try {
|
|
1761
1764
|
const earlyTarget: AICardTarget = isDirect
|
|
@@ -1763,11 +1766,12 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1763
1766
|
: { type: 'group', openConversationId: data.conversationId };
|
|
1764
1767
|
earlyCard = await createAICardForTarget(config, earlyTarget, log);
|
|
1765
1768
|
if (earlyCard) {
|
|
1766
|
-
|
|
1769
|
+
const earlyLoadingText = isAbort ? '🛑 正在停止当前任务…' : '🦸 正在召唤大模型…';
|
|
1770
|
+
await streamAICard(earlyCard, earlyLoadingText, false, config, log);
|
|
1767
1771
|
if (!isDirect) {
|
|
1768
1772
|
registerActiveCard(data.conversationId, earlyCard);
|
|
1769
1773
|
}
|
|
1770
|
-
log.info(
|
|
1774
|
+
log.info(`[DingTalk][earlyCard] ✅ 即时建卡成功 (${isAbort ? '中止任务' : '召唤模型'})`);
|
|
1771
1775
|
}
|
|
1772
1776
|
} catch (e: any) {
|
|
1773
1777
|
log.warn(`[DingTalk][earlyCard] 即时建卡失败(降级延迟建卡):${e?.message}`);
|
|
@@ -2057,6 +2061,55 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
2057
2061
|
// - sharedMemoryAcrossConversations: true 时,所有消息共享同一队列
|
|
2058
2062
|
const queueKey = `${baseSessionId}:${matchedAgentId}`;
|
|
2059
2063
|
|
|
2064
|
+
// 提前提取文本,判断是否为中止命令(/stop、停止、@机器人 /stop 等)
|
|
2065
|
+
const content = extractMessageContent(data);
|
|
2066
|
+
const rawText = content.text || '';
|
|
2067
|
+
const isAbort = isAbortCommand(rawText);
|
|
2068
|
+
|
|
2069
|
+
// ★ 优先级穿透:若是终止命令,绝对不入队排队,直接穿透到最顶层立即执行
|
|
2070
|
+
if (isAbort) {
|
|
2071
|
+
log?.info?.(`[队列][Stop] 收到中止命令,穿透队列直接执行: queueKey=${queueKey}, rawText=${rawText}`);
|
|
2072
|
+
|
|
2073
|
+
// 1. 废弃当前队列中可能积压排队的后续任务,防止当前任务停掉后排队旧消息又被唤醒
|
|
2074
|
+
sessionQueueEpoch.set(queueKey, (sessionQueueEpoch.get(queueKey) || 0) + 1);
|
|
2075
|
+
sessionQueues.delete(queueKey);
|
|
2076
|
+
|
|
2077
|
+
// 2. 毫秒级直接调用 Gateway chat.abort,以最快速度停止当前正在运行的底层任务与进程(对齐 WebUI Stop)
|
|
2078
|
+
try {
|
|
2079
|
+
const core = getDingtalkRuntime();
|
|
2080
|
+
const dmScope = cfg.session?.dmScope || 'per-channel-peer';
|
|
2081
|
+
const abortSessionKey = core.channel.routing.buildAgentSessionKey({
|
|
2082
|
+
agentId: matchedAgentId,
|
|
2083
|
+
channel: 'dingtalk-connector',
|
|
2084
|
+
accountId: accountId,
|
|
2085
|
+
peer: {
|
|
2086
|
+
kind: queueSessionContext.chatType,
|
|
2087
|
+
id: queueSessionContext.sessionPeerId,
|
|
2088
|
+
},
|
|
2089
|
+
dmScope: dmScope,
|
|
2090
|
+
});
|
|
2091
|
+
if (core.gateway?.request) {
|
|
2092
|
+
core.gateway.request("chat.abort", {
|
|
2093
|
+
sessionKey: abortSessionKey,
|
|
2094
|
+
agentId: matchedAgentId,
|
|
2095
|
+
}).then((res: any) => {
|
|
2096
|
+
log?.info?.(`[队列][Stop] Gateway chat.abort 成功: ${JSON.stringify(res)}`);
|
|
2097
|
+
}).catch((err: any) => {
|
|
2098
|
+
log?.warn?.(`[队列][Stop] Gateway chat.abort 异常: ${err?.message || err}`);
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
} catch (abortErr: any) {
|
|
2102
|
+
log?.warn?.(`[队列][Stop] 快速触发 Gateway abort 失败: ${abortErr?.message || abortErr}`);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// 3. 穿透执行 handleDingTalkMessageInternal,走 dispatchReplyFromConfig 的 fastAbort 分支返回停止回文
|
|
2106
|
+
const stopTask = handleDingTalkMessageInternal(params).catch((err: any) => {
|
|
2107
|
+
log?.error?.(`[队列][Stop] 终止消息处理异常: ${err?.message || err}`);
|
|
2108
|
+
});
|
|
2109
|
+
trackBackgroundWork(stopTask, `stop-${queueKey}`);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2060
2113
|
try {
|
|
2061
2114
|
|
|
2062
2115
|
// 更新会话活跃时间
|
|
@@ -2071,6 +2124,7 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
2071
2124
|
// ═══════════════════════════════════════════════════════════
|
|
2072
2125
|
const previousTask = sessionQueues.get(queueKey) || Promise.resolve();
|
|
2073
2126
|
const isQueueBusy = sessionQueues.has(queueKey);
|
|
2127
|
+
const myEpoch = sessionQueueEpoch.get(queueKey) || 0;
|
|
2074
2128
|
|
|
2075
2129
|
// 异步 ACK 写回的卡片:主任务 dequeue 时再读,避免 await ACK 再入队
|
|
2076
2130
|
const cardHolder: { card?: AICardInstance; ackDone: boolean } = {
|
|
@@ -2080,6 +2134,14 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
2080
2134
|
|
|
2081
2135
|
const currentTask = previousTask
|
|
2082
2136
|
.then(async () => {
|
|
2137
|
+
// 检查本任务排队期间队列是否已被 /stop 作废
|
|
2138
|
+
if ((sessionQueueEpoch.get(queueKey) || 0) !== myEpoch) {
|
|
2139
|
+
log?.info?.(
|
|
2140
|
+
`[队列] 任务已被后续 /stop 命令取消,跳过执行: queueKey=${queueKey}`,
|
|
2141
|
+
);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2083
2145
|
log?.info?.(
|
|
2084
2146
|
`[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`,
|
|
2085
2147
|
);
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -200,6 +200,12 @@ const MODEL_ERROR_RULES: [RegExp, string][] = [
|
|
|
200
200
|
"⚠️ 指定的模型不存在,请检查模型 ID 是否正确",
|
|
201
201
|
],
|
|
202
202
|
|
|
203
|
+
// 用户主动中止(/stop、WebUI Stop、user_abort 等)
|
|
204
|
+
[
|
|
205
|
+
/\b(?:aborted|abort|cancelled|canceled|user_abort)\b|The operation was aborted|Dispatch reply operation aborted|Agent was aborted/i,
|
|
206
|
+
"🛑 任务已中止",
|
|
207
|
+
],
|
|
208
|
+
|
|
203
209
|
// overloaded(严格对齐 OpenClaw:不含 no available channel)
|
|
204
210
|
[
|
|
205
211
|
/\boverloaded(?:_error)?\b|\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b|\bhigh (?:demand|load)\b|服务过载|当前负载过高|访问量过大/i,
|
package/src/utils/constants.ts
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
/** 新会话触发命令 */
|
|
6
6
|
export const NEW_SESSION_COMMANDS = ['/new', '/reset', '/clear', '新会话', '重新开始', '清空对话'];
|
|
7
7
|
|
|
8
|
+
/** 中止/停止触发命令 */
|
|
9
|
+
export const STOP_COMMANDS = [
|
|
10
|
+
'/stop',
|
|
11
|
+
'/abort',
|
|
12
|
+
'stop',
|
|
13
|
+
'abort',
|
|
14
|
+
'停止',
|
|
15
|
+
'停下来',
|
|
16
|
+
'暂停',
|
|
17
|
+
'取消',
|
|
18
|
+
'别说了',
|
|
19
|
+
'闭嘴',
|
|
20
|
+
'halt',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
8
23
|
/**
|
|
9
24
|
* 媒体类消息类型集合。
|
|
10
25
|
*
|
package/src/utils/session.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 构建 OpenClaw 标准会话上下文
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { NEW_SESSION_COMMANDS } from './constants.ts';
|
|
6
|
+
import { NEW_SESSION_COMMANDS, STOP_COMMANDS } from './constants.ts';
|
|
7
7
|
|
|
8
8
|
/** OpenClaw 标准会话上下文 */
|
|
9
9
|
export interface SessionContext {
|
|
@@ -135,10 +135,59 @@ export function buildSessionContext(params: {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
138
|
+
* 尝试动态加载 SDK 中的 isAbortRequestText
|
|
139
|
+
*/
|
|
140
|
+
let sdkIsAbortRequestText: ((text?: string, options?: any) => boolean) | null = null;
|
|
141
|
+
try {
|
|
142
|
+
const mod = await import("openclaw/plugin-sdk/command-primitives-runtime") as any;
|
|
143
|
+
if (typeof mod.isAbortRequestText === "function") {
|
|
144
|
+
sdkIsAbortRequestText = mod.isAbortRequestText;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// 忽略加载异常,走内联 fallback
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 检查消息是否是中止/停止任务命令
|
|
152
|
+
* 支持 /stop, stop, 停止, 暂停 等,以及群聊中带 @机器人 /stop 的情况
|
|
153
|
+
*/
|
|
154
|
+
export function isAbortCommand(text?: string): boolean {
|
|
155
|
+
if (!text) return false;
|
|
156
|
+
const trimmed = text.trim();
|
|
157
|
+
if (!trimmed) return false;
|
|
158
|
+
|
|
159
|
+
// 1. SDK 原生判断
|
|
160
|
+
try {
|
|
161
|
+
if (sdkIsAbortRequestText?.(trimmed)) return true;
|
|
162
|
+
} catch {}
|
|
163
|
+
|
|
164
|
+
// 2. 去除 @ 提及前缀后判断(针对群聊中 @机器人 /stop 的情况)
|
|
165
|
+
const stripped = trimmed.replace(/@[^\s]+/g, "").trim();
|
|
166
|
+
if (stripped) {
|
|
167
|
+
try {
|
|
168
|
+
if (sdkIsAbortRequestText?.(stripped)) return true;
|
|
169
|
+
} catch {}
|
|
170
|
+
|
|
171
|
+
const lower = stripped.toLowerCase().replace(/^[!/]/, "");
|
|
172
|
+
if (STOP_COMMANDS.some((cmd) => cmd.toLowerCase().replace(/^[!/]/, "") === lower)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (STOP_COMMANDS.some((cmd) => stripped === cmd)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 归一化斜杠命令
|
|
139
185
|
*/
|
|
140
186
|
export function normalizeSlashCommand(text: string): string {
|
|
141
187
|
const trimmed = text.trim();
|
|
188
|
+
if (isAbortCommand(trimmed)) {
|
|
189
|
+
return '/stop';
|
|
190
|
+
}
|
|
142
191
|
const lower = trimmed.toLowerCase();
|
|
143
192
|
if (NEW_SESSION_COMMANDS.some((cmd) => lower === cmd.toLowerCase())) {
|
|
144
193
|
return '/new';
|