@soimy/dingtalk 3.6.2 → 3.6.3
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/dist/index.js +267 -97
- package/dist/index.js.map +3 -3
- package/dist/src/card-draft-controller.d.ts.map +1 -1
- package/dist/src/card-service.d.ts.map +1 -1
- package/dist/src/message-utils.d.ts +10 -0
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/reply-strategy-card.d.ts.map +1 -1
- package/dist/src/reply-strategy-markdown.d.ts.map +1 -1
- package/dist/src/reply-strategy-types.d.ts +2 -0
- package/dist/src/reply-strategy-types.d.ts.map +1 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +4 -3
- package/src/card-service.ts +28 -4
- package/src/message-utils.ts +103 -7
- package/src/reply-strategy-card.ts +12 -3
- package/src/reply-strategy-markdown.ts +139 -20
- package/src/reply-strategy-types.ts +3 -0
- package/src/send-service.ts +67 -65
package/dist/index.js
CHANGED
|
@@ -1814,7 +1814,7 @@ async function updateCardVariables(outTrackId, params, token, config) {
|
|
|
1814
1814
|
{
|
|
1815
1815
|
outTrackId,
|
|
1816
1816
|
cardData: { cardParamMap: stringMap },
|
|
1817
|
-
cardUpdateOptions: { updateCardDataByKey: true
|
|
1817
|
+
cardUpdateOptions: { updateCardDataByKey: true }
|
|
1818
1818
|
},
|
|
1819
1819
|
{
|
|
1820
1820
|
headers: {
|
|
@@ -2614,6 +2614,19 @@ async function createAICard(config, conversationId, log, options = {}) {
|
|
|
2614
2614
|
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
2615
2615
|
}
|
|
2616
2616
|
clearAICardDegrade(accountId, log);
|
|
2617
|
+
try {
|
|
2618
|
+
await putAICardStreamingField(aiCardInstance, template.contentKey, "", false, log, {
|
|
2619
|
+
suppressDegrade: true
|
|
2620
|
+
});
|
|
2621
|
+
aiCardInstance.state = AICardStatus.INPUTING;
|
|
2622
|
+
if (shouldPersistPending) {
|
|
2623
|
+
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
2624
|
+
}
|
|
2625
|
+
} catch (kickErr) {
|
|
2626
|
+
log?.debug?.(
|
|
2627
|
+
`[DingTalk][AICard] Non-critical: failed to kick card into streaming mode: ${kickErr.message}`
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2617
2630
|
return aiCardInstance;
|
|
2618
2631
|
} catch (err) {
|
|
2619
2632
|
log?.error?.(`[DingTalk][AICard] Create failed: ${err.message}`);
|
|
@@ -2712,13 +2725,13 @@ async function clearAICardStreamingContent(card, log) {
|
|
|
2712
2725
|
log?.debug?.(`[DingTalk][AICard] Non-critical: failed to clear streaming content: ${message}`);
|
|
2713
2726
|
}
|
|
2714
2727
|
}
|
|
2715
|
-
async function finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2728
|
+
async function finalizeAICardStreamingLifecycleIfNeeded(card, log) {
|
|
2716
2729
|
if (!card.streamLifecycleOpened) {
|
|
2717
2730
|
return;
|
|
2718
2731
|
}
|
|
2719
2732
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2720
2733
|
try {
|
|
2721
|
-
await putAICardStreamingField(card, template.streamingKey,
|
|
2734
|
+
await putAICardStreamingField(card, template.streamingKey, "", true, log, {
|
|
2722
2735
|
suppressDegrade: true
|
|
2723
2736
|
});
|
|
2724
2737
|
} catch (err) {
|
|
@@ -2734,7 +2747,7 @@ async function commitAICardBlocks(card, options, log) {
|
|
|
2734
2747
|
return;
|
|
2735
2748
|
}
|
|
2736
2749
|
await ensureFreshToken(card, log);
|
|
2737
|
-
await finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2750
|
+
await finalizeAICardStreamingLifecycleIfNeeded(card, log);
|
|
2738
2751
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2739
2752
|
const updates = {
|
|
2740
2753
|
[template.blockListKey]: options.blockListJson,
|
|
@@ -2797,7 +2810,7 @@ async function finalizeStoppedAICard(card, options, log) {
|
|
|
2797
2810
|
await ensureFreshToken(card, log);
|
|
2798
2811
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2799
2812
|
const payload = buildStoppedCardFinalizePayload(options);
|
|
2800
|
-
await finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2813
|
+
await finalizeAICardStreamingLifecycleIfNeeded(card, log);
|
|
2801
2814
|
try {
|
|
2802
2815
|
await updateCardVariables(
|
|
2803
2816
|
card.outTrackId || card.cardInstanceId,
|
|
@@ -4475,7 +4488,7 @@ function stripLeadingInvisibleChars(value) {
|
|
|
4475
4488
|
|
|
4476
4489
|
// src/inbound-handler.ts
|
|
4477
4490
|
import fs6 from "node:fs";
|
|
4478
|
-
import * as
|
|
4491
|
+
import * as path10 from "node:path";
|
|
4479
4492
|
import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
4480
4493
|
import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
|
|
4481
4494
|
|
|
@@ -6716,7 +6729,7 @@ function isMarkdownTableSeparator(line) {
|
|
|
6716
6729
|
return false;
|
|
6717
6730
|
}
|
|
6718
6731
|
const cells = normalized.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
6719
|
-
return cells.length > 0 && cells.every((cell) => /^:?-{
|
|
6732
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{1,}:?$/.test(cell));
|
|
6720
6733
|
}
|
|
6721
6734
|
function isMarkdownTableRow(line) {
|
|
6722
6735
|
const trimmed = line.trim();
|
|
@@ -6725,9 +6738,58 @@ function isMarkdownTableRow(line) {
|
|
|
6725
6738
|
function parseMarkdownTableRow(line) {
|
|
6726
6739
|
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
6727
6740
|
}
|
|
6728
|
-
function
|
|
6729
|
-
const
|
|
6730
|
-
|
|
6741
|
+
function parseSeparatorAlignment(cell) {
|
|
6742
|
+
const trimmed = cell.trim();
|
|
6743
|
+
const hasLeftColon = trimmed.startsWith(":");
|
|
6744
|
+
const hasRightColon = trimmed.endsWith(":");
|
|
6745
|
+
if (hasLeftColon && hasRightColon) {
|
|
6746
|
+
return "center";
|
|
6747
|
+
}
|
|
6748
|
+
if (hasRightColon) {
|
|
6749
|
+
return "right";
|
|
6750
|
+
}
|
|
6751
|
+
if (hasLeftColon) {
|
|
6752
|
+
return "left";
|
|
6753
|
+
}
|
|
6754
|
+
return "center";
|
|
6755
|
+
}
|
|
6756
|
+
function buildSeparatorRow(alignments) {
|
|
6757
|
+
const cells = alignments.map((align) => {
|
|
6758
|
+
if (align === "left") {
|
|
6759
|
+
return ":---";
|
|
6760
|
+
}
|
|
6761
|
+
if (align === "right") {
|
|
6762
|
+
return "---:";
|
|
6763
|
+
}
|
|
6764
|
+
return ":---:";
|
|
6765
|
+
});
|
|
6766
|
+
return "|" + cells.join("|") + "|";
|
|
6767
|
+
}
|
|
6768
|
+
function renderMarkdownTable(headerLine, separatorLine, dataLines) {
|
|
6769
|
+
const headerCells = parseMarkdownTableRow(headerLine);
|
|
6770
|
+
const separatorCells = parseMarkdownTableRow(separatorLine);
|
|
6771
|
+
const dataRows = dataLines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
|
|
6772
|
+
if (headerCells.length === 0) {
|
|
6773
|
+
return "";
|
|
6774
|
+
}
|
|
6775
|
+
const colCount = Math.max(
|
|
6776
|
+
headerCells.length,
|
|
6777
|
+
separatorCells.length,
|
|
6778
|
+
...dataRows.map((cells) => cells.length)
|
|
6779
|
+
);
|
|
6780
|
+
const alignments = [];
|
|
6781
|
+
for (let i = 0; i < colCount; i++) {
|
|
6782
|
+
const sepCell = separatorCells[i] || "";
|
|
6783
|
+
alignments.push(parseSeparatorAlignment(sepCell));
|
|
6784
|
+
}
|
|
6785
|
+
const separator = buildSeparatorRow(alignments);
|
|
6786
|
+
const allRows = [headerCells, ...dataRows];
|
|
6787
|
+
const rendered = allRows.map((cells) => {
|
|
6788
|
+
const padded = cells.length < colCount ? [...cells, ...Array(colCount - cells.length).fill("")] : cells;
|
|
6789
|
+
return "|" + padded.join("|") + "|";
|
|
6790
|
+
});
|
|
6791
|
+
rendered.splice(1, 0, separator);
|
|
6792
|
+
return rendered.join("\n");
|
|
6731
6793
|
}
|
|
6732
6794
|
function convertMarkdownTablesToPlainText(text) {
|
|
6733
6795
|
const lines = text.split("\n");
|
|
@@ -6743,13 +6805,20 @@ function convertMarkdownTablesToPlainText(text) {
|
|
|
6743
6805
|
continue;
|
|
6744
6806
|
}
|
|
6745
6807
|
if (!inCodeFence && index + 1 < lines.length && isMarkdownTableRow(line) && isMarkdownTableSeparator(lines[index + 1] || "")) {
|
|
6746
|
-
const
|
|
6808
|
+
const headerLine = line;
|
|
6809
|
+
const separatorLine = lines[index + 1] || "";
|
|
6810
|
+
const dataLines = [];
|
|
6747
6811
|
index += 2;
|
|
6748
6812
|
while (index < lines.length && isMarkdownTableRow(lines[index] || "")) {
|
|
6749
|
-
|
|
6813
|
+
dataLines.push(lines[index] || "");
|
|
6750
6814
|
index += 1;
|
|
6751
6815
|
}
|
|
6752
|
-
|
|
6816
|
+
const renderedTable = renderMarkdownTable(headerLine, separatorLine, dataLines);
|
|
6817
|
+
const lastOutput = output[output.length - 1];
|
|
6818
|
+
if (lastOutput !== void 0 && lastOutput.trim() !== "") {
|
|
6819
|
+
output.push("");
|
|
6820
|
+
}
|
|
6821
|
+
output.push(renderedTable);
|
|
6753
6822
|
continue;
|
|
6754
6823
|
}
|
|
6755
6824
|
output.push(line);
|
|
@@ -7247,6 +7316,9 @@ function buildPersistedOutboundText(text, options) {
|
|
|
7247
7316
|
}
|
|
7248
7317
|
return text;
|
|
7249
7318
|
}
|
|
7319
|
+
function shouldRouteSessionMediaViaProactive(mediaType) {
|
|
7320
|
+
return mediaType === "voice" || mediaType === "video" || mediaType === "file";
|
|
7321
|
+
}
|
|
7250
7322
|
var DINGTALK_TEXT_CHUNK_LIMIT = 3800;
|
|
7251
7323
|
var CARD_MEDIA_CONTROLLER_ATTACH_WAIT_MS = 150;
|
|
7252
7324
|
var CARD_MEDIA_CONTROLLER_ATTACH_POLL_MS = 25;
|
|
@@ -7643,51 +7715,33 @@ async function sendBySession(config, sessionWebhook, text, options = {}) {
|
|
|
7643
7715
|
const token = await getAccessToken(config, options.log);
|
|
7644
7716
|
const log = options.log || getLogger();
|
|
7645
7717
|
if (options.mediaPath && options.mediaType) {
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
const durationMs = uploadedDurationMs ?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
|
|
7656
|
-
body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
|
|
7657
|
-
log?.debug?.(
|
|
7658
|
-
`[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`
|
|
7659
|
-
);
|
|
7660
|
-
} else if (options.mediaType === "video") {
|
|
7661
|
-
body = { msgtype: "video", video: { media_id: mediaId } };
|
|
7662
|
-
} else if (options.mediaType === "file") {
|
|
7663
|
-
body = { msgtype: "file", file: { media_id: mediaId } };
|
|
7664
|
-
}
|
|
7665
|
-
if (body) {
|
|
7666
|
-
const result = await http_client_default({
|
|
7667
|
-
url: sessionWebhook,
|
|
7668
|
-
method: "POST",
|
|
7669
|
-
data: body,
|
|
7670
|
-
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
7671
|
-
...getProxyBypassOption(config)
|
|
7672
|
-
});
|
|
7718
|
+
if (options.mediaType === "image") {
|
|
7719
|
+
const uploadResult = await uploadMedia2(config, options.mediaPath, options.mediaType, log, {
|
|
7720
|
+
mediaLocalRoots: options.mediaLocalRoots
|
|
7721
|
+
});
|
|
7722
|
+
if (uploadResult) {
|
|
7723
|
+
const imageMarkdown = ``;
|
|
7724
|
+
text = text ? `${text}
|
|
7725
|
+
|
|
7726
|
+
${imageMarkdown}` : imageMarkdown;
|
|
7673
7727
|
log?.debug?.(
|
|
7674
|
-
`[DingTalk] Session webhook
|
|
7728
|
+
`[DingTalk] Session webhook image will be delivered as markdown media reference mediaId=${uploadResult.mediaId}`
|
|
7675
7729
|
);
|
|
7676
|
-
|
|
7677
|
-
const
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
}
|
|
7683
|
-
return result.data;
|
|
7730
|
+
} else {
|
|
7731
|
+
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
|
|
7732
|
+
text = `${text}
|
|
7733
|
+
|
|
7734
|
+
\u{1F4CE} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
|
|
7735
|
+
log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
|
|
7684
7736
|
}
|
|
7685
7737
|
} else {
|
|
7686
7738
|
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
|
|
7687
7739
|
text = `${text}
|
|
7688
7740
|
|
|
7689
|
-
\u{1F4CE} \
|
|
7690
|
-
log?.warn?.(
|
|
7741
|
+
\u{1F4CE} \u5F53\u524D\u4F1A\u8BDD\u65E0\u6CD5\u76F4\u63A5\u53D1\u9001 ${options.mediaType}\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
|
|
7742
|
+
log?.warn?.(
|
|
7743
|
+
`[DingTalk] Session webhook does not support native ${options.mediaType} replies; falling back to text description`
|
|
7744
|
+
);
|
|
7691
7745
|
}
|
|
7692
7746
|
}
|
|
7693
7747
|
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
@@ -7733,6 +7787,48 @@ async function sendMessage(config, conversationId, text, options = {}) {
|
|
|
7733
7787
|
try {
|
|
7734
7788
|
const messageType = config.messageType || "markdown";
|
|
7735
7789
|
const log = options.log || getLogger();
|
|
7790
|
+
if (options.sessionWebhook && options.mediaPath && shouldRouteSessionMediaViaProactive(options.mediaType)) {
|
|
7791
|
+
log?.debug?.(
|
|
7792
|
+
`[DingTalk] Session webhook does not support ${options.mediaType} replies reliably; using proactive media API instead`
|
|
7793
|
+
);
|
|
7794
|
+
const proactiveMediaResult = await sendProactiveMedia(
|
|
7795
|
+
config,
|
|
7796
|
+
conversationId,
|
|
7797
|
+
options.mediaPath,
|
|
7798
|
+
options.mediaType,
|
|
7799
|
+
options
|
|
7800
|
+
);
|
|
7801
|
+
if (!proactiveMediaResult.ok) {
|
|
7802
|
+
log?.warn?.(
|
|
7803
|
+
`[DingTalk] Proactive ${options.mediaType} reply failed; falling back to session markdown: ` + (proactiveMediaResult.error || "unknown")
|
|
7804
|
+
);
|
|
7805
|
+
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
7806
|
+
const delivery2 = extractOutboundDeliveryMetadata(data);
|
|
7807
|
+
const messageId2 = delivery2.messageId || delivery2.processQueryKey || delivery2.outTrackId;
|
|
7808
|
+
const persistedText = buildPersistedOutboundText(text, options);
|
|
7809
|
+
persistOutboundMessageContext({
|
|
7810
|
+
storePath: options.storePath,
|
|
7811
|
+
accountId: options.accountId,
|
|
7812
|
+
conversationId: options.conversationId || conversationId,
|
|
7813
|
+
text: persistedText,
|
|
7814
|
+
messageType: "outbound-media",
|
|
7815
|
+
quotedRef: options.quotedRef,
|
|
7816
|
+
log,
|
|
7817
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
7818
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
7819
|
+
delivery: {
|
|
7820
|
+
...delivery2,
|
|
7821
|
+
kind: "session"
|
|
7822
|
+
}
|
|
7823
|
+
});
|
|
7824
|
+
return { ok: true, data, messageId: messageId2 };
|
|
7825
|
+
}
|
|
7826
|
+
return {
|
|
7827
|
+
ok: true,
|
|
7828
|
+
data: proactiveMediaResult.data,
|
|
7829
|
+
messageId: proactiveMediaResult.messageId
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7736
7832
|
if (messageType === "card" && options.card && !options.forceMarkdown) {
|
|
7737
7833
|
const card = options.card;
|
|
7738
7834
|
if (isCardInTerminalState(card.state)) {
|
|
@@ -7754,26 +7850,6 @@ async function sendMessage(config, conversationId, text, options = {}) {
|
|
|
7754
7850
|
};
|
|
7755
7851
|
}
|
|
7756
7852
|
}
|
|
7757
|
-
if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
|
|
7758
|
-
log?.debug?.(
|
|
7759
|
-
"[DingTalk] Session webhook does not support voice replies reliably; using proactive media API for this voice response"
|
|
7760
|
-
);
|
|
7761
|
-
const proactiveVoiceResult = await sendProactiveMedia(
|
|
7762
|
-
config,
|
|
7763
|
-
conversationId,
|
|
7764
|
-
options.mediaPath,
|
|
7765
|
-
options.mediaType,
|
|
7766
|
-
options
|
|
7767
|
-
);
|
|
7768
|
-
if (!proactiveVoiceResult.ok) {
|
|
7769
|
-
return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
|
|
7770
|
-
}
|
|
7771
|
-
return {
|
|
7772
|
-
ok: true,
|
|
7773
|
-
data: proactiveVoiceResult.data,
|
|
7774
|
-
messageId: proactiveVoiceResult.messageId
|
|
7775
|
-
};
|
|
7776
|
-
}
|
|
7777
7853
|
if (options.sessionWebhook) {
|
|
7778
7854
|
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
7779
7855
|
const delivery2 = extractOutboundDeliveryMetadata(data);
|
|
@@ -9061,8 +9137,6 @@ function createCardDraftController(params) {
|
|
|
9061
9137
|
if (stopped || failed) {
|
|
9062
9138
|
return;
|
|
9063
9139
|
}
|
|
9064
|
-
await contentLoop.flush();
|
|
9065
|
-
await contentLoop.waitForInFlight();
|
|
9066
9140
|
if (hasStreamingContent) {
|
|
9067
9141
|
await clearStreamingContentFromCard();
|
|
9068
9142
|
}
|
|
@@ -9265,6 +9339,7 @@ function createCardDraftController(params) {
|
|
|
9265
9339
|
discardCurrentAnswer();
|
|
9266
9340
|
} else {
|
|
9267
9341
|
sealCurrentAnswer();
|
|
9342
|
+
queueRender();
|
|
9268
9343
|
}
|
|
9269
9344
|
await beginBoundaryFlush();
|
|
9270
9345
|
return;
|
|
@@ -9482,7 +9557,6 @@ function createCardReplyStrategy(ctx) {
|
|
|
9482
9557
|
};
|
|
9483
9558
|
const { mode, usedDeprecatedCardRealTimeStream } = resolveCardStreamingMode(config);
|
|
9484
9559
|
const streamAnswerLive = mode === "answer" || mode === "all";
|
|
9485
|
-
const renderAnswerBlocksLive = mode === "all";
|
|
9486
9560
|
const streamThinkingLive = mode === "all";
|
|
9487
9561
|
let lifecycleState = "open";
|
|
9488
9562
|
const shouldAcceptAnswerSnapshot = () => lifecycleState === "open";
|
|
@@ -9616,7 +9690,10 @@ function createCardReplyStrategy(ctx) {
|
|
|
9616
9690
|
finalTextForFallback = normalized.answerText;
|
|
9617
9691
|
return;
|
|
9618
9692
|
}
|
|
9619
|
-
await controller.updateAnswer(normalized.answerText
|
|
9693
|
+
await controller.updateAnswer(normalized.answerText, {
|
|
9694
|
+
stream: streamAnswerLive,
|
|
9695
|
+
renderBlocks: !streamAnswerLive
|
|
9696
|
+
});
|
|
9620
9697
|
}
|
|
9621
9698
|
};
|
|
9622
9699
|
const rewriteLocalMarkdownImagesToPlaceholders = (text) => {
|
|
@@ -9652,7 +9729,8 @@ function createCardReplyStrategy(ctx) {
|
|
|
9652
9729
|
}
|
|
9653
9730
|
await controller.updateAnswer(answerSnapshot, {
|
|
9654
9731
|
stream: streamAnswerLive,
|
|
9655
|
-
|
|
9732
|
+
// Active answer previews live in the content field; blockList is committed at boundaries/finalize.
|
|
9733
|
+
renderBlocks: false
|
|
9656
9734
|
});
|
|
9657
9735
|
};
|
|
9658
9736
|
const applySplitTextToTimeline = async (text, options = {}) => {
|
|
@@ -9712,6 +9790,11 @@ function createCardReplyStrategy(ctx) {
|
|
|
9712
9790
|
// Card mode keeps runtime block streaming disabled, but still consumes
|
|
9713
9791
|
// reasoning blocks through explicit callbacks and delivery metadata.
|
|
9714
9792
|
disableBlockStreaming: ctx.disableBlockStreaming ?? true,
|
|
9793
|
+
// DingTalk card mode owns the visible reply surface. In group chats,
|
|
9794
|
+
// OpenClaw defaults source replies to message-tool-only; override that
|
|
9795
|
+
// so final replies are delivered into this card instead of spawning a
|
|
9796
|
+
// separate visible message/card via the message tool.
|
|
9797
|
+
sourceReplyDeliveryMode: "automatic",
|
|
9715
9798
|
onAssistantMessageStart: async () => {
|
|
9716
9799
|
if (isLifecycleSealed() || isStopRequested?.()) {
|
|
9717
9800
|
return;
|
|
@@ -9951,6 +10034,7 @@ function createCardReplyStrategy(ctx) {
|
|
|
9951
10034
|
}
|
|
9952
10035
|
try {
|
|
9953
10036
|
await flushPendingReasoning();
|
|
10037
|
+
await controller.clearStreamingContent?.();
|
|
9954
10038
|
await controller.flush();
|
|
9955
10039
|
await controller.waitForInFlight();
|
|
9956
10040
|
const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : void 0);
|
|
@@ -10066,6 +10150,7 @@ function createCardReplyStrategy(ctx) {
|
|
|
10066
10150
|
}
|
|
10067
10151
|
|
|
10068
10152
|
// src/reply-strategy-markdown.ts
|
|
10153
|
+
import path8 from "node:path";
|
|
10069
10154
|
var EMPTY_FINAL_FALLBACK_TEXT = "\u2705 Done";
|
|
10070
10155
|
function renderQuotedSegment(text) {
|
|
10071
10156
|
return text.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
|
|
@@ -10102,6 +10187,10 @@ function computeSharedPrefixTail(previous, next) {
|
|
|
10102
10187
|
const suffix = current.slice(sharedPrefixLength);
|
|
10103
10188
|
return suffix.trim() ? suffix : "";
|
|
10104
10189
|
}
|
|
10190
|
+
function renderMarkdownImage(mediaPath) {
|
|
10191
|
+
const filename = path8.basename(mediaPath) || "image";
|
|
10192
|
+
return ``;
|
|
10193
|
+
}
|
|
10105
10194
|
function createMarkdownReplyStrategy(ctx) {
|
|
10106
10195
|
let finalText;
|
|
10107
10196
|
let activeAnswerText = "";
|
|
@@ -10125,7 +10214,7 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10125
10214
|
}
|
|
10126
10215
|
sentVisibleContent = true;
|
|
10127
10216
|
};
|
|
10128
|
-
const
|
|
10217
|
+
const prepareAnswerSuffix = (text) => {
|
|
10129
10218
|
const current = typeof text === "string" ? text : "";
|
|
10130
10219
|
if (current.length > 0) {
|
|
10131
10220
|
activeAnswerText = current;
|
|
@@ -10133,37 +10222,118 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10133
10222
|
}
|
|
10134
10223
|
const suffix = computeIncrementalSuffix(lastSentAnswerText, current);
|
|
10135
10224
|
if (suffix) {
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10225
|
+
return {
|
|
10226
|
+
text: suffix,
|
|
10227
|
+
markSent: () => {
|
|
10228
|
+
lastSentAnswerText = current;
|
|
10229
|
+
}
|
|
10230
|
+
};
|
|
10139
10231
|
}
|
|
10140
10232
|
if (current.trim() && lastSentAnswerText && !current.startsWith(lastSentAnswerText)) {
|
|
10141
10233
|
const suffix2 = computeSharedPrefixTail(lastSentAnswerText, current);
|
|
10142
10234
|
ctx.log?.warn?.(
|
|
10143
10235
|
`[DingTalk][Markdown] answer prefix drift detected; falling back to shared-prefix tail prevLen=${lastSentAnswerText.length} currentLen=${current.length}`
|
|
10144
10236
|
);
|
|
10145
|
-
lastSentAnswerText = "";
|
|
10146
10237
|
if (suffix2) {
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10238
|
+
return {
|
|
10239
|
+
text: suffix2,
|
|
10240
|
+
markSent: () => {
|
|
10241
|
+
lastSentAnswerText = current;
|
|
10242
|
+
}
|
|
10243
|
+
};
|
|
10244
|
+
}
|
|
10245
|
+
return {
|
|
10246
|
+
text: current,
|
|
10247
|
+
markSent: () => {
|
|
10248
|
+
lastSentAnswerText = current;
|
|
10249
|
+
}
|
|
10250
|
+
};
|
|
10251
|
+
}
|
|
10252
|
+
return null;
|
|
10253
|
+
};
|
|
10254
|
+
const emitAnswerSuffix = async (text) => {
|
|
10255
|
+
const suffix = prepareAnswerSuffix(text);
|
|
10256
|
+
if (suffix) {
|
|
10257
|
+
await sendMarkdownSegment(suffix.text);
|
|
10258
|
+
suffix.markSent();
|
|
10259
|
+
}
|
|
10260
|
+
};
|
|
10261
|
+
const prepareMarkdownImageAttachments = async (mediaUrls) => {
|
|
10262
|
+
const imageMarkdown = [];
|
|
10263
|
+
const passthroughMediaUrls = [];
|
|
10264
|
+
const cleanups = [];
|
|
10265
|
+
for (const rawMediaUrl of mediaUrls) {
|
|
10266
|
+
const preparedMedia = await prepareMediaInput(
|
|
10267
|
+
rawMediaUrl,
|
|
10268
|
+
ctx.log,
|
|
10269
|
+
ctx.config.mediaUrlAllowlist
|
|
10270
|
+
);
|
|
10271
|
+
const actualMediaPath = preparedMedia.cleanup ? preparedMedia.path : resolveRelativePath(preparedMedia.path);
|
|
10272
|
+
const mediaType = resolveOutboundMediaType({
|
|
10273
|
+
mediaPath: actualMediaPath,
|
|
10274
|
+
asVoice: false
|
|
10275
|
+
});
|
|
10276
|
+
if (mediaType === "image") {
|
|
10277
|
+
imageMarkdown.push(renderMarkdownImage(actualMediaPath));
|
|
10278
|
+
if (preparedMedia.cleanup) {
|
|
10279
|
+
cleanups.push(preparedMedia.cleanup);
|
|
10280
|
+
}
|
|
10281
|
+
} else {
|
|
10282
|
+
await preparedMedia.cleanup?.();
|
|
10283
|
+
passthroughMediaUrls.push(rawMediaUrl);
|
|
10150
10284
|
}
|
|
10151
|
-
await sendMarkdownSegment(current);
|
|
10152
|
-
lastSentAnswerText = current;
|
|
10153
10285
|
}
|
|
10286
|
+
return { imageMarkdown, passthroughMediaUrls, cleanups };
|
|
10154
10287
|
};
|
|
10155
10288
|
return {
|
|
10156
10289
|
getReplyOptions() {
|
|
10157
10290
|
return {
|
|
10158
|
-
disableBlockStreaming: ctx.disableBlockStreaming === true
|
|
10291
|
+
disableBlockStreaming: ctx.disableBlockStreaming === true,
|
|
10292
|
+
// DingTalk markdown/sessionWebhook mode owns the visible reply surface.
|
|
10293
|
+
// Keep runtime final replies on this strategy even when group chats
|
|
10294
|
+
// default source replies to message-tool-only.
|
|
10295
|
+
sourceReplyDeliveryMode: "automatic"
|
|
10159
10296
|
};
|
|
10160
10297
|
},
|
|
10161
10298
|
async deliver(payload) {
|
|
10299
|
+
let answerTextSentWithImages = false;
|
|
10300
|
+
let toolTextSentWithImages = false;
|
|
10162
10301
|
if (payload.mediaUrls.length > 0) {
|
|
10163
|
-
|
|
10164
|
-
|
|
10302
|
+
const prepared = payload.audioAsVoice === true ? {
|
|
10303
|
+
imageMarkdown: [],
|
|
10304
|
+
passthroughMediaUrls: payload.mediaUrls,
|
|
10305
|
+
cleanups: []
|
|
10306
|
+
} : await prepareMarkdownImageAttachments(payload.mediaUrls);
|
|
10307
|
+
try {
|
|
10308
|
+
if (prepared.passthroughMediaUrls.length > 0) {
|
|
10309
|
+
await ctx.deliverMedia(prepared.passthroughMediaUrls, {
|
|
10310
|
+
audioAsVoice: payload.audioAsVoice
|
|
10311
|
+
});
|
|
10312
|
+
sentVisibleContent = true;
|
|
10313
|
+
}
|
|
10314
|
+
if (prepared.imageMarkdown.length > 0) {
|
|
10315
|
+
const answerSuffix = payload.kind === "block" || payload.kind === "final" ? prepareAnswerSuffix(payload.text) : typeof payload.text === "string" ? { text: renderQuotedSegment(payload.text), markSent: () => {
|
|
10316
|
+
} } : null;
|
|
10317
|
+
const markdownParts = [answerSuffix?.text || "", ...prepared.imageMarkdown].filter(
|
|
10318
|
+
(part) => part.trim().length > 0
|
|
10319
|
+
);
|
|
10320
|
+
if (markdownParts.length > 0) {
|
|
10321
|
+
await sendMarkdownSegment(markdownParts.join("\n\n"));
|
|
10322
|
+
answerSuffix?.markSent();
|
|
10323
|
+
}
|
|
10324
|
+
answerTextSentWithImages = payload.kind === "block" || payload.kind === "final";
|
|
10325
|
+
toolTextSentWithImages = payload.kind === "tool";
|
|
10326
|
+
}
|
|
10327
|
+
} finally {
|
|
10328
|
+
for (const cleanup of prepared.cleanups) {
|
|
10329
|
+
await cleanup();
|
|
10330
|
+
}
|
|
10331
|
+
}
|
|
10165
10332
|
}
|
|
10166
10333
|
if (payload.kind === "tool") {
|
|
10334
|
+
if (toolTextSentWithImages) {
|
|
10335
|
+
return;
|
|
10336
|
+
}
|
|
10167
10337
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
10168
10338
|
if (!text.trim()) {
|
|
10169
10339
|
return;
|
|
@@ -10171,7 +10341,7 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10171
10341
|
await sendMarkdownSegment(renderQuotedSegment(text));
|
|
10172
10342
|
return;
|
|
10173
10343
|
}
|
|
10174
|
-
if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string") {
|
|
10344
|
+
if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string" && !answerTextSentWithImages) {
|
|
10175
10345
|
await emitAnswerSuffix(payload.text);
|
|
10176
10346
|
}
|
|
10177
10347
|
},
|
|
@@ -10495,12 +10665,12 @@ async function dispatchSubAgents(params) {
|
|
|
10495
10665
|
|
|
10496
10666
|
// src/targeting/group-members-store.ts
|
|
10497
10667
|
import * as fs5 from "node:fs";
|
|
10498
|
-
import * as
|
|
10668
|
+
import * as path9 from "node:path";
|
|
10499
10669
|
var GROUP_MEMBERS_NAMESPACE = "members.group-roster";
|
|
10500
10670
|
function groupMembersFilePath(storePath, groupId) {
|
|
10501
|
-
const dir =
|
|
10671
|
+
const dir = path9.join(path9.dirname(storePath), "dingtalk-members");
|
|
10502
10672
|
const safeId = groupId.replace(/\+/g, "-").replace(/\//g, "_");
|
|
10503
|
-
return
|
|
10673
|
+
return path9.join(dir, `${safeId}.json`);
|
|
10504
10674
|
}
|
|
10505
10675
|
function readLegacyRoster(storePath, groupId) {
|
|
10506
10676
|
const filePath = groupMembersFilePath(storePath, groupId);
|
|
@@ -11581,7 +11751,7 @@ async function handleDingTalkMessage(params) {
|
|
|
11581
11751
|
if (!hasPathLikeShape) {
|
|
11582
11752
|
return void 0;
|
|
11583
11753
|
}
|
|
11584
|
-
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(
|
|
11754
|
+
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path10.extname(trimmed).toLowerCase()) ? trimmed : void 0;
|
|
11585
11755
|
}, extractSharedAudioAsVoice = function(payload, inlineReplyPayload) {
|
|
11586
11756
|
const richPayload = payload;
|
|
11587
11757
|
const sharedValue = parseBooleanLike(richPayload.audioAsVoice);
|
|
@@ -13025,14 +13195,14 @@ var RegistrationError = class extends Error {
|
|
|
13025
13195
|
function asString(value) {
|
|
13026
13196
|
return typeof value === "string" ? value : "";
|
|
13027
13197
|
}
|
|
13028
|
-
async function apiPost(
|
|
13029
|
-
const url = `${REGISTRATION_BASE_URL}${
|
|
13198
|
+
async function apiPost(path11, payload) {
|
|
13199
|
+
const url = `${REGISTRATION_BASE_URL}${path11}`;
|
|
13030
13200
|
const resp = await http_client_default.post(url, payload, { timeout: 15e3 });
|
|
13031
13201
|
const data = resp.data;
|
|
13032
13202
|
const errcode = data.errcode;
|
|
13033
13203
|
if (errcode !== void 0 && errcode !== 0) {
|
|
13034
13204
|
const errmsg = asString(data.errmsg) || "unknown error";
|
|
13035
|
-
throw new RegistrationError(`API error [${
|
|
13205
|
+
throw new RegistrationError(`API error [${path11}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`);
|
|
13036
13206
|
}
|
|
13037
13207
|
return data;
|
|
13038
13208
|
}
|