@jeik/dingtalk-connector 0.8.27 → 0.8.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.en.md +28 -35
- package/README.md +28 -43
- package/dist/background-work-Cxb4b-91.mjs +42 -0
- package/dist/card-content-cache-c4Ls96eJ.mjs +2 -0
- package/dist/chunk-upload-PwhogwfE.mjs +2 -0
- package/dist/{common-DE0gd0Jg.mjs → common-BZiVgd3A.mjs} +2 -2
- package/dist/{common-B_A78n8u.mjs → common-DgL28gCZ.mjs} +1 -1
- package/dist/{connection-fc6B4z4G.mjs → connection-CZ7ZoN5_.mjs} +313 -86
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-BkI1gKaC.mjs +2 -0
- package/dist/{gateway-methods--aKsyMs-.mjs → gateway-methods-kNX3qvqw.mjs} +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{message-handler-DQZiKx3i.mjs → message-handler-B1usz1wb.mjs} +238 -122
- package/dist/{messaging-WleKf54h.mjs → messaging-DVS3UKOp.mjs} +6 -2
- package/dist/{runtime-DzZo6Gvk.mjs → runtime-A6G7WAWn.mjs} +3 -3
- package/docs/RELEASE_NOTES_V0.8.28.md +56 -0
- package/package.json +2 -2
- package/src/core/connection.ts +465 -158
- package/src/core/message-handler.ts +173 -86
- package/src/reply-dispatcher.ts +234 -77
- package/src/services/messaging/card.ts +2 -0
- package/src/utils/background-work.ts +63 -0
- package/dist/card-content-cache-Dwd8HICP.mjs +0 -2
- package/dist/chunk-upload-DaLXXZH3.mjs +0 -2
- package/dist/gateway-methods-BQd4E0Sk.mjs +0 -2
- /package/dist/{card-content-cache-CYUzCaQJ.mjs → card-content-cache-bDQFz2u8.mjs} +0 -0
- /package/dist/{chunk-upload-BBQgGtcZ.mjs → chunk-upload-BE71drPg.mjs} +0 -0
- /package/dist/{game-xiyou-CAT0ilSj.mjs → game-xiyou-uwsv23AC.mjs} +0 -0
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
uploadAndReplaceFileMarkers
|
|
57
57
|
} from "../services/media/index.ts";
|
|
58
58
|
import { sendProactive, type AICardTarget } from "../services/messaging/index.ts";
|
|
59
|
-
import { createAICardForTarget, streamAICard, registerActiveCard, type AICardInstance } from "../services/messaging/card.ts";
|
|
59
|
+
import { createAICardForTarget, streamAICard, finishAICard, registerActiveCard, type AICardInstance } from "../services/messaging/card.ts";
|
|
60
60
|
import {
|
|
61
61
|
collectCardLookupIds,
|
|
62
62
|
lookupCardContent,
|
|
@@ -67,10 +67,18 @@ import { normalizeSlashCommand } from "../utils/session.ts";
|
|
|
67
67
|
import { getDingtalkRuntime } from "../runtime.ts";
|
|
68
68
|
import { dingtalkHttp } from '../utils/http-client.ts';
|
|
69
69
|
import { createLoggerFromConfig } from '../utils/index.ts';
|
|
70
|
+
import { trackBackgroundWork } from "../utils/background-work.ts";
|
|
70
71
|
import * as fs from 'fs';
|
|
71
72
|
import * as path from 'path';
|
|
72
73
|
import * as os from 'os';
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 单条会话队列任务最长执行时间。
|
|
77
|
+
* 503 重试 + 工具循环偶发挂死时,若不超时,后续同会话消息会永远堵在 previousTask 上,
|
|
78
|
+
* 表现:连发多条「网关没反应 / 不贴表情」(表情在排队 ACK 或 dequeued 后才贴)。
|
|
79
|
+
*/
|
|
80
|
+
const SESSION_QUEUE_TASK_TIMEOUT_MS = 3 * 60 * 1000; // 3 分钟
|
|
81
|
+
|
|
74
82
|
// ============ 常量 ============
|
|
75
83
|
|
|
76
84
|
const AI_CARD_TEMPLATE_ID = '02fcf2f4-5e02-4a85-b672-46d1f715543e.schema';
|
|
@@ -1645,6 +1653,10 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1645
1653
|
}
|
|
1646
1654
|
|
|
1647
1655
|
// ===== 使用 SDK 的 dispatchReplyFromConfig =====
|
|
1656
|
+
// 提升到 try 外:catch 需要 finish 即时建卡;replyOptions.onError 不存在(OpenClaw 不导出)
|
|
1657
|
+
let earlyCard: AICardInstance | undefined = params.preCreatedCard;
|
|
1658
|
+
let finalizeDispatchError: ((error: unknown, kind?: string) => Promise<void>) | undefined;
|
|
1659
|
+
|
|
1648
1660
|
try {
|
|
1649
1661
|
const core = getDingtalkRuntime();
|
|
1650
1662
|
|
|
@@ -1736,7 +1748,6 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1736
1748
|
const skipAICardForCard = !isDirect && (groupReplyModeForCard === 'text' || groupReplyModeForCard === 'markdown');
|
|
1737
1749
|
const streamingEnabledForCard = !skipAICardForCard && (config as any).streaming !== false;
|
|
1738
1750
|
|
|
1739
|
-
let earlyCard: AICardInstance | undefined = params.preCreatedCard;
|
|
1740
1751
|
if (!earlyCard && streamingEnabledForCard && !asyncMode) {
|
|
1741
1752
|
try {
|
|
1742
1753
|
const earlyTarget: AICardTarget = isDirect
|
|
@@ -1757,7 +1768,13 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1757
1768
|
}
|
|
1758
1769
|
|
|
1759
1770
|
// 创建 reply dispatcher,使用解析后的 agentId
|
|
1760
|
-
const {
|
|
1771
|
+
const {
|
|
1772
|
+
dispatcher,
|
|
1773
|
+
replyOptions,
|
|
1774
|
+
markDispatchIdle,
|
|
1775
|
+
getAsyncModeResponse,
|
|
1776
|
+
finalizeDispatchError: finalizeErr,
|
|
1777
|
+
} = createDingtalkReplyDispatcher({
|
|
1761
1778
|
cfg,
|
|
1762
1779
|
agentId: matchedAgentId, // ✅ 使用手动匹配的 agentId
|
|
1763
1780
|
runtime: runtime as RuntimeEnv,
|
|
@@ -1770,6 +1787,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1770
1787
|
asyncMode,
|
|
1771
1788
|
preCreatedCard: earlyCard,
|
|
1772
1789
|
});
|
|
1790
|
+
finalizeDispatchError = finalizeErr;
|
|
1773
1791
|
|
|
1774
1792
|
// ===== 注入当前 bot 的 clientId(用于 dws CLI --client-id 参数) =====
|
|
1775
1793
|
// 多 bot 场景下,AI 需要知道当前对话属于哪个 bot,以便在调用
|
|
@@ -1905,32 +1923,59 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1905
1923
|
|
|
1906
1924
|
} catch (err: any) {
|
|
1907
1925
|
log?.error?.(`SDK dispatch 失败: ${err.message}`);
|
|
1908
|
-
|
|
1909
|
-
// 解析上游错误文本(例如 503 No available channel),匹配中文提示文案
|
|
1910
|
-
const errorText = matchModelErrorText(err.message, { includeCatchAll: true }) ?? `⚠️ 抱歉,处理请求时出错: ${err.message}`;
|
|
1911
1926
|
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1927
|
+
// 统一错误定稿:优先走 dispatcher 的 finalizeDispatchError(finish AI Card + 中文提示)。
|
|
1928
|
+
// 注意:OpenClaw createReplyDispatcherWithTyping 返回的 replyOptions 不含 onError,
|
|
1929
|
+
// 绝不能依赖 replyOptions.onError(会永远 undefined / 甚至 catch 作用域 ReferenceError)。
|
|
1930
|
+
if (finalizeDispatchError) {
|
|
1914
1931
|
try {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1932
|
+
log?.info?.(`[DingTalk] 统一错误定稿:调用 finalizeDispatchError 处理同步异常`);
|
|
1933
|
+
await finalizeDispatchError(err, "invoke");
|
|
1934
|
+
} catch (finalizeErr: any) {
|
|
1935
|
+
log?.error?.(`[DingTalk] finalizeDispatchError 失败: ${finalizeErr?.message || finalizeErr}`);
|
|
1918
1936
|
}
|
|
1919
1937
|
} else {
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1938
|
+
// dispatcher 尚未创建就失败:直接 finish 即时建卡,再不行发纯文本
|
|
1939
|
+
const errorText =
|
|
1940
|
+
matchModelErrorText(err.message, { includeCatchAll: true }) ??
|
|
1941
|
+
`⚠️ 抱歉,处理请求时出错: ${err.message}`;
|
|
1942
|
+
let cardSuccess = false;
|
|
1943
|
+
if (earlyCard) {
|
|
1944
|
+
try {
|
|
1945
|
+
await finishAICard(
|
|
1946
|
+
earlyCard,
|
|
1947
|
+
errorText,
|
|
1948
|
+
config,
|
|
1949
|
+
log,
|
|
1950
|
+
undefined,
|
|
1951
|
+
undefined,
|
|
1952
|
+
data.conversationId,
|
|
1953
|
+
);
|
|
1954
|
+
cardSuccess = true;
|
|
1955
|
+
log?.info?.(`[DingTalk] 极端兜底:finishAICard 错误卡成功`);
|
|
1956
|
+
} catch (cardErr: any) {
|
|
1957
|
+
log?.error?.(`更新错误卡片失败: ${cardErr.message}`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (!cardSuccess) {
|
|
1961
|
+
log?.warn?.(`[DingTalk] 无可用错误定稿路径,走纯文本发送`);
|
|
1962
|
+
try {
|
|
1963
|
+
const token = await getAccessToken(config);
|
|
1964
|
+
const body: any = {
|
|
1965
|
+
msgtype: "text",
|
|
1966
|
+
text: { content: errorText },
|
|
1967
|
+
};
|
|
1968
|
+
if (!isDirect) body.at = { atUserIds: [senderId], isAtAll: false };
|
|
1969
|
+
|
|
1970
|
+
await dingtalkHttp.post(sessionWebhook, body, {
|
|
1971
|
+
headers: {
|
|
1972
|
+
"x-acs-dingtalk-access-token": token,
|
|
1973
|
+
"Content-Type": "application/json",
|
|
1974
|
+
},
|
|
1975
|
+
});
|
|
1976
|
+
} catch (fallbackErr: any) {
|
|
1977
|
+
log?.error?.(`错误消息发送也失败: ${fallbackErr.message}`);
|
|
1978
|
+
}
|
|
1934
1979
|
}
|
|
1935
1980
|
}
|
|
1936
1981
|
}
|
|
@@ -2007,89 +2052,131 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
2007
2052
|
// 更新会话活跃时间
|
|
2008
2053
|
sessionLastActivity.set(queueKey, Date.now());
|
|
2009
2054
|
|
|
2010
|
-
//
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
//
|
|
2055
|
+
// ═══════════════════════════════════════════════════════════
|
|
2056
|
+
// ⚠️ 关键:下面「读 previous → 建 currentTask → set」之间禁止任何 await。
|
|
2057
|
+
// JS 虽是单线程,但 await 会让出执行权;若在 set 之前 await 繁忙 ACK(最多 3s),
|
|
2058
|
+
// 并发第 2/3 条会看到「队列仍只有第 1 条」甚至「空闲」,表现为:
|
|
2059
|
+
// - 任务进行中连发却不弹排队卡(要发两三次才命中 isQueueBusy)
|
|
2060
|
+
// - 多条消息错误地 chain 到同一 previous,乱序/并行
|
|
2061
|
+
// ═══════════════════════════════════════════════════════════
|
|
2014
2062
|
const previousTask = sessionQueues.get(queueKey) || Promise.resolve();
|
|
2063
|
+
const isQueueBusy = sessionQueues.has(queueKey);
|
|
2015
2064
|
|
|
2016
|
-
//
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2065
|
+
// 异步 ACK 写回的卡片:主任务 dequeue 时再读,避免 await ACK 再入队
|
|
2066
|
+
const cardHolder: { card?: AICardInstance; ackDone: boolean } = {
|
|
2067
|
+
card: undefined,
|
|
2068
|
+
ackDone: !isQueueBusy,
|
|
2069
|
+
};
|
|
2021
2070
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2071
|
+
const currentTask = previousTask
|
|
2072
|
+
.then(async () => {
|
|
2073
|
+
log?.info?.(
|
|
2074
|
+
`[队列] 开始处理消息,queueKey=${queueKey} wasBusy=${isQueueBusy} timeoutMs=${SESSION_QUEUE_TASK_TIMEOUT_MS}`,
|
|
2075
|
+
);
|
|
2025
2076
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
fallbackToNormal: true,
|
|
2033
|
-
});
|
|
2034
|
-
log?.info?.(`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`);
|
|
2035
|
-
} catch (ackErr: any) {
|
|
2036
|
-
log?.warn?.(`[队列] 发送普通 ACK 失败: ${ackErr?.message || ackErr}`);
|
|
2077
|
+
// 若排队 ACK 仍在建卡,最多再等 2s 拿 preCreatedCard,避免丢复用
|
|
2078
|
+
if (isQueueBusy && !cardHolder.ackDone) {
|
|
2079
|
+
const waitStart = Date.now();
|
|
2080
|
+
while (!cardHolder.ackDone && Date.now() - waitStart < 2_000) {
|
|
2081
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2082
|
+
}
|
|
2037
2083
|
}
|
|
2038
|
-
// text/markdown 模式不贴🤔表情(表情是 AI Card 场景的配套功能)
|
|
2039
|
-
} else {
|
|
2040
|
-
// aicard 模式:创建 AI Card 显示排队 ACK
|
|
2041
|
-
const cardTarget: AICardTarget = isDirect
|
|
2042
|
-
? { type: 'user', userId: senderId }
|
|
2043
|
-
: { type: 'group', openConversationId: data.conversationId };
|
|
2044
2084
|
|
|
2085
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
2045
2086
|
try {
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2087
|
+
await Promise.race([
|
|
2088
|
+
handleDingTalkMessageInternal({
|
|
2089
|
+
...params,
|
|
2090
|
+
preCreatedCard: cardHolder.card,
|
|
2091
|
+
emotionAlreadyAdded: true,
|
|
2092
|
+
}),
|
|
2093
|
+
new Promise<never>((_, reject) => {
|
|
2094
|
+
timeoutId = setTimeout(() => {
|
|
2095
|
+
reject(
|
|
2096
|
+
new Error(
|
|
2097
|
+
`会话队列任务超时(${SESSION_QUEUE_TASK_TIMEOUT_MS}ms),释放队列以免堵死后续消息`,
|
|
2098
|
+
),
|
|
2099
|
+
);
|
|
2100
|
+
}, SESSION_QUEUE_TASK_TIMEOUT_MS);
|
|
2101
|
+
}),
|
|
2102
|
+
]);
|
|
2103
|
+
log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
|
|
2104
|
+
} finally {
|
|
2105
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
2061
2106
|
}
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// 创建当前消息的处理任务
|
|
2066
|
-
const currentTask = previousTask
|
|
2067
|
-
.then(async () => {
|
|
2068
|
-
log?.info?.(`[队列] 开始处理消息,queueKey=${queueKey}`);
|
|
2069
|
-
await handleDingTalkMessageInternal({ ...params, preCreatedCard, emotionAlreadyAdded: isQueueBusy });
|
|
2070
|
-
log?.info?.(`[队列] 消息处理完成,queueKey=${queueKey}`);
|
|
2071
2107
|
})
|
|
2072
2108
|
.catch((err: any) => {
|
|
2073
2109
|
log?.error?.(`[队列] 消息处理异常,queueKey=${queueKey}, error=${err.message}`);
|
|
2074
|
-
// 不抛出错误,避免阻塞后续消息
|
|
2075
2110
|
})
|
|
2076
2111
|
.finally(() => {
|
|
2077
|
-
// 如果当前任务是队列中的最后一个任务,清理队列
|
|
2078
2112
|
if (sessionQueues.get(queueKey) === currentTask) {
|
|
2079
2113
|
sessionQueues.delete(queueKey);
|
|
2080
2114
|
log?.info?.(`[队列] 队列已清空,queueKey=${queueKey}`);
|
|
2081
2115
|
}
|
|
2082
2116
|
});
|
|
2083
|
-
|
|
2084
|
-
//
|
|
2117
|
+
|
|
2118
|
+
// ★ 立刻占槽:必须在任何 await 之前,保证并发入站能看到 isQueueBusy
|
|
2085
2119
|
sessionQueues.set(queueKey, currentTask);
|
|
2120
|
+
trackBackgroundWork(currentTask, queueKey);
|
|
2121
|
+
log?.info?.(
|
|
2122
|
+
`[队列] 已登记 queueKey=${queueKey} busy=${isQueueBusy} depth≈pending`,
|
|
2123
|
+
);
|
|
2086
2124
|
|
|
2087
|
-
//
|
|
2088
|
-
|
|
2125
|
+
// ===== 登记之后再做用户反馈(可异步,不再阻塞占槽)=====
|
|
2126
|
+
addEmotionReply(config, data, log).catch((err: any) => {
|
|
2127
|
+
log?.warn?.(`[队列] 即时贴表情失败: ${err?.message || err}`);
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
if (isQueueBusy) {
|
|
2131
|
+
const ackPhrases = QUEUE_BUSY_ACK_PHRASES;
|
|
2132
|
+
const ackText = ackPhrases[Math.floor(Math.random() * ackPhrases.length)];
|
|
2133
|
+
const groupReplyMode = config.groupReplyMode || "aicard";
|
|
2134
|
+
const skipAICard =
|
|
2135
|
+
!isDirect && (groupReplyMode === "text" || groupReplyMode === "markdown");
|
|
2136
|
+
|
|
2137
|
+
// 火并忘记:不 await,避免拖慢本回调、也不挡住下一条消息的占槽
|
|
2138
|
+
void (async () => {
|
|
2139
|
+
try {
|
|
2140
|
+
if (skipAICard) {
|
|
2141
|
+
await sendProactive(
|
|
2142
|
+
config,
|
|
2143
|
+
{ openConversationId: data.conversationId },
|
|
2144
|
+
ackText,
|
|
2145
|
+
{ msgType: "text", useAICard: false, fallbackToNormal: true },
|
|
2146
|
+
);
|
|
2147
|
+
log?.info?.(
|
|
2148
|
+
`[队列] 队列繁忙,已发送普通文本 ACK(groupReplyMode=${groupReplyMode})`,
|
|
2149
|
+
);
|
|
2150
|
+
} else {
|
|
2151
|
+
const cardTarget: AICardTarget = isDirect
|
|
2152
|
+
? { type: "user", userId: senderId }
|
|
2153
|
+
: { type: "group", openConversationId: data.conversationId };
|
|
2154
|
+
const card = await createAICardForTarget(config, cardTarget, log);
|
|
2155
|
+
if (card) {
|
|
2156
|
+
await streamAICard(card, ackText, false, config, log);
|
|
2157
|
+
cardHolder.card = card;
|
|
2158
|
+
log?.info?.(
|
|
2159
|
+
`[队列] 队列繁忙,已创建排队 ACK Card,cardInstanceId=${card.cardInstanceId}`,
|
|
2160
|
+
);
|
|
2161
|
+
} else {
|
|
2162
|
+
log?.warn?.(`[队列] 创建排队 ACK Card 失败(返回 null)`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
} catch (ackErr: any) {
|
|
2166
|
+
log?.warn?.(`[队列] 繁忙 ACK 异常: ${ackErr?.message || ackErr}`);
|
|
2167
|
+
} finally {
|
|
2168
|
+
cardHolder.ackDone = true;
|
|
2169
|
+
}
|
|
2170
|
+
})();
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// 立即返回,不阻塞 WebSocket 收下一条
|
|
2089
2174
|
} catch (err: any) {
|
|
2090
2175
|
log?.error?.(`[队列] 队列管理异常,直接处理: ${err.message}`);
|
|
2091
|
-
|
|
2092
|
-
|
|
2176
|
+
const fallback = handleDingTalkMessageInternal(params).catch((e: any) => {
|
|
2177
|
+
log?.error?.(`[队列] 直接处理也失败: ${e.message}`);
|
|
2178
|
+
});
|
|
2179
|
+
trackBackgroundWork(fallback, "queue-fallback");
|
|
2093
2180
|
}
|
|
2094
2181
|
}
|
|
2095
2182
|
|