@jeik/dingtalk-connector 0.8.36 → 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 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-g8tJtpfU.mjs";
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-CIfI_3Jh.mjs";
6
- import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CwQLykl6.mjs";
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-0LEoMD85.mjs"),
974
- import("./connection-Bh3U9I4x.mjs")
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-CGEa8tg2.mjs";
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
@@ -1,4 +1,4 @@
1
- import { t as CHANNEL_ID } from "./channel-B56QFS9m.mjs";
1
+ import { t as CHANNEL_ID } from "./channel-BbpJFyga.mjs";
2
2
  //#region src/services/dws-delivery-context.ts
3
3
  /**
4
4
  * DWS 发消息成功后注入 outbound_message 上下文(对齐 OpenClaw message 工具 OC-4)。
@@ -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-CV8y0xGw.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-DBRRhEk5.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -0,0 +1,2 @@
1
+ import { t as registerGatewayMethods } from "./gateway-methods-lTdxcCpO.mjs";
2
+ export { registerGatewayMethods };
@@ -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-CIfI_3Jh.mjs";
4
- import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-CwQLykl6.mjs";
5
- import { d as getUnionId, h as recallEmotionReply } from "./utils-legacy-CGEa8tg2.mjs";
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-Bx5izlVF.mjs");
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-Bx5izlVF.mjs");
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-B56QFS9m.mjs";
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-Ce1FXmCx.mjs";
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-CIfI_3Jh.mjs";
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-Bwz2Fls_.mjs")).getAccessToken(config);
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-Bwz2Fls_.mjs")).getAccessToken(config);
699
- const { DINGTALK_API } = await import("./utils-Bwz2Fls_.mjs");
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-Bwz2Fls_.mjs")).getAccessToken(config);
745
- const { DINGTALK_API } = await import("./utils-Bwz2Fls_.mjs");
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-Bwz2Fls_.mjs")).getAccessToken(config);
783
- const { DINGTALK_API } = await import("./utils-Bwz2Fls_.mjs");
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-g8tJtpfU.mjs";
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-B56QFS9m.mjs";
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-CIfI_3Jh.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-CwQLykl6.mjs";
7
- import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
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-CGEa8tg2.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-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-DDtX37T2.mjs");
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-DDtX37T2.mjs");
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-DDtX37T2.mjs");
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-DDtX37T2.mjs");
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-ZJalcxYA.mjs").then(({ maybeInjectDwsOutboundContext }) => maybeInjectDwsOutboundContext({
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("[DingTalk][earlyCard] ✅ 即时建卡成功");
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-DDtX37T2.mjs");
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-g8tJtpfU.mjs";
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-CIfI_3Jh.mjs";
5
- import { r as MEDIA_MSG_TYPES } from "./session-DJ4jYqPv.mjs";
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-DDtX37T2.mjs");
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-DDtX37T2.mjs");
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-DDtX37T2.mjs");
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 lower = text.trim().toLowerCase();
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 { QUEUE_BUSY_ACK_PHRASES as a, NEW_SESSION_COMMANDS as i, normalizeSlashCommand as n, MEDIA_MSG_TYPES as r, buildSessionContext as t };
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-CIfI_3Jh.mjs";
3
- import "./session-DJ4jYqPv.mjs";
2
+ import { r as getAccessToken, t as DINGTALK_API } from "./utils-DXwaa6O-.mjs";
3
+ import "./session-DsDDfhMw.mjs";
4
4
  export { DINGTALK_API, getAccessToken };
@@ -1,4 +1,5 @@
1
1
  import { n as dingtalkOapiHttp, t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
2
+ import "./session-DsDDfhMw.mjs";
2
3
  //#region src/utils/token.ts
3
4
  const DINGTALK_API = "https://api.dingtalk.com";
4
5
  const DINGTALK_OAPI = "https://oapi.dingtalk.com";
@@ -1,3 +1,4 @@
1
+ import "./session-DsDDfhMw.mjs";
1
2
  //#region src/utils/utils-legacy.ts
2
3
  /** 钉钉 API 常量 */
3
4
  const DINGTALK_API = "https://api.dingtalk.com";
@@ -0,0 +1,3 @@
1
+ import "./session-DsDDfhMw.mjs";
2
+ import { l as getAccessToken } from "./utils-legacy-D9TCnrMR.mjs";
3
+ export { getAccessToken };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dingtalk-connector",
3
3
  "name": "DingTalk Channel",
4
- "version": "0.8.36",
4
+ "version": "0.8.38",
5
5
  "description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版",
6
6
  "author": "jeik",
7
7
  "main": "index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.36",
3
+ "version": "0.8.38",
4
4
  "description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版(基于官方 0.8.24,长连接与消息体验增强)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- await streamAICard(earlyCard, '🦸 正在召唤大模型…', false, config, log);
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('[DingTalk][earlyCard] ✅ 即时建卡成功');
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
  );
@@ -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,
@@ -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
  *
@@ -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';
@@ -1,2 +0,0 @@
1
- import { t as registerGatewayMethods } from "./gateway-methods-Ce1FXmCx.mjs";
2
- export { registerGatewayMethods };
@@ -1,3 +0,0 @@
1
- import "./session-DJ4jYqPv.mjs";
2
- import { l as getAccessToken } from "./utils-legacy-CGEa8tg2.mjs";
3
- export { getAccessToken };