@jeik/dingtalk-connector 0.8.21-fix2 → 0.8.21-fix4

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.
@@ -731,6 +731,12 @@ Options:
731
731
  globalThis['proc' + 'ess'].exit(1);
732
732
  }
733
733
 
734
+ // 打印当前安装向导版本(= 本包版本),方便确认装的是哪一版
735
+ try {
736
+ const { version } = createRequire(import.meta.url)('../package.json');
737
+ console.log('\n' + cyan(`${PKG_NAME} v${version}`) + dim(' (install wizard)'));
738
+ } catch {}
739
+
734
740
  // Step 1: Install connector plugin (unless --local)
735
741
  let pluginInstalled = true;
736
742
  if (!isLocal) {
@@ -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-Ci31A3vg.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-D7aACzCW.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -0,0 +1,2 @@
1
+ import { t as registerGatewayMethods } from "./gateway-methods-DM8lWI4F.mjs";
2
+ export { registerGatewayMethods };
@@ -1,7 +1,7 @@
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
3
  import { r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
4
- import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-B6l1sRvX.mjs";
4
+ import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-CBQl0D00.mjs";
5
5
  import { c as getUnionId, d as recallEmotionReply } from "./utils-legacy-CALCPP1t.mjs";
6
6
  //#region src/docs.ts
7
7
  var DingtalkDocsClient = class {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-DUgpo5zC.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods-C4tcgI7P.mjs";
1
+ import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-5-KOSMI9.mjs";
2
+ import { t as registerGatewayMethods } from "./gateway-methods-DM8lWI4F.mjs";
3
3
  //#region index.ts
4
4
  /**
5
5
  * 检测同一 plugin id 在多个路径被加载的情况。
@@ -1,10 +1,10 @@
1
1
  import { u as uploadMediaToDingTalk } from "./media-cz72EVS3.mjs";
2
2
  import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
3
- import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-DUgpo5zC.mjs";
3
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-5-KOSMI9.mjs";
4
4
  import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
5
5
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
6
6
  import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
7
- import { a as sendTextMessage, f as isQpsLimitError, h as unregisterActiveCard, i as sendProactive, l as createAICardForTarget, m as streamAICard, p as registerActiveCard, r as sendMessage, t as sendMarkdownMessage, u as finishAICard } from "./messaging-B6l1sRvX.mjs";
7
+ import { _ as PROCESS_TAG, a as sendTextMessage, b as finalClean, f as isQpsLimitError, g as FINAL_TAG, h as unregisterActiveCard, i as sendProactive, l as createAICardForTarget, m as streamAICard, p as registerActiveCard, r as sendMessage, t as sendMarkdownMessage, u as finishAICard, v as displayClean, y as extractFinal } from "./messaging-CBQl0D00.mjs";
8
8
  import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
9
9
  import { d as recallEmotionReply, o as getAccessToken, r as addEmotionReply, s as getOapiAccessToken$1, t as DINGTALK_API } from "./utils-legacy-CALCPP1t.mjs";
10
10
  import "./chunk-upload-BBQgGtcZ.mjs";
@@ -204,42 +204,6 @@ async function uploadAndReplaceFileMarkers(content, sessionWebhook, config, oapi
204
204
  return result;
205
205
  }
206
206
  //#endregion
207
- //#region src/services/reply-markers.ts
208
- const PROCESS_TAG = "[-process-]";
209
- const FINAL_TAG = "[-final-]";
210
- const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
211
- /** 取最终答案:去掉那一个 [-final-](开头/中间/结尾都行),其余原样,首尾空白清掉。
212
- * 没有 [-final-] 返回 null。多处出现取最后一个当信号。 */
213
- function extractFinal(text) {
214
- if (!text) return null;
215
- const i = text.lastIndexOf(FINAL_TAG);
216
- if (i < 0) return null;
217
- return (text.slice(0, i) + text.slice(i + 9)).replace(EDGE_WS, "");
218
- }
219
- /** 剥掉一个标记 token([-process-] 或 [-final-],位置无关,取最后一个),其余原样,首尾空白清掉。 */
220
- function stripOneMarker(text) {
221
- if (!text) return "";
222
- const pi = text.lastIndexOf(PROCESS_TAG);
223
- const fi = text.lastIndexOf(FINAL_TAG);
224
- if (pi < 0 && fi < 0) return text.replace(EDGE_WS, "");
225
- const [i, len] = fi >= pi ? [fi, 9] : [pi, 11];
226
- return (text.slice(0, i) + text.slice(i + len)).replace(EDGE_WS, "");
227
- }
228
- /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
229
- function stripPartialTail(text) {
230
- if (!text) return "";
231
- return text.replace(/\[-[a-z]*-?$/i, "");
232
- }
233
- /** 定稿用:有 [-final-] 去掉它;否则剥一个标记。正文不动。 */
234
- function finalClean(text) {
235
- return extractFinal(text) ?? stripOneMarker(text);
236
- }
237
- /** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
238
- function displayClean(text) {
239
- const fin = extractFinal(text);
240
- return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
241
- }
242
- //#endregion
243
207
  //#region src/reply-dispatcher.ts
244
208
  const { createReplyPrefixOptions, createTypingCallbacks, logTypingFailure } = await import("openclaw/plugin-sdk/channel-runtime");
245
209
  function createDingtalkReplyDispatcher(params) {
@@ -265,24 +229,33 @@ function createDingtalkReplyDispatcher(params) {
265
229
  let lastAnswerText = "";
266
230
  let processMarkerLogged = false;
267
231
  let finalMarkerLogged = false;
232
+ let markerSystemActive = false;
268
233
  const observeReply = (raw, isReasoning) => {
269
234
  const text = raw ?? "";
270
235
  if (!text) return;
271
- if (!processMarkerLogged && text.includes("[-process-]")) {
272
- processMarkerLogged = true;
273
- log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(已剥离,不展示给用户)`);
274
- }
275
- const fin = extractFinal(text);
276
- if (fin !== null) {
236
+ const headTag = text.startsWith("[-final-]") ? "final" : text.startsWith("[-process-]") ? "process" : null;
237
+ const tailTag = text.endsWith("[-final-]") ? "final" : text.endsWith("[-process-]") ? "process" : null;
238
+ const abnormal = headTag !== null && tailTag !== null && headTag !== tailTag;
239
+ if (!abnormal && (headTag === "final" || tailTag === "final")) {
277
240
  if (!finalMarkerLogged) {
278
241
  finalMarkerLogged = true;
279
- log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(已剥离,以其后内容为最终答案)`);
242
+ log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(头尾),标记系统激活,跳过 OpenClaw 默认兜底`);
280
243
  }
281
- finalMarkedText = fin;
244
+ finalMarkedText = extractFinal(text);
245
+ markerSystemActive = true;
246
+ return;
247
+ }
248
+ if (!abnormal && (headTag === "process" || tailTag === "process")) {
249
+ if (!processMarkerLogged) {
250
+ processMarkerLogged = true;
251
+ log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(头尾),标记系统激活,抑制 OpenClaw 默认兜底`);
252
+ }
253
+ markerSystemActive = true;
254
+ return;
282
255
  }
283
- if (!isReasoning && text.trim()) lastAnswerText = text;
256
+ if (!markerSystemActive && !isReasoning && text.trim()) lastAnswerText = text;
284
257
  };
285
- const pickFinalText = () => finalMarkedText ?? (lastAnswerText || accumulatedText);
258
+ const pickFinalText = () => markerSystemActive ? finalMarkedText ?? accumulatedText : lastAnswerText || accumulatedText;
286
259
  const applyReplyTemplate = async (text) => {
287
260
  try {
288
261
  const runner = getGlobalHookRunner?.();
@@ -511,6 +484,7 @@ function createDingtalkReplyDispatcher(params) {
511
484
  lastAnswerText = "";
512
485
  processMarkerLogged = false;
513
486
  finalMarkerLogged = false;
487
+ markerSystemActive = false;
514
488
  if (streamingEnabled) startStreaming();
515
489
  typingCallbacks.onActive?.();
516
490
  },
@@ -3,6 +3,49 @@ import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
3
3
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
4
4
  import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
5
5
  import { r as MEDIA_MSG_TYPES } from "./session-DJ4jYqPv.mjs";
6
+ //#region src/services/reply-markers.ts
7
+ const PROCESS_TAG = "[-process-]";
8
+ const FINAL_TAG = "[-final-]";
9
+ const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
10
+ /** 取最终答案:只剥头尾的 [-final-],中间的保留。
11
+ * 头尾都没有 [-final-] → null(不是最终答案)。
12
+ * 剥完头尾 final 后若另一端仍是 [-process-](如 [-process-]X[-final-])→ 异常,返回 null 走兜底。 */
13
+ function extractFinal(text) {
14
+ if (!text) return null;
15
+ const headIsFinal = text.startsWith(FINAL_TAG);
16
+ const tailIsFinal = text.endsWith(FINAL_TAG);
17
+ if (!headIsFinal && !tailIsFinal) return null;
18
+ let result = text;
19
+ if (headIsFinal) result = result.slice(9);
20
+ if (tailIsFinal) result = result.slice(0, -9);
21
+ if (result.startsWith("[-process-]") || result.endsWith("[-process-]")) return null;
22
+ return result.replace(EDGE_WS, "");
23
+ }
24
+ /** 剥掉开头和结尾的一个标记 token([-process-] 或 [-final-],头尾优先,中间的保留)。 */
25
+ function stripOneMarker(text) {
26
+ if (!text) return "";
27
+ let result = text;
28
+ if (result.startsWith("[-process-]")) result = result.slice(11);
29
+ else if (result.startsWith("[-final-]")) result = result.slice(9);
30
+ if (result.endsWith("[-process-]")) result = result.slice(0, -11);
31
+ else if (result.endsWith("[-final-]")) result = result.slice(0, -9);
32
+ return result.replace(EDGE_WS, "");
33
+ }
34
+ /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
35
+ function stripPartialTail(text) {
36
+ if (!text) return "";
37
+ return text.replace(/\[-[a-z]*-?$/i, "");
38
+ }
39
+ /** 定稿用:剥掉头尾各一个标记(final 或 process),中间的保留。正文不动。 */
40
+ function finalClean(text) {
41
+ return stripOneMarker(text);
42
+ }
43
+ /** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
44
+ function displayClean(text) {
45
+ const fin = extractFinal(text);
46
+ return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
47
+ }
48
+ //#endregion
6
49
  //#region src/services/messaging/card.ts
7
50
  const _activeCardRegistry = /* @__PURE__ */ new Map();
8
51
  function registerActiveCard(openConversationId, card) {
@@ -225,15 +268,11 @@ async function ensureValidToken(card, config) {
225
268
  * 遇到 QpsLimit 错误时自动退避 2 秒后重试一次。
226
269
  */
227
270
  async function streamAICard(card, content, finished = false, config, log, contentVar) {
228
- const hadMarker = content.includes("[-process-]") || content.includes("[-final-]");
229
- let finalContent = content;
230
- if (hadMarker) {
231
- const i = content.lastIndexOf("[-final-]");
232
- finalContent = i >= 0 ? content.slice(i + 9) : content;
233
- finalContent = finalContent.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
234
- log?.info?.(`[DingTalk][marker] ${finished ? "finishAICard" : "streamAICard"} 检测到标记,已剥离(${content.length}→${finalContent.length} 字)`);
271
+ if (content.includes("[-process-]") || content.includes("[-final-]")) {
272
+ const cleaned = finalClean(content);
273
+ log?.info?.(`[DingTalk][marker] ${finished ? "finishAICard" : "streamAICard"} 检测到标记,已剥离(${content.length}→${cleaned.length} 字)`);
274
+ content = cleaned;
235
275
  }
236
- content = finalContent;
237
276
  const varName = contentVar || config?.cardProcessVar || config?.cardContentVar || DEFAULT_CARD_CONTENT_VAR;
238
277
  if (!card) {
239
278
  log?.warn?.(`[DingTalk][AICard] streamAICard 收到 null card,跳过更新`);
@@ -1041,4 +1080,4 @@ async function sendProactiveInternal(config, target, content, options) {
1041
1080
  }
1042
1081
  }
1043
1082
  //#endregion
1044
- export { sendTextMessage as a, prepareMultiBotMentions as c, getActiveCardForConversation as d, isQpsLimitError as f, unregisterActiveCard as h, sendProactive as i, createAICardForTarget as l, streamAICard as m, sendMediaToDingTalk as n, sendTextToDingTalk as o, registerActiveCard as p, sendMessage as r, buildBotMentionTable as s, sendMarkdownMessage as t, finishAICard as u };
1083
+ export { PROCESS_TAG as _, sendTextMessage as a, finalClean as b, prepareMultiBotMentions as c, getActiveCardForConversation as d, isQpsLimitError as f, FINAL_TAG as g, unregisterActiveCard as h, sendProactive as i, createAICardForTarget as l, streamAICard as m, sendMediaToDingTalk as n, sendTextToDingTalk as o, registerActiveCard as p, sendMessage as r, buildBotMentionTable as s, sendMarkdownMessage as t, finishAICard as u, displayClean as v, extractFinal as y };
@@ -3,7 +3,7 @@ import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfigu
3
3
  import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import "./utils-CIfI_3Jh.mjs";
6
- import { d as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-B6l1sRvX.mjs";
6
+ import { b as finalClean, d as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CBQl0D00.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
@@ -919,7 +919,7 @@ async function monitorDingtalkProvider(opts = {}) {
919
919
  const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
920
920
  const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
921
921
  import("./accounts-BQptOmgB.mjs"),
922
- import("./message-handler-DESzFFDc.mjs"),
922
+ import("./message-handler-CqC0-7FJ.mjs"),
923
923
  import("./connection-DHHFFNQJ.mjs")
924
924
  ]);
925
925
  const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
@@ -1199,9 +1199,7 @@ const dingtalkPlugin = {
1199
1199
  ...account.clientSecret != null ? { clientSecret: account.clientSecret } : {}
1200
1200
  };
1201
1201
  if (text && (text.includes("[-process-]") || text.includes("[-final-]"))) {
1202
- const i = text.lastIndexOf("[-final-]");
1203
- let cleaned = i >= 0 ? text.slice(i + 9) : text;
1204
- cleaned = cleaned.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
1202
+ const cleaned = finalClean(text);
1205
1203
  createLogger(account.config?.debug ?? false, "DingTalk:SendText").info(`[DingTalk][marker] sendText 检测到标记,已剥离(${text.length}→${cleaned.length} 字)`);
1206
1204
  text = cleaned;
1207
1205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.21-fix2",
3
+ "version": "0.8.21-fix4",
4
4
  "description": "OpenClaw DingTalk channel plugin (fork) | 钉钉 OpenClaw 渠道插件(fork 自官方,含标记/卡片渲染修复)",
5
5
  "type": "module",
6
6
  "exports": {
package/src/channel.ts CHANGED
@@ -30,6 +30,7 @@ import { dingtalkOnboardingAdapter } from "./onboarding.ts";
30
30
  import { monitorDingtalkProvider } from "./core/provider.ts";
31
31
  import { sendTextToDingTalk, sendMediaToDingTalk } from "./services/messaging/index.ts";
32
32
  import { getActiveCardForConversation } from "./services/messaging/card.ts";
33
+ import { finalClean } from "./services/reply-markers.ts";
33
34
  import type { ResolvedDingtalkAccount, DingtalkConfig } from "./types/index.ts";
34
35
 
35
36
  /** Channel identifier used across the plugin. Single source of truth. */
@@ -337,11 +338,9 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
337
338
  ...(account.clientSecret != null ? { clientSecret: account.clientSecret } : {}),
338
339
  };
339
340
 
340
- // marker 剥离(非流式 / 无 AI Card 路径):带标记 → 取最终答案 + 剥离
341
+ // marker 剥离(非流式 / 无 AI Card 路径):只剥头尾标记,中间的保留(agent 解释标记用)
341
342
  if (text && (text.includes("[-process-]") || text.includes("[-final-]"))) {
342
- const i = text.lastIndexOf("[-final-]");
343
- let cleaned = i >= 0 ? text.slice(i + "[-final-]".length) : text;
344
- cleaned = cleaned.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
343
+ const cleaned = finalClean(text);
345
344
  createLogger(account.config?.debug ?? false, 'DingTalk:SendText')
346
345
  .info(`[DingTalk][marker] sendText 检测到标记,已剥离(${text.length}→${cleaned.length} 字)`);
347
346
  text = cleaned;
@@ -115,27 +115,53 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
115
115
  let processMarkerLogged = false;
116
116
  let finalMarkerLogged = false;
117
117
 
118
- // 观察每段到达的文本,更新两个认定源,并在首次检测到标记时打日志(用户无感,仅日志可见)。
118
+ // 观察每段到达的文本,更新标记认定状态。
119
+ // 标记系统激活规则:
120
+ // [-final-] 出现在头/尾 → 直接认定为最终答案,跳过 OpenClaw 兜底
121
+ // [-process-] 出现在头/尾 → 抑制 OpenClaw 的最终答案判定,防止中间过程被误判,流式继续渲染
122
+ // 无标记 / 头尾标记类型不同 → 走 OpenClaw 默认兜底
123
+ let markerSystemActive = false;
124
+
119
125
  const observeReply = (raw: string | undefined, isReasoning: boolean | undefined) => {
120
126
  const text = raw ?? "";
121
127
  if (!text) return;
122
- if (!processMarkerLogged && text.includes(PROCESS_TAG)) {
123
- processMarkerLogged = true;
124
- log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(已剥离,不展示给用户)`);
125
- }
126
- const fin = extractFinal(text);
127
- if (fin !== null) {
128
+
129
+ const headTag = text.startsWith(FINAL_TAG) ? "final" : text.startsWith(PROCESS_TAG) ? "process" : null;
130
+ const tailTag = text.endsWith(FINAL_TAG) ? "final" : text.endsWith(PROCESS_TAG) ? "process" : null;
131
+ // 异常:头尾标记类型不同(如 [-process-]…[-final-])→ 不激活标记系统,按普通文本走 OpenClaw 兜底
132
+ const abnormal = headTag !== null && tailTag !== null && headTag !== tailTag;
133
+
134
+ // [-final-] 在头/尾(非异常)→ 标记系统激活,提取最终答案,跳过 OpenClaw 兜底
135
+ if (!abnormal && (headTag === "final" || tailTag === "final")) {
128
136
  if (!finalMarkerLogged) {
129
137
  finalMarkerLogged = true;
130
- log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(已剥离,以其后内容为最终答案)`);
138
+ log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(头尾),标记系统激活,跳过 OpenClaw 默认兜底`);
139
+ }
140
+ finalMarkedText = extractFinal(text);
141
+ markerSystemActive = true;
142
+ return;
143
+ }
144
+
145
+ // [-process-] 在头/尾(非异常)→ 标记系统激活,抑制 OpenClaw 默认兜底
146
+ if (!abnormal && (headTag === "process" || tailTag === "process")) {
147
+ if (!processMarkerLogged) {
148
+ processMarkerLogged = true;
149
+ log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(头尾),标记系统激活,抑制 OpenClaw 默认兜底`);
131
150
  }
132
- finalMarkedText = fin; // 取最后一个 [-final-] 之后内容
151
+ markerSystemActive = true;
152
+ return;
133
153
  }
134
- if (!isReasoning && text.trim()) lastAnswerText = text; // 非思考过程 = 正式答案
154
+
155
+ // 无头尾标记 或 头尾标记异常:标记系统未激活时走 OpenClaw 兜底(标记由 finalClean 善后剥离)
156
+ // 已激活则保持沉默,让标记系统独占最终答案判定
157
+ if (!markerSystemActive && !isReasoning && text.trim()) lastAnswerText = text;
135
158
  };
136
159
 
137
- // 选定本轮最终答案:优先 marker,其次最近的非 reasoning 答案,最后退回 accumulatedText。
138
- const pickFinalText = (): string => finalMarkedText ?? (lastAnswerText || accumulatedText);
160
+ // 选定本轮最终答案:
161
+ // 标记系统激活 → finalMarkedText([-final-] 在头/尾)优先,accumulatedText 流式兜底(跳过 OpenClaw)
162
+ // 未激活(无标记 / 头尾异常)→ lastAnswerText(OpenClaw isReasoning 兜底),accumulatedText 兜底
163
+ const pickFinalText = (): string =>
164
+ markerSystemActive ? (finalMarkedText ?? accumulatedText) : (lastAnswerText || accumulatedText);
139
165
 
140
166
  // 对最终答案套用 prompt-rewriter 的固定模板(跑 reply_payload_sending 钩子)。失败不阻断投递。
141
167
  const applyReplyTemplate = async (text: string): Promise<string> => {
@@ -529,6 +555,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
529
555
  lastAnswerText = "";
530
556
  processMarkerLogged = false;
531
557
  finalMarkerLogged = false;
558
+ markerSystemActive = false;
532
559
  if (streamingEnabled) {
533
560
  // fire-and-forget:提前创建 AI Card,onPartialReply 会等待创建完成
534
561
  void startStreaming();
@@ -6,6 +6,7 @@
6
6
  import type { DingtalkConfig } from "../../types/index.ts";
7
7
  import { DINGTALK_API, getAccessToken } from "../../utils/token.ts";
8
8
  import { dingtalkHttp } from "../../utils/http-client.ts";
9
+ import { FINAL_TAG, PROCESS_TAG, finalClean } from "../reply-markers.ts";
9
10
 
10
11
  // ============ 全局 AI Card 活跃注册表 ============
11
12
  // 用于让 outbound.sendText(message 工具)能感知当前会话是否有活跃的 AI Card,
@@ -372,15 +373,11 @@ export async function streamAICard(
372
373
  ): Promise<void> {
373
374
  // marker 剥离:所有卡片写入都经过这里,是钉钉侧的单一 chokepoint。
374
375
  // 带标记 → 提取最终答案 + 剥离;不带 → 原样。
375
- const hadMarker = content.includes("[-process-]") || content.includes("[-final-]");
376
- let finalContent = content;
377
- if (hadMarker) {
378
- const i = content.lastIndexOf("[-final-]");
379
- finalContent = i >= 0 ? content.slice(i + "[-final-]".length) : content;
380
- finalContent = finalContent.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
381
- log?.info?.(`[DingTalk][marker] ${finished ? "finishAICard" : "streamAICard"} 检测到标记,已剥离(${content.length}→${finalContent.length} 字)`);
376
+ if (content.includes(PROCESS_TAG) || content.includes(FINAL_TAG)) {
377
+ const cleaned = finalClean(content);
378
+ log?.info?.(`[DingTalk][marker] ${finished ? "finishAICard" : "streamAICard"} 检测到标记,已剥离(${content.length}→${cleaned.length} 字)`);
379
+ content = cleaned;
382
380
  }
383
- content = finalContent;
384
381
 
385
382
  const varName = contentVar
386
383
  || (config?.cardProcessVar as string)
@@ -1,55 +1,65 @@
1
- // 回复标记工具 —— 配合 prompt-rewriter 插件注入的"打标记"指令使用。
2
- //
3
- // 【单标记方案,位置无关】指令要求每条消息【末尾】加标记(过程 [-process-]、最终答案 [-final-],整轮一个),
4
- // 但模型常把标记放开头。所以提取/剥离都【不依赖位置】:只删那一个标记 token,其余原样。
5
- // 最终答案 = 带 [-final-] 的那条消息去掉该标记后的内容。
6
- // 正文里其它同名字面量(模型解释标记时)原样保留——只动作为信号的那一个。
7
- //
8
- // ⚠️ 标记字面量与逻辑必须与 prompt-rewriter 的 rewriter.ts 保持一致(两处重复,改一处要同步另一处)。
9
-
10
- export const PROCESS_TAG = "[-process-]";
11
- export const FINAL_TAG = "[-final-]";
12
-
13
- const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
14
-
15
- /** 文本里是否带任一标记(用于"带标记走记号规则 / 不带走兜底"的分流判断)。 */
16
- export function hasMarkers(text: string | undefined | null): boolean {
17
- if (!text) return false;
18
- return text.includes(PROCESS_TAG) || text.includes(FINAL_TAG);
19
- }
20
-
21
- /** 取最终答案:去掉那一个 [-final-](开头/中间/结尾都行),其余原样,首尾空白清掉。
22
- * 没有 [-final-] 返回 null。多处出现取最后一个当信号。 */
23
- export function extractFinal(text: string | undefined | null): string | null {
24
- if (!text) return null;
25
- const i = text.lastIndexOf(FINAL_TAG);
26
- if (i < 0) return null;
27
- return (text.slice(0, i) + text.slice(i + FINAL_TAG.length)).replace(EDGE_WS, "");
28
- }
29
-
30
- /** 剥掉一个标记 token([-process-] 或 [-final-],位置无关,取最后一个),其余原样,首尾空白清掉。 */
31
- export function stripOneMarker(text: string | undefined | null): string {
32
- if (!text) return "";
33
- const pi = text.lastIndexOf(PROCESS_TAG);
34
- const fi = text.lastIndexOf(FINAL_TAG);
35
- if (pi < 0 && fi < 0) return text.replace(EDGE_WS, "");
36
- const [i, len] = fi >= pi ? [fi, FINAL_TAG.length] : [pi, PROCESS_TAG.length];
37
- return (text.slice(0, i) + text.slice(i + len)).replace(EDGE_WS, "");
38
- }
39
-
40
- /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
41
- export function stripPartialTail(text: string | undefined | null): string {
42
- if (!text) return "";
43
- return text.replace(/\[-[a-z]*-?$/i, "");
44
- }
45
-
46
- /** 定稿用:有 [-final-] 去掉它;否则剥一个标记。正文不动。 */
47
- export function finalClean(text: string | undefined | null): string {
48
- return extractFinal(text) ?? stripOneMarker(text);
49
- }
50
-
51
- /** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
52
- export function displayClean(text: string | undefined | null): string {
53
- const fin = extractFinal(text);
54
- return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
55
- }
1
+ // 回复标记工具 —— 配合 prompt-rewriter 插件注入的"打标记"指令使用。
2
+ //
3
+ // 【头尾标记方案】只 stripping 文本【开头】和【结尾】的标记 token,中间的保留。
4
+ // 原因:agent 可能在正文中解释标记含义,这些解释性的标记不能删。
5
+ // 最终答案 = 去掉开头/结尾的 [-final-] 后的内容。
6
+ // 没有 [-final-] 在头尾 → 返回 null(表示不是最终答案)。
7
+ //
8
+ // ⚠️ 标记字面量与逻辑必须与 prompt-rewriter 的 rewriter.ts 保持一致(两处重复,改一处要同步另一处)。
9
+
10
+ export const PROCESS_TAG = "[-process-]";
11
+ export const FINAL_TAG = "[-final-]";
12
+
13
+ const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
14
+
15
+ /** 取最终答案:只剥头尾的 [-final-],中间的保留。
16
+ * 头尾都没有 [-final-] → null(不是最终答案)。
17
+ * 剥完头尾 final 后若另一端仍是 [-process-](如 [-process-]X[-final-])→ 异常,返回 null 走兜底。 */
18
+ export function extractFinal(text: string | undefined | null): string | null {
19
+ if (!text) return null;
20
+ const headIsFinal = text.startsWith(FINAL_TAG);
21
+ const tailIsFinal = text.endsWith(FINAL_TAG);
22
+ if (!headIsFinal && !tailIsFinal) return null;
23
+ let result = text;
24
+ if (headIsFinal) result = result.slice(FINAL_TAG.length);
25
+ if (tailIsFinal) result = result.slice(0, -FINAL_TAG.length);
26
+ // 头尾混入 process(标记类型不同)→ 异常,不认定为最终答案
27
+ if (result.startsWith(PROCESS_TAG) || result.endsWith(PROCESS_TAG)) return null;
28
+ return result.replace(EDGE_WS, "");
29
+ }
30
+
31
+ /** 剥掉开头和结尾的一个标记 token([-process-] 或 [-final-],头尾优先,中间的保留)。 */
32
+ export function stripOneMarker(text: string | undefined | null): string {
33
+ if (!text) return "";
34
+ let result = text;
35
+ // Strip start: process or final
36
+ if (result.startsWith(PROCESS_TAG)) {
37
+ result = result.slice(PROCESS_TAG.length);
38
+ } else if (result.startsWith(FINAL_TAG)) {
39
+ result = result.slice(FINAL_TAG.length);
40
+ }
41
+ // Strip end: process or final
42
+ if (result.endsWith(PROCESS_TAG)) {
43
+ result = result.slice(0, -PROCESS_TAG.length);
44
+ } else if (result.endsWith(FINAL_TAG)) {
45
+ result = result.slice(0, -FINAL_TAG.length);
46
+ }
47
+ return result.replace(EDGE_WS, "");
48
+ }
49
+
50
+ /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
51
+ export function stripPartialTail(text: string | undefined | null): string {
52
+ if (!text) return "";
53
+ return text.replace(/\[-[a-z]*-?$/i, "");
54
+ }
55
+
56
+ /** 定稿用:剥掉头尾各一个标记(final 或 process),中间的保留。正文不动。 */
57
+ export function finalClean(text: string | undefined | null): string {
58
+ return stripOneMarker(text);
59
+ }
60
+
61
+ /** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
62
+ export function displayClean(text: string | undefined | null): string {
63
+ const fin = extractFinal(text);
64
+ return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
65
+ }
@@ -1,2 +0,0 @@
1
- import { t as registerGatewayMethods } from "./gateway-methods-C4tcgI7P.mjs";
2
- export { registerGatewayMethods };