@jeik/dingtalk-connector 0.8.21

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +684 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +179 -0
  4. package/README.md +219 -0
  5. package/bin/dingtalk-connector.js +838 -0
  6. package/bin/wizard-config.mjs +94 -0
  7. package/dist/accounts-BAzdqkAV.mjs +268 -0
  8. package/dist/accounts-BQptOmgB.mjs +2 -0
  9. package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
  10. package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
  11. package/dist/common-C8pYKU_y.mjs +2 -0
  12. package/dist/common-Dt9n6fQN.mjs +101 -0
  13. package/dist/connection-DHHFFNQJ.mjs +423 -0
  14. package/dist/entry-bundled.d.mts +16 -0
  15. package/dist/entry-bundled.mjs +31 -0
  16. package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
  17. package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
  18. package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
  19. package/dist/http-client-CpnJHB89.mjs +2 -0
  20. package/dist/http-client-DFWZgO1n.mjs +33 -0
  21. package/dist/index.d.mts +193 -0
  22. package/dist/index.mjs +45 -0
  23. package/dist/logger-BmJkQkm1.mjs +2 -0
  24. package/dist/logger-mZ9OSbmD.mjs +58 -0
  25. package/dist/media-C_SVin7s.mjs +2 -0
  26. package/dist/media-cz72EVS3.mjs +509 -0
  27. package/dist/message-handler-DESzFFDc.mjs +1971 -0
  28. package/dist/messaging-B6l1sRvX.mjs +1044 -0
  29. package/dist/runtime-DUgpo5zC.mjs +1422 -0
  30. package/dist/session-DJ4jYqPv.mjs +114 -0
  31. package/dist/utils-Bjh4r_qS.mjs +4 -0
  32. package/dist/utils-CIfI_3Jh.mjs +63 -0
  33. package/dist/utils-legacy-CALCPP1t.mjs +230 -0
  34. package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
  35. package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
  36. package/docs/DEAP_AGENT_GUIDE.md +115 -0
  37. package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
  38. package/docs/MULTI_AGENT_SETUP.md +306 -0
  39. package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
  40. package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
  41. package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
  42. package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
  43. package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
  44. package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
  45. package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
  46. package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
  47. package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
  48. package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
  49. package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
  50. package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
  51. package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
  52. package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
  53. package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
  54. package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
  55. package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
  56. package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
  57. package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
  58. package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
  59. package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
  60. package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
  61. package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
  62. package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
  63. package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
  64. package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
  65. package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
  66. package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
  67. package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
  68. package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
  69. package/docs/TROUBLESHOOTING.md +122 -0
  70. package/index.ts +77 -0
  71. package/openclaw.plugin.json +551 -0
  72. package/package.json +147 -0
  73. package/skills/dingtalk-channel-rules/SKILL.md +91 -0
  74. package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
  75. package/skills/dws-cli/SKILL.md +129 -0
  76. package/skills/dws-cli/references/error-codes.md +95 -0
  77. package/skills/dws-cli/references/field-rules.md +105 -0
  78. package/skills/dws-cli/references/global-reference.md +104 -0
  79. package/skills/dws-cli/references/intent-guide.md +114 -0
  80. package/skills/dws-cli/references/products/aitable.md +452 -0
  81. package/skills/dws-cli/references/products/attendance.md +93 -0
  82. package/skills/dws-cli/references/products/calendar.md +217 -0
  83. package/skills/dws-cli/references/products/chat.md +292 -0
  84. package/skills/dws-cli/references/products/contact.md +108 -0
  85. package/skills/dws-cli/references/products/ding.md +57 -0
  86. package/skills/dws-cli/references/products/report.md +162 -0
  87. package/skills/dws-cli/references/products/simple.md +128 -0
  88. package/skills/dws-cli/references/products/todo.md +138 -0
  89. package/skills/dws-cli/references/products/workbench.md +39 -0
  90. package/skills/dws-cli/references/recovery-guide.md +94 -0
  91. package/src/channel.ts +588 -0
  92. package/src/config/accounts.ts +242 -0
  93. package/src/config/schema.ts +180 -0
  94. package/src/core/connection.ts +741 -0
  95. package/src/core/message-handler.ts +1788 -0
  96. package/src/core/provider.ts +111 -0
  97. package/src/core/state.ts +54 -0
  98. package/src/device-auth-config.ts +14 -0
  99. package/src/device-auth.ts +197 -0
  100. package/src/directory.ts +95 -0
  101. package/src/docs.ts +293 -0
  102. package/src/game-xiyou/achievement-engine.ts +252 -0
  103. package/src/game-xiyou/bounty-system.ts +315 -0
  104. package/src/game-xiyou/commands.ts +223 -0
  105. package/src/game-xiyou/drop-engine.ts +241 -0
  106. package/src/game-xiyou/encounter-system.ts +135 -0
  107. package/src/game-xiyou/escape-engine.ts +164 -0
  108. package/src/game-xiyou/exp-calculator.ts +139 -0
  109. package/src/game-xiyou/index.ts +479 -0
  110. package/src/game-xiyou/level-system.ts +91 -0
  111. package/src/game-xiyou/monster-pool.ts +180 -0
  112. package/src/game-xiyou/pity-counter.ts +114 -0
  113. package/src/game-xiyou/random-event-engine.ts +648 -0
  114. package/src/game-xiyou/renderer.ts +679 -0
  115. package/src/game-xiyou/storage.ts +218 -0
  116. package/src/game-xiyou/treasure-system.ts +105 -0
  117. package/src/game-xiyou/types.ts +582 -0
  118. package/src/game-xiyou/uid-resolver.ts +49 -0
  119. package/src/gateway-methods.ts +740 -0
  120. package/src/onboarding.ts +553 -0
  121. package/src/policy.ts +32 -0
  122. package/src/probe.ts +210 -0
  123. package/src/reply-dispatcher.ts +874 -0
  124. package/src/runtime.ts +32 -0
  125. package/src/sdk/helpers.ts +322 -0
  126. package/src/sdk/types.ts +519 -0
  127. package/src/secret-input.ts +19 -0
  128. package/src/services/media/audio.ts +54 -0
  129. package/src/services/media/chunk-upload.ts +296 -0
  130. package/src/services/media/common.ts +155 -0
  131. package/src/services/media/file.ts +75 -0
  132. package/src/services/media/image.ts +81 -0
  133. package/src/services/media/index.ts +10 -0
  134. package/src/services/media/video.ts +162 -0
  135. package/src/services/media.ts +1143 -0
  136. package/src/services/messaging/card.ts +604 -0
  137. package/src/services/messaging/index.ts +18 -0
  138. package/src/services/messaging/mentions.ts +267 -0
  139. package/src/services/messaging/send.ts +141 -0
  140. package/src/services/messaging.ts +1191 -0
  141. package/src/services/reply-markers.ts +55 -0
  142. package/src/targets.ts +45 -0
  143. package/src/types/index.ts +59 -0
  144. package/src/types/pdf-parse.d.ts +3 -0
  145. package/src/utils/agent.ts +63 -0
  146. package/src/utils/async.ts +51 -0
  147. package/src/utils/constants.ts +27 -0
  148. package/src/utils/http-client.ts +38 -0
  149. package/src/utils/index.ts +8 -0
  150. package/src/utils/logger.ts +78 -0
  151. package/src/utils/session.ts +147 -0
  152. package/src/utils/token.ts +93 -0
  153. package/src/utils/utils-legacy.ts +454 -0
  154. package/tsconfig.json +20 -0
@@ -0,0 +1,1044 @@
1
+ import { u as uploadMediaToDingTalk } from "./media-cz72EVS3.mjs";
2
+ import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
3
+ import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
4
+ import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
5
+ import { r as MEDIA_MSG_TYPES } from "./session-DJ4jYqPv.mjs";
6
+ //#region src/services/messaging/card.ts
7
+ const _activeCardRegistry = /* @__PURE__ */ new Map();
8
+ function registerActiveCard(openConversationId, card) {
9
+ _activeCardRegistry.set(openConversationId, card);
10
+ }
11
+ function unregisterActiveCard(openConversationId) {
12
+ _activeCardRegistry.delete(openConversationId);
13
+ }
14
+ function getActiveCardForConversation(openConversationId) {
15
+ return _activeCardRegistry.get(openConversationId) ?? null;
16
+ }
17
+ const DEFAULT_CARD_TEMPLATE_ID = "02fcf2f4-5e02-4a85-b672-46d1f715543e.schema";
18
+ const DEFAULT_CARD_CONTENT_VAR = "msgContent";
19
+ /**
20
+ * 钉钉卡片 API 的最大 QPS(官方限制约 40 次/秒)。
21
+ * 保守取 20,为 createAICardForTarget / finishAICard 等非流式调用留余量。
22
+ */
23
+ const CARD_API_MAX_QPS = 20;
24
+ /** QPS 限流退避时长(ms),遇到 403 QpsLimit 后暂停发送 */
25
+ const QPS_BACKOFF_DURATION_MS = 2e3;
26
+ /**
27
+ * 全局令牌桶限流器,所有 streamAICard 调用共享。
28
+ *
29
+ * 解决的问题:每个 reply-dispatcher 实例有独立的 500ms 节流间隔,
30
+ * 但多个会话并发时总 QPS 会叠加超过钉钉 API 限制(40 次/秒),
31
+ * 导致频繁触发 403 QpsLimit 错误。
32
+ *
33
+ * 工作原理:
34
+ * - 令牌桶以 CARD_API_MAX_QPS 的速率补充令牌
35
+ * - 每次 API 调用前消耗一个令牌,无令牌时等待
36
+ * - 遇到 QpsLimit 错误时触发退避,暂停所有调用
37
+ */
38
+ const cardRateLimiter = {
39
+ /** 当前可用令牌数 */
40
+ tokens: CARD_API_MAX_QPS,
41
+ /** 上次令牌补充时间 */
42
+ lastRefillTime: Date.now(),
43
+ /** QPS 退避截止时间(遇到限流错误后设置) */
44
+ backoffUntil: 0,
45
+ /**
46
+ * 串行化锁:保证并发的 waitForToken 被一个一个处理。
47
+ * 否则多个并发调用会同时通过 `tokens < 1` 检查并各自扣减,
48
+ * 令牌桶会被并发击穿,导致实际 QPS 远超 CARD_API_MAX_QPS。
49
+ */
50
+ _queueTail: Promise.resolve(),
51
+ /**
52
+ * 补充令牌:按时间流逝恢复令牌数
53
+ */
54
+ refill() {
55
+ const now = Date.now();
56
+ const elapsedSeconds = (now - this.lastRefillTime) / 1e3;
57
+ if (elapsedSeconds > 0) {
58
+ this.tokens = Math.min(CARD_API_MAX_QPS, this.tokens + elapsedSeconds * CARD_API_MAX_QPS);
59
+ this.lastRefillTime = now;
60
+ }
61
+ },
62
+ /**
63
+ * 等待直到有可用令牌,或退避期结束
64
+ * @returns 等待的毫秒数(0 表示无需等待)
65
+ *
66
+ * 通过 `_queueTail` 将所有并发调用串行化,确保 token 扣减真正生效。
67
+ */
68
+ async waitForToken() {
69
+ const prev = this._queueTail;
70
+ let release;
71
+ this._queueTail = new Promise((resolve) => {
72
+ release = resolve;
73
+ });
74
+ try {
75
+ await prev;
76
+ } catch {}
77
+ try {
78
+ let totalWaitMs = 0;
79
+ const now = Date.now();
80
+ if (now < this.backoffUntil) {
81
+ const backoffWaitMs = this.backoffUntil - now;
82
+ await sleep(backoffWaitMs);
83
+ totalWaitMs += backoffWaitMs;
84
+ }
85
+ this.refill();
86
+ if (this.tokens < 1) {
87
+ const waitMs = Math.ceil((1 - this.tokens) / CARD_API_MAX_QPS * 1e3);
88
+ await sleep(waitMs);
89
+ totalWaitMs += waitMs;
90
+ this.refill();
91
+ }
92
+ this.tokens -= 1;
93
+ return totalWaitMs;
94
+ } finally {
95
+ release();
96
+ }
97
+ },
98
+ /**
99
+ * 触发退避:遇到 QpsLimit 错误时调用
100
+ */
101
+ triggerBackoff() {
102
+ const backoffEnd = Date.now() + QPS_BACKOFF_DURATION_MS;
103
+ this.backoffUntil = backoffEnd;
104
+ this.tokens = 0;
105
+ this.lastRefillTime = backoffEnd;
106
+ }
107
+ };
108
+ /** 简单的 sleep 工具函数 */
109
+ function sleep(ms) {
110
+ return new Promise((resolve) => setTimeout(resolve, ms));
111
+ }
112
+ /**
113
+ * 判断错误是否为钉钉 QPS 限流错误。
114
+ *
115
+ * 导出给上层调用(如 reply-dispatcher),用于在错误处理时区分
116
+ * 「瞬时可恢复错误」与「真正的发送失败」,避免把 QPS 限流这种
117
+ * 内部已自动退避重试、后续会自动恢复的错误展示为用户可见的
118
+ * 「消息发送失败」提示。
119
+ */
120
+ function isQpsLimitError(err) {
121
+ const errorCode = err?.response?.data?.code;
122
+ return err?.response?.status === 403 && typeof errorCode === "string" && errorCode.includes("QpsLimit");
123
+ }
124
+ /** AI Card 状态 */
125
+ const AICardStatus = {
126
+ PROCESSING: "1",
127
+ INPUTING: "2",
128
+ FINISHED: "3",
129
+ EXECUTING: "4",
130
+ FAILED: "5"
131
+ };
132
+ /**
133
+ * 确保 Markdown 表格前有空行,否则钉钉无法正确渲染表格
134
+ */
135
+ function ensureTableBlankLines(text) {
136
+ const lines = text.split("\n");
137
+ const result = [];
138
+ const tableDividerRegex = /^\s*\|?\s*:?-+:?\s*(\|?\s*:?-+:?\s*)+\|?\s*$/;
139
+ const tableRowRegex = /^\s*\|?.*\|.*\|?\s*$/;
140
+ const isDivider = (line) => line && typeof line === "string" && line.includes("|") && tableDividerRegex.test(line);
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const currentLine = lines[i];
143
+ const nextLine = lines[i + 1] ?? "";
144
+ if (tableRowRegex.test(currentLine) && isDivider(nextLine) && i > 0 && lines[i - 1].trim() !== "" && !tableRowRegex.test(lines[i - 1])) result.push("");
145
+ result.push(currentLine);
146
+ }
147
+ return result.join("\n");
148
+ }
149
+ /**
150
+ * 构建卡片投放请求体
151
+ */
152
+ function buildDeliverBody(cardInstanceId, target, robotCode) {
153
+ const base = {
154
+ outTrackId: cardInstanceId,
155
+ userIdType: 1
156
+ };
157
+ if (target.type === "group") return {
158
+ ...base,
159
+ openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`,
160
+ imGroupOpenDeliverModel: { robotCode }
161
+ };
162
+ return {
163
+ ...base,
164
+ openSpaceId: `dtv1.card//IM_ROBOT.${target.userId}`,
165
+ imRobotOpenDeliverModel: {
166
+ spaceType: "IM_ROBOT",
167
+ robotCode,
168
+ extension: { dynamicSummary: "true" }
169
+ }
170
+ };
171
+ }
172
+ /**
173
+ * 通用 AI Card 创建函数
174
+ */
175
+ async function createAICardForTarget(config, target, log) {
176
+ const targetDesc = target.type === "group" ? `群聊 ${target.openConversationId}` : `用户 ${target.userId}`;
177
+ const cardTemplateId = config.cardTemplateId || DEFAULT_CARD_TEMPLATE_ID;
178
+ try {
179
+ const token = await getAccessToken(config);
180
+ const cardInstanceId = `card_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
181
+ log?.info?.(`[DingTalk][AICard] 开始创建卡片:${targetDesc}, outTrackId=${cardInstanceId}, templateId=${cardTemplateId}`);
182
+ const createBody = {
183
+ cardTemplateId,
184
+ outTrackId: cardInstanceId,
185
+ cardData: { cardParamMap: { config: JSON.stringify({ autoLayout: true }) } },
186
+ callbackType: "STREAM",
187
+ imGroupOpenSpaceModel: { supportForward: true },
188
+ imRobotOpenSpaceModel: { supportForward: true }
189
+ };
190
+ await dingtalkHttp.post(`${DINGTALK_API}/v1.0/card/instances`, createBody, { headers: {
191
+ "x-acs-dingtalk-access-token": token,
192
+ "Content-Type": "application/json"
193
+ } });
194
+ const deliverBody = buildDeliverBody(cardInstanceId, target, String(config.clientId ?? ""));
195
+ await dingtalkHttp.post(`${DINGTALK_API}/v1.0/card/instances/deliver`, deliverBody, { headers: {
196
+ "x-acs-dingtalk-access-token": token,
197
+ "Content-Type": "application/json"
198
+ } });
199
+ return {
200
+ cardInstanceId,
201
+ accessToken: token,
202
+ tokenExpireTime: Date.now() + 7200 * 1e3,
203
+ inputingStarted: false
204
+ };
205
+ } catch (err) {
206
+ log?.error?.(`[DingTalk][AICard] 创建卡片失败 (${targetDesc}): ${err.message}`);
207
+ if (err.response) log?.error?.(`[DingTalk][AICard] 错误响应:status=${err.response.status}`);
208
+ return null;
209
+ }
210
+ }
211
+ /**
212
+ * 确保 Token 有效(自动刷新过期的 Token)
213
+ */
214
+ async function ensureValidToken(card, config) {
215
+ if (Date.now() > card.tokenExpireTime - 300 * 1e3) {
216
+ card.accessToken = await getAccessToken(config);
217
+ card.tokenExpireTime = Date.now() + 7200 * 1e3;
218
+ }
219
+ return card.accessToken;
220
+ }
221
+ /**
222
+ * 流式更新 AI Card 内容
223
+ *
224
+ * 内置全局令牌桶限流:所有会话共享同一速率限制,
225
+ * 遇到 QpsLimit 错误时自动退避 2 秒后重试一次。
226
+ */
227
+ async function streamAICard(card, content, finished = false, config, log, contentVar) {
228
+ const hadMarker = content.includes("[-process-]") || content.includes("[-final-]");
229
+ let finalContent = content;
230
+ if (hadMarker) {
231
+ const i = content.lastIndexOf("[-final-]");
232
+ finalContent = i >= 0 ? content.slice(i + 9) : content;
233
+ finalContent = finalContent.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
234
+ log?.info?.(`[DingTalk][marker] ${finished ? "finishAICard" : "streamAICard"} 检测到标记,已剥离(${content.length}→${finalContent.length} 字)`);
235
+ }
236
+ content = finalContent;
237
+ const varName = contentVar || config?.cardProcessVar || config?.cardContentVar || DEFAULT_CARD_CONTENT_VAR;
238
+ if (!card) {
239
+ log?.warn?.(`[DingTalk][AICard] streamAICard 收到 null card,跳过更新`);
240
+ return;
241
+ }
242
+ if (config) await ensureValidToken(card, config);
243
+ if (!card.inputingStarted) {
244
+ const inputingWaitMs = await cardRateLimiter.waitForToken();
245
+ if (inputingWaitMs > 0) log?.debug?.(`[DingTalk][AICard] INPUTING 等待限流令牌 ${inputingWaitMs}ms`);
246
+ const statusBody = {
247
+ outTrackId: card.cardInstanceId,
248
+ cardData: { cardParamMap: {
249
+ flowStatus: AICardStatus.INPUTING,
250
+ [varName]: content,
251
+ staticMsgContent: "",
252
+ sys_full_json_obj: JSON.stringify({ order: [varName] }),
253
+ config: JSON.stringify({ autoLayout: true })
254
+ } }
255
+ };
256
+ const putInputing = () => dingtalkHttp.put(`${DINGTALK_API}/v1.0/card/instances`, statusBody, { headers: {
257
+ "x-acs-dingtalk-access-token": card.accessToken,
258
+ "Content-Type": "application/json"
259
+ } });
260
+ try {
261
+ const statusResp = await putInputing();
262
+ log?.info?.(`[DingTalk][AICard] INPUTING 响应:status=${statusResp.status}`);
263
+ } catch (err) {
264
+ if (isQpsLimitError(err)) {
265
+ cardRateLimiter.triggerBackoff();
266
+ log?.warn?.(`[DingTalk][AICard] INPUTING 触发 QPS 限流,退避 ${QPS_BACKOFF_DURATION_MS}ms 后重试`);
267
+ await cardRateLimiter.waitForToken();
268
+ try {
269
+ const retryResp = await putInputing();
270
+ log?.info?.(`[DingTalk][AICard] INPUTING 重试成功:status=${retryResp.status}`);
271
+ } catch (retryErr) {
272
+ log?.error?.(`[DingTalk][AICard] INPUTING 重试失败:${retryErr.message}`);
273
+ throw retryErr;
274
+ }
275
+ } else {
276
+ log?.error?.(`[DingTalk][AICard] INPUTING 切换失败:${err.message}`);
277
+ throw err;
278
+ }
279
+ }
280
+ card.inputingStarted = true;
281
+ }
282
+ const fixedContent = ensureTableBlankLines(content);
283
+ const body = {
284
+ outTrackId: card.cardInstanceId,
285
+ guid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
286
+ key: varName,
287
+ content: fixedContent,
288
+ isFull: true,
289
+ isFinalize: finished,
290
+ isError: false
291
+ };
292
+ const streamWaitMs = await cardRateLimiter.waitForToken();
293
+ if (streamWaitMs > 0) log?.debug?.(`[DingTalk][AICard] streaming 等待限流令牌 ${streamWaitMs}ms`);
294
+ log?.info?.(`[DingTalk][AICard] PUT /v1.0/card/streaming contentLen=${content.length} isFinalize=${finished}`);
295
+ try {
296
+ const streamResp = await dingtalkHttp.put(`${DINGTALK_API}/v1.0/card/streaming`, body, { headers: {
297
+ "x-acs-dingtalk-access-token": card.accessToken,
298
+ "Content-Type": "application/json"
299
+ } });
300
+ log?.info?.(`[DingTalk][AICard] streaming 响应:status=${streamResp.status}`);
301
+ } catch (err) {
302
+ if (isQpsLimitError(err)) {
303
+ cardRateLimiter.triggerBackoff();
304
+ log?.warn?.(`[DingTalk][AICard] streaming 触发 QPS 限流,退避 ${QPS_BACKOFF_DURATION_MS}ms 后重试`);
305
+ await cardRateLimiter.waitForToken();
306
+ try {
307
+ body.guid = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
308
+ await dingtalkHttp.put(`${DINGTALK_API}/v1.0/card/streaming`, body, { headers: {
309
+ "x-acs-dingtalk-access-token": card.accessToken,
310
+ "Content-Type": "application/json"
311
+ } });
312
+ log?.info?.(`[DingTalk][AICard] streaming 重试成功`);
313
+ return;
314
+ } catch (retryErr) {
315
+ log?.error?.(`[DingTalk][AICard] streaming 重试失败:${retryErr.message}`);
316
+ throw retryErr;
317
+ }
318
+ }
319
+ throw err;
320
+ }
321
+ }
322
+ /**
323
+ * 完成 AI Card
324
+ */
325
+ async function finishAICard(card, content, config, log, contentVar) {
326
+ const varName = contentVar || config?.cardContentVar || DEFAULT_CARD_CONTENT_VAR;
327
+ if (config) await ensureValidToken(card, config);
328
+ const fixedContent = ensureTableBlankLines(content);
329
+ log?.info?.(`[DingTalk][AICard] 开始 finish,最终内容长度=${fixedContent.length}`);
330
+ await streamAICard(card, fixedContent, true, config, log);
331
+ const body = {
332
+ outTrackId: card.cardInstanceId,
333
+ cardData: { cardParamMap: {
334
+ flowStatus: AICardStatus.FINISHED,
335
+ [varName]: fixedContent,
336
+ staticMsgContent: "",
337
+ sys_full_json_obj: JSON.stringify({ order: [varName] }),
338
+ config: JSON.stringify({ autoLayout: true })
339
+ } },
340
+ cardUpdateOptions: { updateCardDataByKey: true }
341
+ };
342
+ const putFinished = () => dingtalkHttp.put(`${DINGTALK_API}/v1.0/card/instances`, body, { headers: {
343
+ "x-acs-dingtalk-access-token": card.accessToken,
344
+ "Content-Type": "application/json"
345
+ } });
346
+ try {
347
+ await cardRateLimiter.waitForToken();
348
+ const finishResp = await putFinished();
349
+ log?.info?.(`[DingTalk][AICard] FINISHED 响应:status=${finishResp.status}`);
350
+ } catch (err) {
351
+ if (isQpsLimitError(err)) {
352
+ cardRateLimiter.triggerBackoff();
353
+ log?.warn?.(`[DingTalk][AICard] FINISHED 触发 QPS 限流,退避 ${QPS_BACKOFF_DURATION_MS}ms 后重试`);
354
+ try {
355
+ await cardRateLimiter.waitForToken();
356
+ const retryResp = await putFinished();
357
+ log?.info?.(`[DingTalk][AICard] FINISHED 重试成功:status=${retryResp.status}`);
358
+ return;
359
+ } catch (retryErr) {
360
+ log?.error?.(`[DingTalk][AICard] FINISHED 重试失败:${retryErr.message}`);
361
+ }
362
+ } else log?.error?.(`[DingTalk][AICard] FINISHED 更新失败:${err.message}`);
363
+ }
364
+ }
365
+ //#endregion
366
+ //#region src/services/messaging/mentions.ts
367
+ /**
368
+ * 从全局 cfg 里构建「bot 别名 → chatbotUserId」的解析表。
369
+ *
370
+ * 会同时扫描:
371
+ * - `channels.dingtalk-connector.accounts.*`:accountId + name + chatbotUserId
372
+ * - `bindings[]`:根据 `match.accountId` 反查 agentId
373
+ */
374
+ function buildBotMentionTable(cfg, options = {}) {
375
+ const accountsMap = (cfg?.channels?.["dingtalk-connector"])?.accounts || {};
376
+ const byAccountId = /* @__PURE__ */ new Map();
377
+ for (const [accountId, acct] of Object.entries(accountsMap)) {
378
+ if (!acct) continue;
379
+ byAccountId.set(accountId, {
380
+ accountId,
381
+ chatbotUserId: acct.chatbotUserId?.trim?.() || void 0,
382
+ name: acct.name?.trim?.() || void 0,
383
+ agentIds: [],
384
+ aliases: []
385
+ });
386
+ }
387
+ const bindings = cfg?.bindings;
388
+ if (Array.isArray(bindings)) for (const b of bindings) {
389
+ const match = b?.match;
390
+ if (!match) continue;
391
+ if (match.channel && match.channel !== "dingtalk-connector") continue;
392
+ const accountId = match.accountId;
393
+ const agentId = b.agentId;
394
+ if (typeof accountId !== "string" || typeof agentId !== "string") continue;
395
+ const entry = byAccountId.get(accountId);
396
+ if (!entry) continue;
397
+ if (!entry.agentIds.includes(agentId)) entry.agentIds.push(agentId);
398
+ }
399
+ const extraMap = /* @__PURE__ */ new Map();
400
+ if (options.extraAliases) {
401
+ for (const [alias, accountId] of Object.entries(options.extraAliases)) if (alias && accountId) extraMap.set(alias.toLowerCase(), accountId);
402
+ }
403
+ for (const entry of byAccountId.values()) {
404
+ const aliasSet = /* @__PURE__ */ new Set();
405
+ aliasSet.add(entry.accountId);
406
+ if (entry.name) aliasSet.add(entry.name);
407
+ for (const aid of entry.agentIds) aliasSet.add(aid);
408
+ for (const [alias, accountId] of extraMap.entries()) if (accountId === entry.accountId) aliasSet.add(alias);
409
+ entry.aliases = Array.from(aliasSet);
410
+ }
411
+ return Array.from(byAccountId.values());
412
+ }
413
+ /** chatbotUserId 加密 ID 的正则(用于检测文本里已经写成加密形式的 @) */
414
+ const CHATBOT_ID_PATTERN = /\$:LWCP_v1:\$[A-Za-z0-9+/=]+/g;
415
+ /**
416
+ * 把一批 accountId 解析成对应的 chatbotUserId 数组。
417
+ * 找不到 chatbotUserId 的账号会被跳过,并通过 `missing` 报告,方便上层 log 警告。
418
+ */
419
+ function resolveAtAccountIdsToChatbotUserIds(cfg, atAccountIds) {
420
+ if (!atAccountIds || atAccountIds.length === 0) return {
421
+ resolved: [],
422
+ missing: []
423
+ };
424
+ const table = buildBotMentionTable(cfg);
425
+ const byAccountId = new Map(table.map((e) => [e.accountId, e]));
426
+ const resolved = [];
427
+ const missing = [];
428
+ for (const id of atAccountIds) {
429
+ if (!id) continue;
430
+ const entry = byAccountId.get(id);
431
+ if (entry?.chatbotUserId) resolved.push(entry.chatbotUserId);
432
+ else missing.push(id);
433
+ }
434
+ return {
435
+ resolved,
436
+ missing
437
+ };
438
+ }
439
+ /**
440
+ * 对文本中的 @ 别名做自动替换:
441
+ * 1. `@<alias>` → `@<chatbotUserId>`(alias 命中某个 bot 时)
442
+ * 2. 已经是 `@$:LWCP_v1:$xxx` 形式的 @ 原样保留
443
+ *
444
+ * 返回:
445
+ * - `text`:替换后的文本
446
+ * - `injectedChatbotUserIds`:本次替换中涉及到的 chatbotUserId 列表(调用方可合并到 atDingtalkIds)
447
+ */
448
+ function substituteBotMentions(text, cfg, options = {}) {
449
+ if (!text || typeof text !== "string") return {
450
+ text: text ?? "",
451
+ injectedChatbotUserIds: []
452
+ };
453
+ const table = buildBotMentionTable(cfg, options);
454
+ const aliasToChatbotUserId = /* @__PURE__ */ new Map();
455
+ for (const entry of table) {
456
+ if (!entry.chatbotUserId) continue;
457
+ for (const alias of entry.aliases) {
458
+ const key = alias.toLowerCase();
459
+ if (!aliasToChatbotUserId.has(key)) aliasToChatbotUserId.set(key, entry.chatbotUserId);
460
+ }
461
+ }
462
+ if (aliasToChatbotUserId.size === 0) return {
463
+ text,
464
+ injectedChatbotUserIds: []
465
+ };
466
+ const aliases = Array.from(aliasToChatbotUserId.keys()).sort((a, b) => b.length - a.length);
467
+ const injected = /* @__PURE__ */ new Set();
468
+ let out = text;
469
+ for (const alias of aliases) {
470
+ const chatbotUserId = aliasToChatbotUserId.get(alias);
471
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
472
+ const pattern = new RegExp(`@(${escaped})(?![A-Za-z0-9_\\u4e00-\\u9fff\\-])`, "gi");
473
+ out = out.replace(pattern, (match, _matched, offset) => {
474
+ if (out.slice(Math.max(0, offset - 1), offset) === "$") return match;
475
+ injected.add(chatbotUserId);
476
+ return `@${chatbotUserId}`;
477
+ });
478
+ }
479
+ if (options.detectBareAliases) for (const alias of aliases) {
480
+ const chatbotUserId = aliasToChatbotUserId.get(alias);
481
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
482
+ if (new RegExp(`(?<![@A-Za-z0-9_\\u4e00-\\u9fff\\-])(${escaped})(?![A-Za-z0-9_\\u4e00-\\u9fff\\-])`, "gi").test(out)) injected.add(chatbotUserId);
483
+ }
484
+ const rawIds = out.match(CHATBOT_ID_PATTERN) || [];
485
+ for (const id of rawIds) injected.add(id);
486
+ return {
487
+ text: out,
488
+ injectedChatbotUserIds: Array.from(injected)
489
+ };
490
+ }
491
+ /**
492
+ * 高层入口:同时处理显式 `atAccountIds` 与文本里的自然语言 @。
493
+ *
494
+ * 用于 `dingtalk-connector.send*` 系列 Gateway 方法,在调 `sendProactive` 前把最终
495
+ * 的 `content / atDingtalkIds` 准备好。
496
+ */
497
+ function prepareMultiBotMentions(params) {
498
+ const { cfg, content, atAccountIds, atDingtalkIds = [], extraAliases } = params;
499
+ const explicit = resolveAtAccountIdsToChatbotUserIds(cfg, atAccountIds);
500
+ const substituted = substituteBotMentions(content, cfg, { extraAliases });
501
+ const merged = /* @__PURE__ */ new Set();
502
+ for (const id of atDingtalkIds) if (id) merged.add(id);
503
+ for (const id of explicit.resolved) merged.add(id);
504
+ for (const id of substituted.injectedChatbotUserIds) merged.add(id);
505
+ let finalContent = substituted.text;
506
+ for (const id of explicit.resolved) if (!finalContent.includes(`@${id}`)) finalContent = `${finalContent} @${id}`;
507
+ return {
508
+ content: finalContent,
509
+ atDingtalkIds: Array.from(merged),
510
+ missingAccountIds: explicit.missing
511
+ };
512
+ }
513
+ //#endregion
514
+ //#region src/services/messaging.ts
515
+ /**
516
+ * 发送 Markdown 消息
517
+ * 支持 @用户(atUserId)和 @机器人(atDingtalkIds)
518
+ */
519
+ async function sendMarkdownMessage(config, sessionWebhook, title, markdown, options = {}) {
520
+ const token = await getAccessToken(config);
521
+ let text = markdown;
522
+ let mergedAtDingtalkIds = Array.isArray(options.atDingtalkIds) ? [...options.atDingtalkIds] : [];
523
+ if (options.cfg) {
524
+ const substituted = substituteBotMentions(text, options.cfg, { detectBareAliases: Boolean(options.detectBareAliases) });
525
+ text = substituted.text;
526
+ for (const id of substituted.injectedChatbotUserIds) if (!mergedAtDingtalkIds.includes(id)) mergedAtDingtalkIds.push(id);
527
+ }
528
+ if (options.atUserId) text = `${text} @${options.atUserId}`;
529
+ if (mergedAtDingtalkIds.length) {
530
+ for (const id of mergedAtDingtalkIds) if (!text.includes(`@${id}`)) text = `${text} @${id}`;
531
+ }
532
+ const body = {
533
+ msgtype: "markdown",
534
+ markdown: {
535
+ title: title || "Message",
536
+ text
537
+ }
538
+ };
539
+ const atUserIds = options.atUserId ? [options.atUserId] : [];
540
+ const atDingtalkIds = mergedAtDingtalkIds;
541
+ if (atUserIds.length > 0 || atDingtalkIds.length > 0) body.at = {
542
+ ...atUserIds.length > 0 ? { atUserIds } : {},
543
+ ...atDingtalkIds.length > 0 ? { atDingtalkIds } : {},
544
+ isAtAll: false
545
+ };
546
+ return (await dingtalkHttp.post(sessionWebhook, body, { headers: {
547
+ "x-acs-dingtalk-access-token": token,
548
+ "Content-Type": "application/json"
549
+ } })).data;
550
+ }
551
+ /**
552
+ * 发送文本消息
553
+ * 支持 @用户(atUserId)和 @机器人(atDingtalkIds)
554
+ */
555
+ async function sendTextMessage(config, sessionWebhook, text, options = {}) {
556
+ const token = await getAccessToken(config);
557
+ let content = text;
558
+ let mergedAtDingtalkIds = Array.isArray(options.atDingtalkIds) ? [...options.atDingtalkIds] : [];
559
+ if (options.cfg) {
560
+ const substituted = substituteBotMentions(content, options.cfg, { detectBareAliases: Boolean(options.detectBareAliases) });
561
+ content = substituted.text;
562
+ for (const id of substituted.injectedChatbotUserIds) if (!mergedAtDingtalkIds.includes(id)) mergedAtDingtalkIds.push(id);
563
+ }
564
+ if (mergedAtDingtalkIds.length) {
565
+ for (const id of mergedAtDingtalkIds) if (!content.includes(`@${id}`)) content = `${content} @${id}`;
566
+ }
567
+ const body = {
568
+ msgtype: "text",
569
+ text: { content }
570
+ };
571
+ const atUserIds = options.atUserId ? [options.atUserId] : [];
572
+ const atDingtalkIds = mergedAtDingtalkIds;
573
+ if (atUserIds.length > 0 || atDingtalkIds.length > 0) body.at = {
574
+ ...atUserIds.length > 0 ? { atUserIds } : {},
575
+ ...atDingtalkIds.length > 0 ? { atDingtalkIds } : {},
576
+ isAtAll: false
577
+ };
578
+ return (await dingtalkHttp.post(sessionWebhook, body, { headers: {
579
+ "x-acs-dingtalk-access-token": token,
580
+ "Content-Type": "application/json"
581
+ } })).data;
582
+ }
583
+ /**
584
+ * 智能选择 text / markdown
585
+ */
586
+ async function sendMessage(config, sessionWebhook, text, options = {}) {
587
+ const mergedOptions = { ...options };
588
+ let workingText = text;
589
+ if (options.cfg && typeof workingText === "string" && workingText.length > 0) {
590
+ const substituted = substituteBotMentions(workingText, options.cfg, { detectBareAliases: Boolean(options.detectBareAliases) });
591
+ workingText = substituted.text;
592
+ if (substituted.injectedChatbotUserIds.length > 0) {
593
+ const existing = Array.isArray(mergedOptions.atDingtalkIds) ? mergedOptions.atDingtalkIds : [];
594
+ mergedOptions.atDingtalkIds = Array.from(new Set([...existing, ...substituted.injectedChatbotUserIds]));
595
+ }
596
+ }
597
+ if (typeof workingText === "string" && workingText.length > 0) {
598
+ const found = Array.from(new Set(workingText.match(/\$:LWCP_v1:\$[A-Za-z0-9+/=]+/g) || []));
599
+ if (found.length > 0) {
600
+ const existing = Array.isArray(mergedOptions.atDingtalkIds) ? mergedOptions.atDingtalkIds : [];
601
+ mergedOptions.atDingtalkIds = Array.from(new Set([...existing, ...found]));
602
+ }
603
+ }
604
+ const hasMarkdown = /^[#*>-]|[*_`#\[\]]/.test(workingText) || workingText && typeof workingText === "string" && workingText.includes("\n");
605
+ const useMarkdown = mergedOptions.useMarkdown !== false && (mergedOptions.useMarkdown || hasMarkdown);
606
+ const downstreamOptions = { ...mergedOptions };
607
+ delete downstreamOptions.cfg;
608
+ if (useMarkdown) return sendMarkdownMessage(config, sessionWebhook, downstreamOptions.title || workingText.split("\n")[0].replace(/^[#*\s\->]+/, "").slice(0, 20) || "Message", workingText, downstreamOptions);
609
+ return sendTextMessage(config, sessionWebhook, workingText, downstreamOptions);
610
+ }
611
+ /**
612
+ * 构建普通消息的 msgKey 和 msgParam
613
+ *
614
+ * 第四个参数可携带 at 信息:
615
+ * - atDingtalkIds:对方加密 dingtalkId / chatbotUserId(多机器人协作时使用)
616
+ * - atUserIds:普通成员 staffId
617
+ * 这些 ID 会以 `@${id}` 文本附加到 content 末尾(钉钉客户端会尝试将其渲染成 @ 标签)。
618
+ */
619
+ function buildMsgPayload(msgType, content, title, atOptions) {
620
+ const appendAtMentions = (raw) => {
621
+ if (!atOptions) return raw;
622
+ let out = raw ?? "";
623
+ const ids = [...atOptions.atDingtalkIds || [], ...atOptions.atUserIds || []];
624
+ for (const id of ids) if (id && !out.includes(`@${id}`)) out = `${out} @${id}`;
625
+ if (atOptions.atAll && !out.includes("@all")) out = `${out} @all`;
626
+ return out;
627
+ };
628
+ switch (msgType) {
629
+ case "markdown": {
630
+ const text = appendAtMentions(content);
631
+ return {
632
+ msgKey: "sampleMarkdown",
633
+ msgParam: {
634
+ title: title || content.split("\n")[0].replace(/^[#*\s\->]+/, "").slice(0, 20) || "Message",
635
+ text
636
+ }
637
+ };
638
+ }
639
+ case "link": try {
640
+ return {
641
+ msgKey: "sampleLink",
642
+ msgParam: typeof content === "string" ? JSON.parse(content) : content
643
+ };
644
+ } catch {
645
+ return { error: "Invalid link message format, expected JSON" };
646
+ }
647
+ case "actionCard": try {
648
+ return {
649
+ msgKey: "sampleActionCard",
650
+ msgParam: typeof content === "string" ? JSON.parse(content) : content
651
+ };
652
+ } catch {
653
+ return { error: "Invalid actionCard message format, expected JSON" };
654
+ }
655
+ case "image": return {
656
+ msgKey: "sampleImageMsg",
657
+ msgParam: { photoURL: content }
658
+ };
659
+ default: return {
660
+ msgKey: "sampleText",
661
+ msgParam: { content: appendAtMentions(content) }
662
+ };
663
+ }
664
+ }
665
+ /**
666
+ * 发送文本消息(用于 outbound 接口)
667
+ */
668
+ async function sendTextToDingTalk(params) {
669
+ const { config, target, text, replyToId } = params;
670
+ const log = createLoggerFromConfig(config, "sendTextToDingTalk");
671
+ if (!target || typeof target !== "string") {
672
+ log.error("target 参数无效:", target);
673
+ return {
674
+ ok: false,
675
+ error: "Invalid target parameter",
676
+ usedAICard: false
677
+ };
678
+ }
679
+ let targetParam;
680
+ if (target.startsWith("group:")) targetParam = {
681
+ type: "group",
682
+ openConversationId: target.slice(6)
683
+ };
684
+ else if (target.startsWith("user:")) targetParam = {
685
+ type: "user",
686
+ userId: target.slice(5)
687
+ };
688
+ else if (target.startsWith("cid")) targetParam = {
689
+ type: "group",
690
+ openConversationId: target
691
+ };
692
+ else targetParam = {
693
+ type: "user",
694
+ userId: target
695
+ };
696
+ return sendProactive(config, targetParam, text, {
697
+ msgType: "text",
698
+ replyToId
699
+ });
700
+ }
701
+ /**
702
+ * 发送媒体消息(用于 outbound 接口)
703
+ */
704
+ async function sendMediaToDingTalk(params) {
705
+ const log = createLoggerFromConfig(params.config, "sendMediaToDingTalk");
706
+ log.info("开始处理,params:", JSON.stringify({
707
+ target: params.target,
708
+ text: params.text,
709
+ mediaUrl: params.mediaUrl,
710
+ replyToId: params.replyToId,
711
+ hasConfig: !!params.config
712
+ }));
713
+ const { config, target, text, mediaUrl, replyToId, mediaLocalRoots } = params;
714
+ if (!target || typeof target !== "string") {
715
+ log.error("target 参数无效:", target);
716
+ return {
717
+ ok: false,
718
+ error: "Invalid target parameter",
719
+ usedAICard: false
720
+ };
721
+ }
722
+ let targetParam;
723
+ if (target.startsWith("group:")) targetParam = {
724
+ type: "group",
725
+ openConversationId: target.slice(6)
726
+ };
727
+ else if (target.startsWith("user:")) targetParam = {
728
+ type: "user",
729
+ userId: target.slice(5)
730
+ };
731
+ else if (target.startsWith("cid")) targetParam = {
732
+ type: "group",
733
+ openConversationId: target
734
+ };
735
+ else targetParam = {
736
+ type: "user",
737
+ userId: target
738
+ };
739
+ log.info("参数解析完成,mediaUrl:", mediaUrl, "type:", typeof mediaUrl);
740
+ if (!mediaUrl) {
741
+ log.info("mediaUrl 为空,返回错误提示");
742
+ return sendProactive(config, targetParam, text ?? "⚠️ 缺少媒体文件 URL", {
743
+ msgType: "text",
744
+ replyToId
745
+ });
746
+ }
747
+ if (text && text.trim().length > 0) {
748
+ log.info("先发送文本消息:", text);
749
+ await sendProactive(config, targetParam, text, {
750
+ msgType: "text",
751
+ replyToId
752
+ });
753
+ }
754
+ try {
755
+ log.info("开始获取 oapiToken");
756
+ const oapiToken = await getOapiAccessToken(config);
757
+ log.info("oapiToken 获取成功");
758
+ log.info("开始解析文件扩展名,mediaUrl:", mediaUrl);
759
+ const ext = mediaUrl.toLowerCase().split(".").pop() || "";
760
+ log.info("文件扩展名:", ext);
761
+ let mediaType = "file";
762
+ if ([
763
+ "jpg",
764
+ "jpeg",
765
+ "png",
766
+ "gif",
767
+ "bmp",
768
+ "webp"
769
+ ].includes(ext)) mediaType = "image";
770
+ else if ([
771
+ "mp4",
772
+ "avi",
773
+ "mov",
774
+ "mkv",
775
+ "flv",
776
+ "wmv",
777
+ "webm"
778
+ ].includes(ext)) mediaType = "video";
779
+ else if ([
780
+ "mp3",
781
+ "wav",
782
+ "aac",
783
+ "ogg",
784
+ "m4a",
785
+ "flac",
786
+ "wma",
787
+ "amr"
788
+ ].includes(ext)) mediaType = "voice";
789
+ log.info("媒体类型判断完成:", mediaType);
790
+ let maxSize;
791
+ switch (mediaType) {
792
+ case "image":
793
+ maxSize = 10 * 1024 * 1024;
794
+ break;
795
+ case "voice":
796
+ maxSize = 2 * 1024 * 1024;
797
+ break;
798
+ case "video":
799
+ case "file":
800
+ maxSize = 20 * 1024 * 1024;
801
+ break;
802
+ default: maxSize = 20 * 1024 * 1024;
803
+ }
804
+ log.info("准备调用 uploadMediaToDingTalk,参数:", {
805
+ mediaUrl,
806
+ mediaType,
807
+ maxSizeMB: (maxSize / (1024 * 1024)).toFixed(0)
808
+ });
809
+ if (!oapiToken) {
810
+ log.error("oapiToken 为空,无法上传媒体文件");
811
+ return sendProactive(config, targetParam, "⚠️ 媒体文件处理失败:缺少 oapiToken", {
812
+ msgType: "text",
813
+ replyToId
814
+ });
815
+ }
816
+ let resolvedMediaUrl = mediaUrl;
817
+ const { toLocalPath } = await import("./media-C_SVin7s.mjs");
818
+ const _fs = await import("fs");
819
+ const _path = await import("path");
820
+ const directPath = toLocalPath(mediaUrl);
821
+ if (!_fs.existsSync(directPath) && mediaLocalRoots?.length && !_path.isAbsolute(directPath)) for (const root of mediaLocalRoots) {
822
+ const candidate = _path.resolve(root, directPath);
823
+ if (_fs.existsSync(candidate)) {
824
+ log.info(`相对路径解析成功:${mediaUrl} → ${candidate}(基于 mediaLocalRoots)`);
825
+ resolvedMediaUrl = candidate;
826
+ break;
827
+ }
828
+ }
829
+ const uploadResult = await uploadMediaToDingTalk(resolvedMediaUrl, mediaType, oapiToken, maxSize, log);
830
+ log.info("uploadMediaToDingTalk 返回结果:", uploadResult);
831
+ if (!uploadResult) {
832
+ log.error("上传失败,返回错误提示");
833
+ return sendProactive(config, targetParam, "⚠️ 媒体文件上传失败", {
834
+ msgType: "text",
835
+ replyToId
836
+ });
837
+ }
838
+ log.info("提取 media_id:", uploadResult.mediaId);
839
+ const fileName = mediaUrl.split("/").pop() || "file";
840
+ if (mediaType === "image") {
841
+ const result = await sendProactive(config, targetParam, uploadResult.mediaId, {
842
+ msgType: "image",
843
+ replyToId
844
+ });
845
+ return {
846
+ ...result,
847
+ processQueryKey: result.processQueryKey || "image-message-sent"
848
+ };
849
+ }
850
+ if (mediaType === "video") {
851
+ const videoMarker = `[DINGTALK_VIDEO]{"path":"${mediaUrl}"}[/DINGTALK_VIDEO]`;
852
+ const { processVideoMarkers } = await import("./media-C_SVin7s.mjs");
853
+ await processVideoMarkers(videoMarker, "", config, oapiToken, console, true, targetParam);
854
+ if (text?.trim()) {
855
+ const result = await sendProactive(config, targetParam, text, {
856
+ msgType: "text",
857
+ replyToId
858
+ });
859
+ return {
860
+ ...result,
861
+ processQueryKey: result.processQueryKey || "video-text-sent"
862
+ };
863
+ }
864
+ return {
865
+ ok: true,
866
+ usedAICard: false,
867
+ processQueryKey: "video-message-sent"
868
+ };
869
+ }
870
+ (await import("fs")).statSync(mediaUrl);
871
+ const fileInfo = {
872
+ path: mediaUrl,
873
+ fileName,
874
+ fileType: ext || "file"
875
+ };
876
+ const { sendFileProactive } = await import("./media-C_SVin7s.mjs");
877
+ await sendFileProactive(config, targetParam, fileInfo, uploadResult.mediaId, log);
878
+ return {
879
+ ok: true,
880
+ usedAICard: false,
881
+ processQueryKey: "file-message-sent"
882
+ };
883
+ } catch (err) {
884
+ log.error("发送媒体消息失败:", err.message);
885
+ return sendProactive(config, targetParam, `⚠️ 媒体文件处理失败: ${err.message}`, {
886
+ msgType: "text",
887
+ replyToId
888
+ });
889
+ }
890
+ }
891
+ /**
892
+ * 智能发送消息
893
+ */
894
+ async function sendProactive(config, target, content, options = {}) {
895
+ const log = createLoggerFromConfig(config, "sendProactive");
896
+ log.info("开始处理,参数:", JSON.stringify({
897
+ target,
898
+ contentLength: content?.length,
899
+ hasOptions: !!options
900
+ }));
901
+ if (!options.msgType) {
902
+ if (/^[#*>-]|[*_`#\[\]]/.test(content) || content && typeof content === "string" && content.includes("\n")) options.msgType = "markdown";
903
+ }
904
+ if (target.userId || target.userIds) {
905
+ const userId = (target.userIds || [target.userId])[0];
906
+ log.info("发送给用户,userId:", userId);
907
+ return sendProactiveInternal(config, {
908
+ type: "user",
909
+ userId
910
+ }, content, options);
911
+ }
912
+ if (target.openConversationId) {
913
+ log.info("发送给群聊,openConversationId:", target.openConversationId);
914
+ return sendProactiveInternal(config, {
915
+ type: "group",
916
+ openConversationId: target.openConversationId
917
+ }, content, options);
918
+ }
919
+ log.error("target 参数缺少必要字段:", target);
920
+ return {
921
+ ok: false,
922
+ error: "Must specify userId, userIds, or openConversationId",
923
+ usedAICard: false
924
+ };
925
+ }
926
+ /**
927
+ * 内部发送实现
928
+ */
929
+ async function sendProactiveInternal(config, target, content, options) {
930
+ const log = createLoggerFromConfig(config, "sendProactiveInternal");
931
+ log.info("开始处理,参数:", JSON.stringify({
932
+ target,
933
+ contentLength: content?.length,
934
+ msgType: options.msgType,
935
+ useAICard: options.useAICard,
936
+ targetType: target?.type,
937
+ hasTarget: !!target
938
+ }));
939
+ if (!target || typeof target !== "object") {
940
+ log.error("target 参数无效:", target);
941
+ return {
942
+ ok: false,
943
+ error: "Invalid target parameter",
944
+ usedAICard: false
945
+ };
946
+ }
947
+ const { msgType = "text", useAICard = true, fallbackToNormal = true, log: externalLog } = options;
948
+ const isMediaMessage = MEDIA_MSG_TYPES.has(msgType);
949
+ if (useAICard && !isMediaMessage) try {
950
+ const card = await createAICardForTarget(config, target, externalLog);
951
+ if (card) {
952
+ await finishAICard(card, content, config, externalLog);
953
+ return {
954
+ ok: true,
955
+ cardInstanceId: card.cardInstanceId,
956
+ usedAICard: true
957
+ };
958
+ }
959
+ if (!fallbackToNormal) return {
960
+ ok: false,
961
+ error: "Failed to create AI Card",
962
+ usedAICard: false
963
+ };
964
+ } catch (err) {
965
+ externalLog?.error?.(`AI Card 发送失败: ${err.message}`);
966
+ if (!fallbackToNormal) return {
967
+ ok: false,
968
+ error: err.message,
969
+ usedAICard: false
970
+ };
971
+ }
972
+ try {
973
+ log.info("准备发送普通消息,target.type:", target.type);
974
+ const token = await getAccessToken(config);
975
+ const isUser = target.type === "user";
976
+ log.info("isUser:", isUser, "target:", JSON.stringify(target));
977
+ const targetId = isUser ? target.userId : target.openConversationId;
978
+ log.info("targetId:", targetId);
979
+ const webhookUrl = isUser ? `${DINGTALK_API}/v1.0/robot/oToMessages/batchSend` : `${DINGTALK_API}/v1.0/robot/groupMessages/send`;
980
+ const payload = buildMsgPayload(msgType, content, options.title, {
981
+ atDingtalkIds: options.atDingtalkIds,
982
+ atUserIds: options.atUserIds,
983
+ atAll: options.atAll
984
+ });
985
+ if ("error" in payload) {
986
+ log.error("构建消息失败:", payload.error);
987
+ return {
988
+ ok: false,
989
+ error: payload.error,
990
+ usedAICard: false
991
+ };
992
+ }
993
+ const body = {
994
+ robotCode: String(config.clientId),
995
+ msgKey: payload.msgKey,
996
+ msgParam: JSON.stringify(payload.msgParam)
997
+ };
998
+ if (isUser) body.userIds = [targetId];
999
+ else body.openConversationId = targetId;
1000
+ externalLog?.info?.(`发送${isUser ? "单聊" : "群聊"}消息:${isUser ? "userIds=" : "openConversationId="}${targetId}`);
1001
+ const resp = await dingtalkHttp.post(webhookUrl, body, { headers: {
1002
+ "x-acs-dingtalk-access-token": token,
1003
+ "Content-Type": "application/json"
1004
+ } });
1005
+ try {
1006
+ const dataPreview = JSON.stringify(resp.data ?? {});
1007
+ const truncated = dataPreview.length > 2e3 ? `${dataPreview.slice(0, 2e3)}...(truncated)` : dataPreview;
1008
+ const msg = `发送${isUser ? "单聊" : "群聊"}消息响应:status=${resp.status}, processQueryKey=${resp.data?.processQueryKey ?? ""}, data=${truncated}`;
1009
+ log.info(msg);
1010
+ externalLog?.info?.(msg);
1011
+ } catch {
1012
+ const msg = `发送${isUser ? "单聊" : "群聊"}消息响应:status=${resp.status}, processQueryKey=${resp.data?.processQueryKey ?? ""}`;
1013
+ log.info(msg);
1014
+ externalLog?.info?.(msg);
1015
+ }
1016
+ return {
1017
+ ok: true,
1018
+ processQueryKey: resp.data?.processQueryKey,
1019
+ usedAICard: false
1020
+ };
1021
+ } catch (err) {
1022
+ const status = err?.response?.status;
1023
+ const respData = err?.response?.data;
1024
+ let respPreview = "";
1025
+ try {
1026
+ const raw = JSON.stringify(respData ?? {});
1027
+ respPreview = raw.length > 2e3 ? `${raw.slice(0, 2e3)}...(truncated)` : raw;
1028
+ } catch {
1029
+ respPreview = String(respData ?? "");
1030
+ }
1031
+ const baseMsg = err?.message ? String(err.message) : String(err);
1032
+ const extra = typeof status === "number" ? ` status=${status}${respPreview ? `, data=${respPreview}` : ""}` : respPreview ? ` data=${respPreview}` : "";
1033
+ const msg = `发送${target.type === "user" ? "单聊" : "群聊"}消息失败:${baseMsg}${extra}`;
1034
+ log.error(msg);
1035
+ externalLog?.error?.(msg);
1036
+ return {
1037
+ ok: false,
1038
+ error: baseMsg,
1039
+ usedAICard: false
1040
+ };
1041
+ }
1042
+ }
1043
+ //#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 };