@jeik/dingtalk-connector 0.8.21-fix7 → 0.8.21-fix9

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.
@@ -294,16 +294,14 @@ function saveCredentials(clientId, clientSecret, { isLocal = false, pluginInstal
294
294
 
295
295
  // ── plugin install ─────────────────────────────────────────────
296
296
  function getInstallSpec() {
297
- // Read version from own package.json to pass the exact version to openclaw
297
+ // 永远钉住"本包自己的精确版本",让 openclaw 内部装的版本 == 你 npx 实际下到的版本。
298
+ // npx @fix / @0.8.21-fix7 / @latest / 不带 → 各自下到对应包 → 这里读到的就是那个版本。
299
+ // 不带版本号会让 openclaw 装 latest,导致"跑 fix 向导却装了正式版"那个坑。
298
300
  try {
299
- const require = createRequire(import.meta.url);
300
- const { version } = require('../package.json');
301
- // 任何预发布版(含 -)都钉精确版本,否则 openclaw 会装 latest(如 -fix6 被当稳定版误装成 0.8.21)
302
- if (version && version.includes('-')) {
303
- return `${PKG_NAME}@${version}`;
304
- }
301
+ const { version } = createRequire(import.meta.url)('../package.json');
302
+ if (version) return `${PKG_NAME}@${version}`;
305
303
  } catch {}
306
- return PKG_NAME;
304
+ return PKG_NAME; // 兜底:读不到版本才退回不带版本(极少见)
307
305
  }
308
306
 
309
307
  // 在 plugins.load.paths 扫描范围内找会"遮蔽"npm 版的本地 dingtalk 插件副本。
@@ -665,14 +663,21 @@ async function ensureDwsCli() {
665
663
  const versionDisplay = installedVersion ? `v${installedVersion}` : 'unknown version';
666
664
 
667
665
  if (!targetVersion) {
668
- // @latest 模式:解析不到固定版本号 → 始终重装拉最新(latest 不会降级,无需比较/询问)
669
- console.log(dim(` ℹ dws CLI detected (${versionDisplay}), ensuring latest (@latest)...`) + '\n');
670
- const ok = installDwsCli();
671
- if (ok) {
672
- const nv = getInstalledDwsVersion();
673
- console.log(green(` ✔ dws CLI is now ${nv ? 'v' + nv : 'latest'}`) + '\n');
666
+ // @latest 模式:已安装则询问是否跳过更新(默认跳过,保留当前版本)
667
+ const ans = await askUserConfirmation(
668
+ ` 检测到 dws CLI 已安装 (${versionDisplay}),是否跳过更新?(skip dws update?) [Y/n] `,
669
+ );
670
+ if (ans === 'n' || ans === 'no') {
671
+ console.log(dim(` 正在更新 dws CLI 到最新版...`) + '\n');
672
+ const ok = installDwsCli();
673
+ if (ok) {
674
+ const nv = getInstalledDwsVersion();
675
+ console.log(green(` ✔ dws CLI is now ${nv ? 'v' + nv : 'latest'}`) + '\n');
676
+ } else {
677
+ console.log(red(' ⚠ Update failed. Continuing with current version.') + '\n');
678
+ }
674
679
  } else {
675
- console.log(red(' ⚠ Update failed. Continuing with current version.') + '\n');
680
+ console.log(dim(` 已跳过更新,保留当前版本 ${versionDisplay}。`) + '\n');
676
681
  }
677
682
  // 鉴权状态检查(与下方同逻辑)
678
683
  if (isDwsAuthenticated()) {
@@ -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-D7aACzCW.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-B9kuAOFf.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -1,2 +1,2 @@
1
- import { t as registerGatewayMethods } from "./gateway-methods-DM8lWI4F.mjs";
1
+ import { t as registerGatewayMethods } from "./gateway-methods-DfW2oZ6c.mjs";
2
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-CBQl0D00.mjs";
4
+ import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-CvZBBMgv.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-5-KOSMI9.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods-DM8lWI4F.mjs";
1
+ import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-CUSz0JLV.mjs";
2
+ import { t as registerGatewayMethods } from "./gateway-methods-DfW2oZ6c.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-5-KOSMI9.mjs";
3
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-CUSz0JLV.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 { _ 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";
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-CvZBBMgv.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";
@@ -233,22 +233,20 @@ function createDingtalkReplyDispatcher(params) {
233
233
  const observeReply = (raw, isReasoning) => {
234
234
  const text = raw ?? "";
235
235
  if (!text) return;
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")) {
236
+ const fin = extractFinal(text);
237
+ if (fin !== null) {
240
238
  if (!finalMarkerLogged) {
241
239
  finalMarkerLogged = true;
242
- log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(头尾),标记系统激活,跳过 OpenClaw 默认兜底`);
240
+ log.info(`[DingTalk][marker] 检测到 ${FINAL_TAG},最终答案取最后一个 ${PROCESS_TAG} 之后内容,跳过 OpenClaw 兜底`);
243
241
  }
244
- finalMarkedText = extractFinal(text);
242
+ finalMarkedText = fin;
245
243
  markerSystemActive = true;
246
244
  return;
247
245
  }
248
- if (!abnormal && (headTag === "process" || tailTag === "process")) {
246
+ if (text.includes("[-process-]")) {
249
247
  if (!processMarkerLogged) {
250
248
  processMarkerLogged = true;
251
- log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(头尾),标记系统激活,抑制 OpenClaw 默认兜底`);
249
+ log.info(`[DingTalk][marker] 检测到 ${PROCESS_TAG}(过程段),抑制 OpenClaw 默认兜底`);
252
250
  }
253
251
  markerSystemActive = true;
254
252
  return;
@@ -485,7 +483,6 @@ function createDingtalkReplyDispatcher(params) {
485
483
  processMarkerLogged = false;
486
484
  finalMarkerLogged = false;
487
485
  markerSystemActive = false;
488
- if (streamingEnabled) startStreaming();
489
486
  typingCallbacks.onActive?.();
490
487
  },
491
488
  deliver: async (payload, info) => {
@@ -7,43 +7,44 @@ import { r as MEDIA_MSG_TYPES } from "./session-DJ4jYqPv.mjs";
7
7
  const PROCESS_TAG = "[-process-]";
8
8
  const FINAL_TAG = "[-final-]";
9
9
  const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
10
- /** 取最终答案:只剥头尾的 [-final-],中间的保留。
11
- * 头尾都没有 [-final-] → null(不是最终答案)。
12
- * 剥完头尾 final 后若另一端仍是 [-process-](如 [-process-]X[-final-])→ 异常,返回 null 走兜底。 */
10
+ const HR_LINE = /^[ \t]*(?:[-*_][ \t]*){3,}(?:\r?\n|$)/;
11
+ /** 剥掉文本中所有 [-process-] / [-final-] 标记(含中间的),首尾空白清掉。 */
12
+ function stripAllMarkers(text) {
13
+ if (!text) return "";
14
+ return text.split(PROCESS_TAG).join("").split(FINAL_TAG).join("").replace(EDGE_WS, "");
15
+ }
16
+ /** 清掉段落开头的空白行 + markdown 分割横线(反复剥,处理 "\n\n---\n" 这种)。 */
17
+ function stripLeadingSeparators(text) {
18
+ let s = text.replace(/^[ \t\r\n]+/, "");
19
+ let m;
20
+ while ((m = s.match(HR_LINE)) && m[0].length > 0) s = s.slice(m[0].length).replace(/^[ \t\r\n]+/, "");
21
+ return s;
22
+ }
23
+ /** 取最终答案:取「最后一个 [-process-] 右边、[-final-] 左边」那段,剥残留标记 + 开头分隔,首尾清白。
24
+ * 没有 [-final-] → null(不是最终答案,交给 OpenClaw 兜底)。 */
13
25
  function extractFinal(text) {
14
26
  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, "");
27
+ const fi = text.lastIndexOf(FINAL_TAG);
28
+ if (fi < 0) return null;
29
+ const before = text.slice(0, fi);
30
+ const pi = before.lastIndexOf(PROCESS_TAG);
31
+ let seg = pi >= 0 ? before.slice(pi + 11) : before;
32
+ seg = seg.split(PROCESS_TAG).join("").split(FINAL_TAG).join("");
33
+ seg = stripLeadingSeparators(seg).replace(EDGE_WS, "");
34
+ return seg || stripAllMarkers(text);
33
35
  }
34
- /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
36
+ /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给 stripAllMarkers。 */
35
37
  function stripPartialTail(text) {
36
38
  if (!text) return "";
37
39
  return text.replace(/\[-[a-z]*-?$/i, "");
38
40
  }
39
- /** 定稿用:剥掉头尾各一个标记(final 或 process),中间的保留。正文不动。 */
41
+ /** 定稿/单条消息用:剥光所有标记,保留完整正文(message 工具单条消息走这里)。 */
40
42
  function finalClean(text) {
41
- return stripOneMarker(text);
43
+ return stripAllMarkers(text);
42
44
  }
43
- /** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
45
+ /** 流式展示用:剥光所有标记 + 末尾半截标记。 */
44
46
  function displayClean(text) {
45
- const fin = extractFinal(text);
46
- return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
47
+ return stripPartialTail(stripAllMarkers(text));
47
48
  }
48
49
  //#endregion
49
50
  //#region src/services/messaging/card.ts
@@ -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 { b as finalClean, d as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CBQl0D00.mjs";
6
+ import { b as finalClean, d as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CvZBBMgv.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-CqC0-7FJ.mjs"),
922
+ import("./message-handler-CWW2dKz0.mjs"),
923
923
  import("./connection-DHHFFNQJ.mjs")
924
924
  ]);
925
925
  const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.21-fix7",
3
+ "version": "0.8.21-fix9",
4
4
  "description": "OpenClaw DingTalk channel plugin (fork) | 钉钉 OpenClaw 渠道插件(fork 自官方,含标记/卡片渲染修复)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -116,50 +116,44 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
116
116
  let finalMarkerLogged = false;
117
117
 
118
118
  // 观察每段到达的文本,更新标记认定状态。
119
- // 标记系统激活规则:
120
- // [-final-] 出现在头/尾 → 直接认定为最终答案,跳过 OpenClaw 兜底
121
- // [-process-] 出现在头/尾 → 抑制 OpenClaw 的最终答案判定,防止中间过程被误判,流式继续渲染
122
- // 无标记 / 头尾标记类型不同 → 走 OpenClaw 默认兜底
119
+ // 含 [-final-] → 最终答案 = 最后一个 [-process-] 右边、[-final-] 左边那段(extractFinal),跳过兜底
120
+ // 含 [-process-](无 [-final-])→ 过程段,激活标记系统抑制 OpenClaw 兜底
121
+ // 无标记 → 走 OpenClaw 默认兜底
123
122
  let markerSystemActive = false;
124
123
 
125
124
  const observeReply = (raw: string | undefined, isReasoning: boolean | undefined) => {
126
125
  const text = raw ?? "";
127
126
  if (!text) return;
128
127
 
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
+ // [-final-] 出现 → 提取最终段(最后一个 [-process-] 之后),跳过 OpenClaw 兜底
129
+ const fin = extractFinal(text);
130
+ if (fin !== null) {
136
131
  if (!finalMarkerLogged) {
137
132
  finalMarkerLogged = true;
138
- log.info(`[DingTalk][marker] 检测到最终标记 ${FINAL_TAG}(头尾),标记系统激活,跳过 OpenClaw 默认兜底`);
133
+ log.info(`[DingTalk][marker] 检测到 ${FINAL_TAG},最终答案取最后一个 ${PROCESS_TAG} 之后内容,跳过 OpenClaw 兜底`);
139
134
  }
140
- finalMarkedText = extractFinal(text);
135
+ finalMarkedText = fin;
141
136
  markerSystemActive = true;
142
137
  return;
143
138
  }
144
139
 
145
- // [-process-] 在头/尾(非异常)→ 标记系统激活,抑制 OpenClaw 默认兜底
146
- if (!abnormal && (headTag === "process" || tailTag === "process")) {
140
+ // 只有 [-process-](无 [-final-])→ 过程段,激活标记系统抑制兜底
141
+ if (text.includes(PROCESS_TAG)) {
147
142
  if (!processMarkerLogged) {
148
143
  processMarkerLogged = true;
149
- log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(头尾),标记系统激活,抑制 OpenClaw 默认兜底`);
144
+ log.info(`[DingTalk][marker] 检测到 ${PROCESS_TAG}(过程段),抑制 OpenClaw 默认兜底`);
150
145
  }
151
146
  markerSystemActive = true;
152
147
  return;
153
148
  }
154
149
 
155
- // 无头尾标记 或 头尾标记异常:标记系统未激活时走 OpenClaw 兜底(标记由 finalClean 善后剥离)
156
- // 已激活则保持沉默,让标记系统独占最终答案判定
150
+ // 无标记:标记系统未激活时走 OpenClaw 兜底;已激活则沉默,让标记系统独占判定
157
151
  if (!markerSystemActive && !isReasoning && text.trim()) lastAnswerText = text;
158
152
  };
159
153
 
160
154
  // 选定本轮最终答案:
161
- // 标记系统激活 → finalMarkedText([-final-] 在头/尾)优先,accumulatedText 流式兜底(跳过 OpenClaw)
162
- // 未激活(无标记 / 头尾异常)→ lastAnswerText(OpenClaw isReasoning 兜底),accumulatedText 兜底
155
+ // 标记系统激活 → finalMarkedText(最终段)优先,accumulatedText 流式兜底(跳过 OpenClaw)
156
+ // 未激活(无标记)→ lastAnswerText(OpenClaw isReasoning 兜底),accumulatedText 兜底
163
157
  const pickFinalText = (): string =>
164
158
  markerSystemActive ? (finalMarkedText ?? accumulatedText) : (lastAnswerText || accumulatedText);
165
159
 
@@ -556,10 +550,9 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
556
550
  processMarkerLogged = false;
557
551
  finalMarkerLogged = false;
558
552
  markerSystemActive = false;
559
- if (streamingEnabled) {
560
- // fire-and-forget:提前创建 AI Card,onPartialReply 会等待创建完成
561
- void startStreaming();
562
- }
553
+ // 延迟建卡:不在 onReplyStart 抢先建卡,改由第一段真正的对话内容(onPartialReply /
554
+ // deliver block/final)按需创建。否则纯 message 工具轮次(无对话回复,deliver 计数全 0)
555
+ // 会留下一张没人喂的孤儿卡,收尾兜底成"无文本输出"。
563
556
  typingCallbacks.onActive?.();
564
557
  },
565
558
  deliver: async (payload, info) => {
@@ -1,65 +1,63 @@
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
+ // 回复标记工具 —— 配合 prompt-rewriter 插件注入的"打标记"指令使用。
2
+ //
3
+ // 【段落方案】过程段末尾打 [-process-],最终段末尾打 [-final-]。agent 常把整轮写成一段:
4
+ // 过程1…[-process-] 过程2…[-process-] … 最终汇总…[-final-]
5
+ // 最终答案 = 「最后一个 [-process-] 右边、[-final-] 左边」那段(即最终汇总),剥掉残留标记,
6
+ // 并清掉该段开头的换行 / markdown 分割横线(---、***、___)。
7
+ // 没有 [-process-] 时 = [-final-] 左边的全部。没有 [-final-] → 不是最终答案。
8
+ //
9
+ // ⚠️ 标记字面量必须与 prompt-rewriter 的 rewriter.ts 保持一致(两处重复,改一处要同步另一处)。
10
+
11
+ export const PROCESS_TAG = "[-process-]";
12
+ export const FINAL_TAG = "[-final-]";
13
+
14
+ const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
15
+ // markdown 分割横线整行:3+ 个 - / * / _(其间可有空格),如 ---、***、- - -
16
+ const HR_LINE = /^[ \t]*(?:[-*_][ \t]*){3,}(?:\r?\n|$)/;
17
+
18
+ /** 剥掉文本中所有 [-process-] / [-final-] 标记(含中间的),首尾空白清掉。 */
19
+ export function stripAllMarkers(text: string | undefined | null): string {
20
+ if (!text) return "";
21
+ return text.split(PROCESS_TAG).join("").split(FINAL_TAG).join("").replace(EDGE_WS, "");
22
+ }
23
+
24
+ /** 清掉段落开头的空白行 + markdown 分割横线(反复剥,处理 "\n\n---\n" 这种)。 */
25
+ function stripLeadingSeparators(text: string): string {
26
+ let s = text.replace(/^[ \t\r\n]+/, "");
27
+ let m: RegExpMatchArray | null;
28
+ while ((m = s.match(HR_LINE)) && m[0].length > 0) {
29
+ s = s.slice(m[0].length).replace(/^[ \t\r\n]+/, "");
30
+ }
31
+ return s;
32
+ }
33
+
34
+ /** 取最终答案:取「最后一个 [-process-] 右边、[-final-] 左边」那段,剥残留标记 + 开头分隔,首尾清白。
35
+ * 没有 [-final-] → null(不是最终答案,交给 OpenClaw 兜底)。 */
36
+ export function extractFinal(text: string | undefined | null): string | null {
37
+ if (!text) return null;
38
+ const fi = text.lastIndexOf(FINAL_TAG); // [-final-] = 最终段右边界
39
+ if (fi < 0) return null;
40
+ const before = text.slice(0, fi);
41
+ const pi = before.lastIndexOf(PROCESS_TAG); // 最后一个 [-process-] = 最终段左边界
42
+ let seg = pi >= 0 ? before.slice(pi + PROCESS_TAG.length) : before;
43
+ seg = seg.split(PROCESS_TAG).join("").split(FINAL_TAG).join(""); // 残留标记保险
44
+ seg = stripLeadingSeparators(seg).replace(EDGE_WS, "");
45
+ // 段为空(如 [-final-] 在最前)→ 退回剥光全文,避免空答案
46
+ return seg || stripAllMarkers(text);
47
+ }
48
+
49
+ /** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给 stripAllMarkers。 */
50
+ export function stripPartialTail(text: string | undefined | null): string {
51
+ if (!text) return "";
52
+ return text.replace(/\[-[a-z]*-?$/i, "");
53
+ }
54
+
55
+ /** 定稿/单条消息用:剥光所有标记,保留完整正文(message 工具单条消息走这里)。 */
56
+ export function finalClean(text: string | undefined | null): string {
57
+ return stripAllMarkers(text);
58
+ }
59
+
60
+ /** 流式展示用:剥光所有标记 + 末尾半截标记。 */
61
+ export function displayClean(text: string | undefined | null): string {
62
+ return stripPartialTail(stripAllMarkers(text));
63
+ }