@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.
- package/bin/dingtalk-connector.js +6 -0
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-D7aACzCW.mjs +2 -0
- package/dist/{gateway-methods-C4tcgI7P.mjs → gateway-methods-DM8lWI4F.mjs} +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{message-handler-DESzFFDc.mjs → message-handler-CqC0-7FJ.mjs} +22 -48
- package/dist/{messaging-B6l1sRvX.mjs → messaging-CBQl0D00.mjs} +48 -9
- package/dist/{runtime-DUgpo5zC.mjs → runtime-5-KOSMI9.mjs} +3 -5
- package/package.json +1 -1
- package/src/channel.ts +3 -4
- package/src/reply-dispatcher.ts +39 -12
- package/src/services/messaging/card.ts +5 -8
- package/src/services/reply-markers.ts +65 -55
- package/dist/gateway-methods-Ci31A3vg.mjs +0 -2
|
@@ -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) {
|
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-D7aACzCW.mjs");
|
|
27
27
|
registerGatewayMethods(api);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -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-
|
|
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-
|
|
2
|
-
import { t as registerGatewayMethods } from "./gateway-methods-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
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
|
|
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;
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
-
|
|
151
|
+
markerSystemActive = true;
|
|
152
|
+
return;
|
|
133
153
|
}
|
|
134
|
-
|
|
154
|
+
|
|
155
|
+
// 无头尾标记 或 头尾标记异常:标记系统未激活时走 OpenClaw 兜底(标记由 finalClean 善后剥离)
|
|
156
|
+
// 已激活则保持沉默,让标记系统独占最终答案判定
|
|
157
|
+
if (!markerSystemActive && !isReasoning && text.trim()) lastAnswerText = text;
|
|
135
158
|
};
|
|
136
159
|
|
|
137
|
-
//
|
|
138
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// 最终答案 =
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|