@colinlu50/openclaw-lark-stream 2026.3.17

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 (361) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/README.zh.md +70 -0
  4. package/bin/openclaw-lark.js +48 -0
  5. package/index.d.ts +36 -0
  6. package/index.js +118 -0
  7. package/openclaw.plugin.json +10 -0
  8. package/package.json +66 -0
  9. package/skills/feishu-bitable/SKILL.md +248 -0
  10. package/skills/feishu-bitable/references/examples.md +813 -0
  11. package/skills/feishu-bitable/references/field-properties.md +763 -0
  12. package/skills/feishu-bitable/references/record-values.md +911 -0
  13. package/skills/feishu-calendar/SKILL.md +244 -0
  14. package/skills/feishu-channel-rules/SKILL.md +24 -0
  15. package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
  16. package/skills/feishu-create-doc/SKILL.md +719 -0
  17. package/skills/feishu-fetch-doc/SKILL.md +93 -0
  18. package/skills/feishu-im-read/SKILL.md +163 -0
  19. package/skills/feishu-task/SKILL.md +293 -0
  20. package/skills/feishu-troubleshoot/SKILL.md +70 -0
  21. package/skills/feishu-update-doc/SKILL.md +285 -0
  22. package/src/card/builder.d.ts +106 -0
  23. package/src/card/builder.js +443 -0
  24. package/src/card/cardkit.d.ts +90 -0
  25. package/src/card/cardkit.js +181 -0
  26. package/src/card/flush-controller.d.ts +45 -0
  27. package/src/card/flush-controller.js +134 -0
  28. package/src/card/image-resolver.d.ts +45 -0
  29. package/src/card/image-resolver.js +112 -0
  30. package/src/card/markdown-style.d.ts +16 -0
  31. package/src/card/markdown-style.js +97 -0
  32. package/src/card/reply-dispatcher-types.d.ts +120 -0
  33. package/src/card/reply-dispatcher-types.js +57 -0
  34. package/src/card/reply-dispatcher.d.ts +15 -0
  35. package/src/card/reply-dispatcher.js +299 -0
  36. package/src/card/reply-mode.d.ts +38 -0
  37. package/src/card/reply-mode.js +65 -0
  38. package/src/card/streaming-card-controller.d.ts +101 -0
  39. package/src/card/streaming-card-controller.js +810 -0
  40. package/src/card/unavailable-guard.d.ts +35 -0
  41. package/src/card/unavailable-guard.js +83 -0
  42. package/src/channel/abort-detect.d.ts +34 -0
  43. package/src/channel/abort-detect.js +124 -0
  44. package/src/channel/chat-queue.d.ts +41 -0
  45. package/src/channel/chat-queue.js +58 -0
  46. package/src/channel/config-adapter.d.ts +23 -0
  47. package/src/channel/config-adapter.js +101 -0
  48. package/src/channel/directory.d.ts +57 -0
  49. package/src/channel/directory.js +191 -0
  50. package/src/channel/event-handlers.d.ts +15 -0
  51. package/src/channel/event-handlers.js +221 -0
  52. package/src/channel/monitor.d.ts +17 -0
  53. package/src/channel/monitor.js +129 -0
  54. package/src/channel/onboarding-config.d.ts +17 -0
  55. package/src/channel/onboarding-config.js +88 -0
  56. package/src/channel/onboarding-migrate.d.ts +25 -0
  57. package/src/channel/onboarding-migrate.js +67 -0
  58. package/src/channel/onboarding.d.ts +12 -0
  59. package/src/channel/onboarding.js +296 -0
  60. package/src/channel/plugin.d.ts +13 -0
  61. package/src/channel/plugin.js +278 -0
  62. package/src/channel/probe.d.ts +14 -0
  63. package/src/channel/probe.js +21 -0
  64. package/src/channel/types.d.ts +36 -0
  65. package/src/channel/types.js +7 -0
  66. package/src/commands/auth.d.ts +21 -0
  67. package/src/commands/auth.js +161 -0
  68. package/src/commands/diagnose.d.ts +69 -0
  69. package/src/commands/diagnose.js +807 -0
  70. package/src/commands/doctor.d.ts +26 -0
  71. package/src/commands/doctor.js +584 -0
  72. package/src/commands/index.d.ts +25 -0
  73. package/src/commands/index.js +212 -0
  74. package/src/commands/locale.d.ts +7 -0
  75. package/src/commands/locale.js +7 -0
  76. package/src/core/accounts.d.ts +37 -0
  77. package/src/core/accounts.js +163 -0
  78. package/src/core/agent-config.d.ts +100 -0
  79. package/src/core/agent-config.js +139 -0
  80. package/src/core/api-error.d.ts +48 -0
  81. package/src/core/api-error.js +112 -0
  82. package/src/core/app-owner-fallback.d.ts +21 -0
  83. package/src/core/app-owner-fallback.js +38 -0
  84. package/src/core/app-scope-checker.d.ts +87 -0
  85. package/src/core/app-scope-checker.js +190 -0
  86. package/src/core/auth-errors.d.ts +144 -0
  87. package/src/core/auth-errors.js +154 -0
  88. package/src/core/chat-info-cache.d.ts +57 -0
  89. package/src/core/chat-info-cache.js +152 -0
  90. package/src/core/config-schema.d.ts +448 -0
  91. package/src/core/config-schema.js +200 -0
  92. package/src/core/device-flow.d.ts +77 -0
  93. package/src/core/device-flow.js +212 -0
  94. package/src/core/domains.d.ts +18 -0
  95. package/src/core/domains.js +28 -0
  96. package/src/core/feishu-fetch.d.ts +18 -0
  97. package/src/core/feishu-fetch.js +25 -0
  98. package/src/core/footer-config.d.ts +24 -0
  99. package/src/core/footer-config.js +39 -0
  100. package/src/core/lark-client.d.ts +108 -0
  101. package/src/core/lark-client.js +353 -0
  102. package/src/core/lark-logger.d.ts +23 -0
  103. package/src/core/lark-logger.js +154 -0
  104. package/src/core/lark-ticket.d.ts +29 -0
  105. package/src/core/lark-ticket.js +35 -0
  106. package/src/core/message-unavailable.d.ts +53 -0
  107. package/src/core/message-unavailable.js +130 -0
  108. package/src/core/owner-policy.d.ts +31 -0
  109. package/src/core/owner-policy.js +52 -0
  110. package/src/core/permission-url.d.ts +22 -0
  111. package/src/core/permission-url.js +72 -0
  112. package/src/core/raw-request.d.ts +27 -0
  113. package/src/core/raw-request.js +62 -0
  114. package/src/core/scope-manager.d.ts +168 -0
  115. package/src/core/scope-manager.js +213 -0
  116. package/src/core/security-check.d.ts +72 -0
  117. package/src/core/security-check.js +174 -0
  118. package/src/core/shutdown-hooks.d.ts +22 -0
  119. package/src/core/shutdown-hooks.js +56 -0
  120. package/src/core/targets.d.ts +60 -0
  121. package/src/core/targets.js +164 -0
  122. package/src/core/token-store.d.ts +54 -0
  123. package/src/core/token-store.js +314 -0
  124. package/src/core/tool-client.d.ts +176 -0
  125. package/src/core/tool-client.js +380 -0
  126. package/src/core/tool-scopes.d.ts +153 -0
  127. package/src/core/tool-scopes.js +326 -0
  128. package/src/core/tools-config.d.ts +55 -0
  129. package/src/core/tools-config.js +137 -0
  130. package/src/core/types.d.ts +87 -0
  131. package/src/core/types.js +11 -0
  132. package/src/core/uat-client.d.ts +46 -0
  133. package/src/core/uat-client.js +187 -0
  134. package/src/core/version.d.ts +25 -0
  135. package/src/core/version.js +49 -0
  136. package/src/messaging/converters/audio.d.ts +8 -0
  137. package/src/messaging/converters/audio.js +21 -0
  138. package/src/messaging/converters/calendar.d.ts +13 -0
  139. package/src/messaging/converters/calendar.js +50 -0
  140. package/src/messaging/converters/content-converter.d.ts +41 -0
  141. package/src/messaging/converters/content-converter.js +106 -0
  142. package/src/messaging/converters/file.d.ts +8 -0
  143. package/src/messaging/converters/file.js +20 -0
  144. package/src/messaging/converters/folder.d.ts +8 -0
  145. package/src/messaging/converters/folder.js +20 -0
  146. package/src/messaging/converters/hongbao.d.ts +8 -0
  147. package/src/messaging/converters/hongbao.js +16 -0
  148. package/src/messaging/converters/image.d.ts +8 -0
  149. package/src/messaging/converters/image.js +18 -0
  150. package/src/messaging/converters/index.d.ts +8 -0
  151. package/src/messaging/converters/index.js +50 -0
  152. package/src/messaging/converters/interactive/card-converter.d.ts +76 -0
  153. package/src/messaging/converters/interactive/card-converter.js +1173 -0
  154. package/src/messaging/converters/interactive/card-utils.d.ts +9 -0
  155. package/src/messaging/converters/interactive/card-utils.js +42 -0
  156. package/src/messaging/converters/interactive/index.d.ts +8 -0
  157. package/src/messaging/converters/interactive/index.js +21 -0
  158. package/src/messaging/converters/interactive/legacy.d.ts +11 -0
  159. package/src/messaging/converters/interactive/legacy.js +57 -0
  160. package/src/messaging/converters/interactive/types.d.ts +23 -0
  161. package/src/messaging/converters/interactive/types.js +24 -0
  162. package/src/messaging/converters/location.d.ts +8 -0
  163. package/src/messaging/converters/location.js +19 -0
  164. package/src/messaging/converters/merge-forward.d.ts +32 -0
  165. package/src/messaging/converters/merge-forward.js +225 -0
  166. package/src/messaging/converters/post.d.ts +11 -0
  167. package/src/messaging/converters/post.js +135 -0
  168. package/src/messaging/converters/share.d.ts +9 -0
  169. package/src/messaging/converters/share.js +23 -0
  170. package/src/messaging/converters/sticker.d.ts +8 -0
  171. package/src/messaging/converters/sticker.js +18 -0
  172. package/src/messaging/converters/system.d.ts +12 -0
  173. package/src/messaging/converters/system.js +32 -0
  174. package/src/messaging/converters/text.d.ts +8 -0
  175. package/src/messaging/converters/text.js +14 -0
  176. package/src/messaging/converters/todo.d.ts +8 -0
  177. package/src/messaging/converters/todo.js +41 -0
  178. package/src/messaging/converters/types.d.ts +107 -0
  179. package/src/messaging/converters/types.js +7 -0
  180. package/src/messaging/converters/unknown.d.ts +8 -0
  181. package/src/messaging/converters/unknown.js +16 -0
  182. package/src/messaging/converters/utils.d.ts +22 -0
  183. package/src/messaging/converters/utils.js +51 -0
  184. package/src/messaging/converters/video-chat.d.ts +8 -0
  185. package/src/messaging/converters/video-chat.js +23 -0
  186. package/src/messaging/converters/video.d.ts +8 -0
  187. package/src/messaging/converters/video.js +32 -0
  188. package/src/messaging/converters/vote.d.ts +8 -0
  189. package/src/messaging/converters/vote.js +24 -0
  190. package/src/messaging/inbound/dedup.d.ts +59 -0
  191. package/src/messaging/inbound/dedup.js +116 -0
  192. package/src/messaging/inbound/dispatch-builders.d.ts +84 -0
  193. package/src/messaging/inbound/dispatch-builders.js +152 -0
  194. package/src/messaging/inbound/dispatch-commands.d.ts +27 -0
  195. package/src/messaging/inbound/dispatch-commands.js +112 -0
  196. package/src/messaging/inbound/dispatch-context.d.ts +67 -0
  197. package/src/messaging/inbound/dispatch-context.js +136 -0
  198. package/src/messaging/inbound/dispatch.d.ts +47 -0
  199. package/src/messaging/inbound/dispatch.js +264 -0
  200. package/src/messaging/inbound/enrich.d.ts +102 -0
  201. package/src/messaging/inbound/enrich.js +227 -0
  202. package/src/messaging/inbound/gate-effects.d.ts +23 -0
  203. package/src/messaging/inbound/gate-effects.js +43 -0
  204. package/src/messaging/inbound/gate.d.ts +60 -0
  205. package/src/messaging/inbound/gate.js +233 -0
  206. package/src/messaging/inbound/handler.d.ts +35 -0
  207. package/src/messaging/inbound/handler.js +173 -0
  208. package/src/messaging/inbound/media-resolver.d.ts +32 -0
  209. package/src/messaging/inbound/media-resolver.js +87 -0
  210. package/src/messaging/inbound/mention.d.ts +39 -0
  211. package/src/messaging/inbound/mention.js +81 -0
  212. package/src/messaging/inbound/parse-io.d.ts +50 -0
  213. package/src/messaging/inbound/parse-io.js +81 -0
  214. package/src/messaging/inbound/parse.d.ts +28 -0
  215. package/src/messaging/inbound/parse.js +106 -0
  216. package/src/messaging/inbound/permission.d.ts +17 -0
  217. package/src/messaging/inbound/permission.js +40 -0
  218. package/src/messaging/inbound/policy.d.ts +94 -0
  219. package/src/messaging/inbound/policy.js +160 -0
  220. package/src/messaging/inbound/reaction-handler.d.ts +61 -0
  221. package/src/messaging/inbound/reaction-handler.js +221 -0
  222. package/src/messaging/inbound/user-name-cache.d.ts +82 -0
  223. package/src/messaging/inbound/user-name-cache.js +241 -0
  224. package/src/messaging/outbound/actions.d.ts +16 -0
  225. package/src/messaging/outbound/actions.js +309 -0
  226. package/src/messaging/outbound/chat-manage.d.ts +64 -0
  227. package/src/messaging/outbound/chat-manage.js +111 -0
  228. package/src/messaging/outbound/deliver.d.ts +155 -0
  229. package/src/messaging/outbound/deliver.js +298 -0
  230. package/src/messaging/outbound/fetch.d.ts +12 -0
  231. package/src/messaging/outbound/fetch.js +12 -0
  232. package/src/messaging/outbound/forward.d.ts +26 -0
  233. package/src/messaging/outbound/forward.js +48 -0
  234. package/src/messaging/outbound/media-url-utils.d.ts +29 -0
  235. package/src/messaging/outbound/media-url-utils.js +130 -0
  236. package/src/messaging/outbound/media.d.ts +260 -0
  237. package/src/messaging/outbound/media.js +758 -0
  238. package/src/messaging/outbound/outbound.d.ts +89 -0
  239. package/src/messaging/outbound/outbound.js +121 -0
  240. package/src/messaging/outbound/reactions.d.ts +124 -0
  241. package/src/messaging/outbound/reactions.js +378 -0
  242. package/src/messaging/outbound/send.d.ts +152 -0
  243. package/src/messaging/outbound/send.js +355 -0
  244. package/src/messaging/outbound/typing.d.ts +71 -0
  245. package/src/messaging/outbound/typing.js +179 -0
  246. package/src/messaging/shared/message-lookup.d.ts +54 -0
  247. package/src/messaging/shared/message-lookup.js +117 -0
  248. package/src/messaging/types.d.ts +176 -0
  249. package/src/messaging/types.js +10 -0
  250. package/src/tools/auto-auth.d.ts +56 -0
  251. package/src/tools/auto-auth.js +919 -0
  252. package/src/tools/helpers.d.ts +260 -0
  253. package/src/tools/helpers.js +364 -0
  254. package/src/tools/mcp/doc/create.d.ts +12 -0
  255. package/src/tools/mcp/doc/create.js +44 -0
  256. package/src/tools/mcp/doc/fetch.d.ts +12 -0
  257. package/src/tools/mcp/doc/fetch.js +36 -0
  258. package/src/tools/mcp/doc/index.d.ts +12 -0
  259. package/src/tools/mcp/doc/index.js +41 -0
  260. package/src/tools/mcp/doc/update.d.ts +12 -0
  261. package/src/tools/mcp/doc/update.js +61 -0
  262. package/src/tools/mcp/shared.d.ts +59 -0
  263. package/src/tools/mcp/shared.js +226 -0
  264. package/src/tools/oapi/bitable/app-table-field.d.ts +16 -0
  265. package/src/tools/oapi/bitable/app-table-field.js +222 -0
  266. package/src/tools/oapi/bitable/app-table-record.d.ts +20 -0
  267. package/src/tools/oapi/bitable/app-table-record.js +436 -0
  268. package/src/tools/oapi/bitable/app-table-view.d.ts +17 -0
  269. package/src/tools/oapi/bitable/app-table-view.js +195 -0
  270. package/src/tools/oapi/bitable/app-table.d.ts +19 -0
  271. package/src/tools/oapi/bitable/app-table.js +247 -0
  272. package/src/tools/oapi/bitable/app.d.ts +18 -0
  273. package/src/tools/oapi/bitable/app.js +186 -0
  274. package/src/tools/oapi/bitable/index.d.ts +9 -0
  275. package/src/tools/oapi/bitable/index.js +9 -0
  276. package/src/tools/oapi/calendar/calendar.d.ts +15 -0
  277. package/src/tools/oapi/calendar/calendar.js +122 -0
  278. package/src/tools/oapi/calendar/event-attendee.d.ts +16 -0
  279. package/src/tools/oapi/calendar/event-attendee.js +263 -0
  280. package/src/tools/oapi/calendar/event.d.ts +16 -0
  281. package/src/tools/oapi/calendar/event.js +709 -0
  282. package/src/tools/oapi/calendar/freebusy.d.ts +13 -0
  283. package/src/tools/oapi/calendar/freebusy.js +111 -0
  284. package/src/tools/oapi/calendar/index.d.ts +8 -0
  285. package/src/tools/oapi/calendar/index.js +8 -0
  286. package/src/tools/oapi/chat/chat.d.ts +16 -0
  287. package/src/tools/oapi/chat/chat.js +124 -0
  288. package/src/tools/oapi/chat/index.d.ts +10 -0
  289. package/src/tools/oapi/chat/index.js +15 -0
  290. package/src/tools/oapi/chat/members.d.ts +11 -0
  291. package/src/tools/oapi/chat/members.js +81 -0
  292. package/src/tools/oapi/common/get-user.d.ts +12 -0
  293. package/src/tools/oapi/common/get-user.js +106 -0
  294. package/src/tools/oapi/common/index.d.ts +6 -0
  295. package/src/tools/oapi/common/index.js +6 -0
  296. package/src/tools/oapi/common/search-user.d.ts +11 -0
  297. package/src/tools/oapi/common/search-user.js +73 -0
  298. package/src/tools/oapi/drive/doc-comments.d.ts +15 -0
  299. package/src/tools/oapi/drive/doc-comments.js +279 -0
  300. package/src/tools/oapi/drive/doc-media.d.ts +19 -0
  301. package/src/tools/oapi/drive/doc-media.js +335 -0
  302. package/src/tools/oapi/drive/file.d.ts +19 -0
  303. package/src/tools/oapi/drive/file.js +483 -0
  304. package/src/tools/oapi/drive/index.d.ts +12 -0
  305. package/src/tools/oapi/drive/index.js +36 -0
  306. package/src/tools/oapi/helpers.d.ts +182 -0
  307. package/src/tools/oapi/helpers.js +354 -0
  308. package/src/tools/oapi/im/format-messages.d.ts +50 -0
  309. package/src/tools/oapi/im/format-messages.js +165 -0
  310. package/src/tools/oapi/im/index.d.ts +10 -0
  311. package/src/tools/oapi/im/index.js +17 -0
  312. package/src/tools/oapi/im/message-read.d.ts +13 -0
  313. package/src/tools/oapi/im/message-read.js +411 -0
  314. package/src/tools/oapi/im/message.d.ts +16 -0
  315. package/src/tools/oapi/im/message.js +149 -0
  316. package/src/tools/oapi/im/resource.d.ts +13 -0
  317. package/src/tools/oapi/im/resource.js +150 -0
  318. package/src/tools/oapi/im/time-utils.d.ts +46 -0
  319. package/src/tools/oapi/im/time-utils.js +201 -0
  320. package/src/tools/oapi/im/user-name-uat.d.ts +26 -0
  321. package/src/tools/oapi/im/user-name-uat.js +140 -0
  322. package/src/tools/oapi/index.d.ts +11 -0
  323. package/src/tools/oapi/index.js +58 -0
  324. package/src/tools/oapi/sdk-types.d.ts +96 -0
  325. package/src/tools/oapi/sdk-types.js +12 -0
  326. package/src/tools/oapi/search/doc-search.d.ts +13 -0
  327. package/src/tools/oapi/search/doc-search.js +191 -0
  328. package/src/tools/oapi/search/index.d.ts +12 -0
  329. package/src/tools/oapi/search/index.js +33 -0
  330. package/src/tools/oapi/sheets/index.d.ts +12 -0
  331. package/src/tools/oapi/sheets/index.js +31 -0
  332. package/src/tools/oapi/sheets/sheet.d.ts +16 -0
  333. package/src/tools/oapi/sheets/sheet.js +652 -0
  334. package/src/tools/oapi/task/comment.d.ts +15 -0
  335. package/src/tools/oapi/task/comment.js +140 -0
  336. package/src/tools/oapi/task/index.d.ts +8 -0
  337. package/src/tools/oapi/task/index.js +8 -0
  338. package/src/tools/oapi/task/subtask.d.ts +14 -0
  339. package/src/tools/oapi/task/subtask.js +162 -0
  340. package/src/tools/oapi/task/task.d.ts +16 -0
  341. package/src/tools/oapi/task/task.js +344 -0
  342. package/src/tools/oapi/task/tasklist.d.ts +21 -0
  343. package/src/tools/oapi/task/tasklist.js +321 -0
  344. package/src/tools/oapi/wiki/index.d.ts +12 -0
  345. package/src/tools/oapi/wiki/index.js +34 -0
  346. package/src/tools/oapi/wiki/space-node.d.ts +17 -0
  347. package/src/tools/oapi/wiki/space-node.js +230 -0
  348. package/src/tools/oapi/wiki/space.d.ts +15 -0
  349. package/src/tools/oapi/wiki/space.js +130 -0
  350. package/src/tools/oauth-batch-auth.d.ts +11 -0
  351. package/src/tools/oauth-batch-auth.js +142 -0
  352. package/src/tools/oauth-cards.d.ts +39 -0
  353. package/src/tools/oauth-cards.js +315 -0
  354. package/src/tools/oauth.d.ts +47 -0
  355. package/src/tools/oauth.js +620 -0
  356. package/src/tools/onboarding-auth.d.ts +27 -0
  357. package/src/tools/onboarding-auth.js +130 -0
  358. package/src/tools/tat/im/index.d.ts +15 -0
  359. package/src/tools/tat/im/index.js +18 -0
  360. package/src/tools/tat/im/resource.d.ts +15 -0
  361. package/src/tools/tat/im/resource.js +157 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * CardKit streaming APIs for Lark/Feishu.
6
+ */
7
+ import { LarkClient } from '../core/lark-client';
8
+ import { larkLogger } from '../core/lark-logger';
9
+ import { normalizeFeishuTarget, normalizeMessageId, resolveReceiveIdType } from '../core/targets';
10
+ import { runWithMessageUnavailableGuard } from '../core/message-unavailable';
11
+ const log = larkLogger('card/cardkit');
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * 记录 CardKit API 响应日志,检测错误码并抛出异常。
17
+ *
18
+ * 默认 fail-fast:body-level 非零 code 视为业务错误,立即抛出,
19
+ * 由调用方(streaming-card-controller 等)统一走 catch → guard 处理。
20
+ */
21
+ function logCardKitResponse(params) {
22
+ const { resp, api, context } = params;
23
+ const { code, msg } = resp;
24
+ log.info(`cardkit ${api} response`, { code, msg, context });
25
+ if (code && code !== 0) {
26
+ log.warn(`cardkit ${api} FAILED`, { code, msg, context, fullResponse: resp });
27
+ throw new Error(`cardkit ${api} FAILED: code=${code}, msg=${msg ?? ''}, ${context}`);
28
+ }
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // CardKit streaming APIs
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Create a card entity via the CardKit API.
35
+ *
36
+ * Returns the card_id directly, bypassing the idConvert step.
37
+ * The card can then be sent via IM API and streamed via CardKit.
38
+ */
39
+ export async function createCardEntity(params) {
40
+ const { cfg, card, accountId } = params;
41
+ const client = LarkClient.fromCfg(cfg, accountId).sdk;
42
+ // SDK 返回类型不完整,运行时包含 code/msg/data 字段
43
+ const response = (await client.cardkit.v1.card.create({
44
+ data: {
45
+ type: 'card_json',
46
+ data: JSON.stringify(card),
47
+ },
48
+ }));
49
+ // 兼容不同 SDK 包装层:优先 data.card_id,回退顶层 card_id
50
+ const cardId = (response.data?.card_id ?? response.card_id) ?? null;
51
+ logCardKitResponse({ resp: response, api: 'card.create', context: `cardId=${cardId}` });
52
+ return cardId;
53
+ }
54
+ /**
55
+ * Stream text content to a specific card element using the CardKit API.
56
+ *
57
+ * The card automatically diffs the new content against the previous
58
+ * content and renders incremental changes with a typewriter animation.
59
+ *
60
+ * @param params.cardId - CardKit card ID (from `convertMessageToCardId`).
61
+ * @param params.elementId - The element ID to update (e.g. `STREAMING_ELEMENT_ID`).
62
+ * @param params.content - The full cumulative text (not a delta).
63
+ * @param params.sequence - Monotonically increasing sequence number.
64
+ */
65
+ export async function streamCardContent(params) {
66
+ const { cfg, cardId, elementId, content, sequence, accountId } = params;
67
+ const client = LarkClient.fromCfg(cfg, accountId).sdk;
68
+ // SDK 返回类型不完整,运行时包含 code/msg 字段
69
+ const resp = (await client.cardkit.v1.cardElement.content({
70
+ data: { content, sequence },
71
+ path: { card_id: cardId, element_id: elementId },
72
+ }));
73
+ logCardKitResponse({
74
+ resp,
75
+ api: 'cardElement.content',
76
+ context: `seq=${sequence}, contentLen=${content.length}`,
77
+ });
78
+ }
79
+ /**
80
+ * Fully replace a card using the CardKit API.
81
+ *
82
+ * Used for the final "complete" state update (with action buttons, green
83
+ * header, etc.) after streaming finishes.
84
+ *
85
+ * @param params.cardId - CardKit card ID.
86
+ * @param params.card - The new card JSON content.
87
+ * @param params.sequence - Monotonically increasing sequence number.
88
+ */
89
+ export async function updateCardKitCard(params) {
90
+ const { cfg, cardId, card, sequence, accountId } = params;
91
+ const client = LarkClient.fromCfg(cfg, accountId).sdk;
92
+ // SDK 返回类型不完整,运行时包含 code/msg 字段
93
+ const resp = (await client.cardkit.v1.card.update({
94
+ data: {
95
+ card: { type: 'card_json', data: JSON.stringify(card) },
96
+ sequence,
97
+ },
98
+ path: { card_id: cardId },
99
+ }));
100
+ logCardKitResponse({
101
+ resp,
102
+ api: 'card.update',
103
+ context: `seq=${sequence}, cardId=${cardId}`,
104
+ });
105
+ }
106
+ export async function updateCardKitCardForAuth(params) {
107
+ return updateCardKitCard(params);
108
+ }
109
+ /**
110
+ * Send an interactive card message by referencing a CardKit card_id.
111
+ *
112
+ * The content format is: {"type":"card","data":{"card_id":"xxx"}}
113
+ * This links the IM message to the CardKit card entity, enabling
114
+ * streaming updates via cardElement.content().
115
+ */
116
+ export async function sendCardByCardId(params) {
117
+ const { cfg, to, cardId, replyToMessageId, replyInThread, accountId } = params;
118
+ const client = LarkClient.fromCfg(cfg, accountId).sdk;
119
+ const contentPayload = JSON.stringify({
120
+ type: 'card',
121
+ data: { card_id: cardId },
122
+ });
123
+ if (replyToMessageId) {
124
+ // 规范化 message_id,处理合成 ID(如 "om_xxx:auth-complete")
125
+ const normalizedId = normalizeMessageId(replyToMessageId);
126
+ const response = await runWithMessageUnavailableGuard({
127
+ messageId: normalizedId,
128
+ operation: 'im.message.reply(interactive.cardkit)',
129
+ fn: () => client.im.message.reply({
130
+ path: { message_id: normalizedId },
131
+ data: { content: contentPayload, msg_type: 'interactive', reply_in_thread: replyInThread },
132
+ }),
133
+ });
134
+ return {
135
+ messageId: response?.data?.message_id ?? '',
136
+ chatId: response?.data?.chat_id ?? '',
137
+ };
138
+ }
139
+ const target = normalizeFeishuTarget(to);
140
+ if (!target) {
141
+ throw new Error(`[feishu-send] Invalid target: "${to}"`);
142
+ }
143
+ const receiveIdType = resolveReceiveIdType(target);
144
+ const response = await client.im.message.create({
145
+ // SDK 类型将 receive_id_type 限定为字面量联合,但运行时接受动态值
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ params: { receive_id_type: receiveIdType },
148
+ data: {
149
+ receive_id: target,
150
+ msg_type: 'interactive',
151
+ content: contentPayload,
152
+ },
153
+ });
154
+ return {
155
+ messageId: response?.data?.message_id ?? '',
156
+ chatId: response?.data?.chat_id ?? '',
157
+ };
158
+ }
159
+ /**
160
+ * Close (or open) the streaming mode on a CardKit card.
161
+ *
162
+ * Must be called after streaming is complete to restore normal card
163
+ * behaviour (forwarding, interaction callbacks, etc.).
164
+ */
165
+ export async function setCardStreamingMode(params) {
166
+ const { cfg, cardId, streamingMode, sequence, accountId } = params;
167
+ const client = LarkClient.fromCfg(cfg, accountId).sdk;
168
+ // SDK 返回类型不完整,运行时包含 code/msg 字段
169
+ const resp = (await client.cardkit.v1.card.settings({
170
+ data: {
171
+ settings: JSON.stringify({ streaming_mode: streamingMode }),
172
+ sequence,
173
+ },
174
+ path: { card_id: cardId },
175
+ }));
176
+ logCardKitResponse({
177
+ resp,
178
+ api: 'card.settings',
179
+ context: `seq=${sequence}, streaming_mode=${streamingMode}`,
180
+ });
181
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Generic throttled flush controller.
6
+ *
7
+ * A pure scheduling primitive that manages timer-based throttling,
8
+ * mutex-guarded flushing, and reflush-on-conflict. Contains no
9
+ * business logic — the actual flush work is provided via a callback.
10
+ */
11
+ export declare class FlushController {
12
+ private readonly doFlush;
13
+ private flushInProgress;
14
+ private flushResolvers;
15
+ private needsReflush;
16
+ private pendingFlushTimer;
17
+ private lastUpdateTime;
18
+ private isCompleted;
19
+ constructor(doFlush: () => Promise<void>);
20
+ /** Mark the controller as completed — no more flushes after current one. */
21
+ complete(): void;
22
+ /** Cancel any pending deferred flush timer. */
23
+ cancelPendingFlush(): void;
24
+ /** Wait for any in-progress flush to finish. */
25
+ waitForFlush(): Promise<void>;
26
+ /**
27
+ * Execute a flush (mutex-guarded, with reflush on conflict).
28
+ *
29
+ * If a flush is already in progress, marks needsReflush so a
30
+ * follow-up flush fires immediately after the current one completes.
31
+ */
32
+ flush(): Promise<void>;
33
+ /**
34
+ * Throttled update entry point.
35
+ *
36
+ * @param throttleMs - Minimum interval between flushes (varies by
37
+ * CardKit vs IM patch mode). Passed in by the caller so this
38
+ * controller remains business-logic-free.
39
+ */
40
+ throttledUpdate(throttleMs: number): Promise<void>;
41
+ /** Overridable gate: subclasses / consumers can set via setCardMessageReady. */
42
+ private _cardMessageReady;
43
+ cardMessageReady(): boolean;
44
+ setCardMessageReady(ready: boolean): void;
45
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Generic throttled flush controller.
6
+ *
7
+ * A pure scheduling primitive that manages timer-based throttling,
8
+ * mutex-guarded flushing, and reflush-on-conflict. Contains no
9
+ * business logic — the actual flush work is provided via a callback.
10
+ */
11
+ import { THROTTLE_CONSTANTS } from './reply-dispatcher-types';
12
+ // ---------------------------------------------------------------------------
13
+ // FlushController
14
+ // ---------------------------------------------------------------------------
15
+ export class FlushController {
16
+ doFlush;
17
+ flushInProgress = false;
18
+ flushResolvers = [];
19
+ needsReflush = false;
20
+ pendingFlushTimer = null;
21
+ lastUpdateTime = 0;
22
+ isCompleted = false;
23
+ constructor(doFlush) {
24
+ this.doFlush = doFlush;
25
+ }
26
+ /** Mark the controller as completed — no more flushes after current one. */
27
+ complete() {
28
+ this.isCompleted = true;
29
+ }
30
+ /** Cancel any pending deferred flush timer. */
31
+ cancelPendingFlush() {
32
+ if (this.pendingFlushTimer) {
33
+ clearTimeout(this.pendingFlushTimer);
34
+ this.pendingFlushTimer = null;
35
+ }
36
+ }
37
+ /** Wait for any in-progress flush to finish. */
38
+ waitForFlush() {
39
+ if (!this.flushInProgress)
40
+ return Promise.resolve();
41
+ return new Promise((resolve) => this.flushResolvers.push(resolve));
42
+ }
43
+ /**
44
+ * Execute a flush (mutex-guarded, with reflush on conflict).
45
+ *
46
+ * If a flush is already in progress, marks needsReflush so a
47
+ * follow-up flush fires immediately after the current one completes.
48
+ */
49
+ async flush() {
50
+ if (!this.cardMessageReady() || this.flushInProgress || this.isCompleted) {
51
+ if (this.flushInProgress && !this.isCompleted)
52
+ this.needsReflush = true;
53
+ return;
54
+ }
55
+ this.flushInProgress = true;
56
+ this.needsReflush = false;
57
+ // Update timestamp BEFORE the API call to prevent concurrent callers
58
+ // from also entering the flush (race condition fix).
59
+ this.lastUpdateTime = Date.now();
60
+ try {
61
+ await this.doFlush();
62
+ this.lastUpdateTime = Date.now();
63
+ }
64
+ finally {
65
+ this.flushInProgress = false;
66
+ const resolvers = this.flushResolvers;
67
+ this.flushResolvers = [];
68
+ for (const resolve of resolvers)
69
+ resolve();
70
+ // If events arrived while the API call was in flight,
71
+ // schedule an immediate follow-up flush.
72
+ if (this.needsReflush && !this.isCompleted && !this.pendingFlushTimer) {
73
+ this.needsReflush = false;
74
+ this.pendingFlushTimer = setTimeout(() => {
75
+ this.pendingFlushTimer = null;
76
+ void this.flush();
77
+ }, 0);
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Throttled update entry point.
83
+ *
84
+ * @param throttleMs - Minimum interval between flushes (varies by
85
+ * CardKit vs IM patch mode). Passed in by the caller so this
86
+ * controller remains business-logic-free.
87
+ */
88
+ async throttledUpdate(throttleMs) {
89
+ if (!this.cardMessageReady())
90
+ return;
91
+ const now = Date.now();
92
+ const elapsed = now - this.lastUpdateTime;
93
+ if (elapsed >= throttleMs) {
94
+ this.cancelPendingFlush();
95
+ if (elapsed > THROTTLE_CONSTANTS.LONG_GAP_THRESHOLD_MS) {
96
+ // After a long gap, batch briefly so the first visible update
97
+ // contains meaningful text rather than just 1-2 characters.
98
+ this.lastUpdateTime = now;
99
+ this.pendingFlushTimer = setTimeout(() => {
100
+ this.pendingFlushTimer = null;
101
+ void this.flush();
102
+ }, THROTTLE_CONSTANTS.BATCH_AFTER_GAP_MS);
103
+ }
104
+ else {
105
+ await this.flush();
106
+ }
107
+ }
108
+ else if (!this.pendingFlushTimer) {
109
+ // Inside throttle window — schedule a deferred flush
110
+ const delay = throttleMs - elapsed;
111
+ this.pendingFlushTimer = setTimeout(() => {
112
+ this.pendingFlushTimer = null;
113
+ void this.flush();
114
+ }, delay);
115
+ }
116
+ }
117
+ // ------------------------------------------------------------------
118
+ // Internal
119
+ // ------------------------------------------------------------------
120
+ /** Overridable gate: subclasses / consumers can set via setCardMessageReady. */
121
+ _cardMessageReady = false;
122
+ cardMessageReady() {
123
+ return this._cardMessageReady;
124
+ }
125
+ setCardMessageReady(ready) {
126
+ this._cardMessageReady = ready;
127
+ if (ready) {
128
+ // Initialize the timestamp so the first throttledUpdate sees a
129
+ // small elapsed time (matching original behavior where
130
+ // lastCardUpdateTime = Date.now() was set during card creation).
131
+ this.lastUpdateTime = Date.now();
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ImageResolver — converts image URLs in markdown to Feishu image keys.
6
+ *
7
+ * Used by StreamingCardController to asynchronously download and upload
8
+ * images referenced via `![alt](https://...)` in model-generated markdown,
9
+ * replacing them with `![alt](img_xxx)` that Feishu cards can render.
10
+ */
11
+ import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
12
+ export interface ImageResolverOptions {
13
+ cfg: ClawdbotConfig;
14
+ accountId: string | undefined;
15
+ /** Called when a previously-pending image upload completes. */
16
+ onImageResolved: () => void;
17
+ }
18
+ export declare class ImageResolver {
19
+ /** URL → imageKey for successfully uploaded images. */
20
+ private readonly resolved;
21
+ /** URL → upload Promise for in-flight uploads (dedup). */
22
+ private readonly pending;
23
+ /** URLs that have already failed — skip retries. */
24
+ private readonly failed;
25
+ private readonly cfg;
26
+ private readonly accountId;
27
+ private readonly onImageResolved;
28
+ constructor(opts: ImageResolverOptions);
29
+ /**
30
+ * Synchronously resolve image URLs in markdown text.
31
+ *
32
+ * - `img_xxx` references are kept as-is.
33
+ * - URLs with a cached imageKey are replaced inline.
34
+ * - URLs with an in-flight upload are stripped (will appear after re-flush).
35
+ * - New URLs trigger an async upload and are stripped for now.
36
+ */
37
+ resolveImages(text: string): string;
38
+ /**
39
+ * Resolve all image URLs in text synchronously: trigger uploads for new
40
+ * URLs, wait for all pending uploads, then return text with image keys.
41
+ */
42
+ resolveImagesAwait(text: string, timeoutMs: number): Promise<string>;
43
+ private startUpload;
44
+ private doUpload;
45
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ImageResolver — converts image URLs in markdown to Feishu image keys.
6
+ *
7
+ * Used by StreamingCardController to asynchronously download and upload
8
+ * images referenced via `![alt](https://...)` in model-generated markdown,
9
+ * replacing them with `![alt](img_xxx)` that Feishu cards can render.
10
+ */
11
+ import { fetchRemoteImageBuffer, uploadImageLark } from '../messaging/outbound/media';
12
+ import { larkLogger } from '../core/lark-logger';
13
+ const log = larkLogger('card/image-resolver');
14
+ /** Matches complete markdown image syntax: `![alt](value)` */
15
+ const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
16
+ export class ImageResolver {
17
+ /** URL → imageKey for successfully uploaded images. */
18
+ resolved = new Map();
19
+ /** URL → upload Promise for in-flight uploads (dedup). */
20
+ pending = new Map();
21
+ /** URLs that have already failed — skip retries. */
22
+ failed = new Set();
23
+ cfg;
24
+ accountId;
25
+ onImageResolved;
26
+ constructor(opts) {
27
+ this.cfg = opts.cfg;
28
+ this.accountId = opts.accountId;
29
+ this.onImageResolved = opts.onImageResolved;
30
+ }
31
+ /**
32
+ * Synchronously resolve image URLs in markdown text.
33
+ *
34
+ * - `img_xxx` references are kept as-is.
35
+ * - URLs with a cached imageKey are replaced inline.
36
+ * - URLs with an in-flight upload are stripped (will appear after re-flush).
37
+ * - New URLs trigger an async upload and are stripped for now.
38
+ */
39
+ resolveImages(text) {
40
+ if (!text.includes('!['))
41
+ return text;
42
+ return text.replace(IMAGE_RE, (fullMatch, alt, value) => {
43
+ // Already a Feishu image key — keep.
44
+ if (value.startsWith('img_'))
45
+ return fullMatch;
46
+ // Not a remote URL — strip (local paths, data URIs, etc.).
47
+ if (!value.startsWith('http://') && !value.startsWith('https://'))
48
+ return '';
49
+ // Cached — replace with image key.
50
+ const cached = this.resolved.get(value);
51
+ if (cached)
52
+ return `![${alt}](${cached})`;
53
+ // Already failed — don't retry, strip.
54
+ if (this.failed.has(value))
55
+ return '';
56
+ // Upload in progress — strip for now.
57
+ if (this.pending.has(value))
58
+ return '';
59
+ // New URL — kick off async upload, strip for now.
60
+ this.startUpload(value);
61
+ return '';
62
+ });
63
+ }
64
+ /**
65
+ * Resolve all image URLs in text synchronously: trigger uploads for new
66
+ * URLs, wait for all pending uploads, then return text with image keys.
67
+ */
68
+ async resolveImagesAwait(text, timeoutMs) {
69
+ // First pass: trigger uploads for any new URLs
70
+ this.resolveImages(text);
71
+ if (this.pending.size > 0) {
72
+ log.info('resolveImagesAwait: waiting for uploads', { count: this.pending.size, timeoutMs });
73
+ const allUploads = Promise.all(this.pending.values());
74
+ const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
75
+ await Promise.race([allUploads, timeout]);
76
+ if (this.pending.size > 0) {
77
+ log.warn('resolveImagesAwait: timed out with pending uploads', {
78
+ remaining: this.pending.size,
79
+ });
80
+ }
81
+ }
82
+ // Second pass: replace URLs with resolved image keys
83
+ return this.resolveImages(text);
84
+ }
85
+ startUpload(url) {
86
+ const uploadPromise = this.doUpload(url);
87
+ this.pending.set(url, uploadPromise);
88
+ }
89
+ async doUpload(url) {
90
+ try {
91
+ log.info('uploading image', { url });
92
+ const buffer = await fetchRemoteImageBuffer(url);
93
+ const { imageKey } = await uploadImageLark({
94
+ cfg: this.cfg,
95
+ image: buffer,
96
+ imageType: 'message',
97
+ accountId: this.accountId,
98
+ });
99
+ log.info('image uploaded', { url, imageKey });
100
+ this.resolved.set(url, imageKey);
101
+ this.pending.delete(url);
102
+ this.onImageResolved();
103
+ return imageKey;
104
+ }
105
+ catch (err) {
106
+ log.warn('image upload failed', { url, error: String(err) });
107
+ this.pending.delete(url);
108
+ this.failed.add(url);
109
+ return null;
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Markdown 样式优化工具
6
+ */
7
+ /**
8
+ * 优化 Markdown 样式:
9
+ * - 标题降级:H1 → H4,H2~H6 → H5
10
+ * - 表格前后增加段落间距
11
+ * - 有序列表:序号后确保只有一个空格
12
+ * - 无序列表:"- " 格式规范化(跳过分隔线 ---)
13
+ * - 表格:单元格前后补空格,分隔符行规范化,表格前后加空行
14
+ * - 代码块内容不受影响
15
+ */
16
+ export declare function optimizeMarkdownStyle(text: string, cardVersion?: number): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Markdown 样式优化工具
6
+ */
7
+ /**
8
+ * 优化 Markdown 样式:
9
+ * - 标题降级:H1 → H4,H2~H6 → H5
10
+ * - 表格前后增加段落间距
11
+ * - 有序列表:序号后确保只有一个空格
12
+ * - 无序列表:"- " 格式规范化(跳过分隔线 ---)
13
+ * - 表格:单元格前后补空格,分隔符行规范化,表格前后加空行
14
+ * - 代码块内容不受影响
15
+ */
16
+ export function optimizeMarkdownStyle(text, cardVersion = 2) {
17
+ try {
18
+ let r = _optimizeMarkdownStyle(text, cardVersion);
19
+ r = stripInvalidImageKeys(r);
20
+ return r;
21
+ }
22
+ catch {
23
+ return text;
24
+ }
25
+ }
26
+ function _optimizeMarkdownStyle(text, cardVersion = 2) {
27
+ // ── 1. 提取代码块,用占位符保护,处理后再还原 ─────────────────────
28
+ const MARK = '___CB_';
29
+ const codeBlocks = [];
30
+ let r = text.replace(/```[\s\S]*?```/g, (m) => {
31
+ return `${MARK}${codeBlocks.push(m) - 1}___`;
32
+ });
33
+ // ── 2. 标题降级 ────────────────────────────────────────────────────
34
+ // 只有当原文档包含 h1~h3 标题时才执行降级
35
+ // 先处理 H2~H6 → H5,再处理 H1 → H4
36
+ // 顺序不能颠倒:若先 H1→H4,H4(####)会被后面的 #{2,6} 再次匹配成 H5
37
+ const hasH1toH3 = /^#{1,3} /m.test(text);
38
+ if (hasH1toH3) {
39
+ r = r.replace(/^#{2,6} (.+)$/gm, '##### $1'); // H2~H6 → H5
40
+ r = r.replace(/^# (.+)$/gm, '#### $1'); // H1 → H4
41
+ }
42
+ if (cardVersion >= 2) {
43
+ // ── 3. 连续标题间增加段落间距 ───────────────────────────────────────
44
+ r = r.replace(/^(#{4,5} .+)\n{1,2}(#{4,5} )/gm, '$1\n<br>\n$2');
45
+ // ── 4. 表格前后增加段落间距 ─────────────────────────────────────────
46
+ // 4a. 非表格行直接跟表格行时,先补一个空行
47
+ r = r.replace(/^([^|\n].*)\n(\|.+\|)/gm, '$1\n\n$2');
48
+ // 4b. 表格前:在空行之前插入 <br>(即 \n\n| → \n<br>\n\n| )
49
+ r = r.replace(/\n\n((?:\|.+\|[^\S\n]*\n?)+)/g, '\n\n<br>\n\n$1');
50
+ // 4c. 表格后:在表格块末尾追加 <br>
51
+ r = r.replace(/((?:^\|.+\|[^\S\n]*\n?)+)/gm, '$1\n<br>\n');
52
+ // 4d. 表格前是普通文本(非标题、非加粗行)时,只需 <br>,去掉多余空行
53
+ // "text\n\n<br>\n\n|" → "text\n<br>\n|"
54
+ r = r.replace(/^((?!#{4,5} )(?!\*\*).+)\n\n(<br>)\n\n(\|)/gm, '$1\n$2\n$3');
55
+ // 4d2. 表格前是加粗行时,<br> 紧贴加粗行,空行保留在后面
56
+ // "**bold**\n\n<br>\n\n|" → "**bold**\n<br>\n\n|"
57
+ r = r.replace(/^(\*\*.+)\n\n(<br>)\n\n(\|)/gm, '$1\n$2\n\n$3');
58
+ // 4e. 表格后是普通文本(非标题、非加粗行)时,只需 <br>,去掉多余空行
59
+ // "| row |\n\n<br>\ntext" → "| row |\n<br>\ntext"
60
+ r = r.replace(/(\|[^\n]*\n)\n(<br>\n)((?!#{4,5} )(?!\*\*))/gm, '$1$2$3');
61
+ // ── 5. 还原代码块,并在前后追加 <br> ──────────────────────────────
62
+ codeBlocks.forEach((block, i) => {
63
+ r = r.replace(`${MARK}${i}___`, `\n<br>\n${block}\n<br>\n`);
64
+ });
65
+ }
66
+ else {
67
+ // ── 5. 还原代码块(无 <br>)───────────────────────────────────────
68
+ codeBlocks.forEach((block, i) => {
69
+ r = r.replace(`${MARK}${i}___`, block);
70
+ });
71
+ }
72
+ // ── 6. 压缩多余空行(3 个以上连续换行 → 2 个)────────────────────
73
+ r = r.replace(/\n{3,}/g, '\n\n');
74
+ return r;
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // stripInvalidImageKeys
78
+ // ---------------------------------------------------------------------------
79
+ /** Matches complete markdown image syntax: `![alt](value)` */
80
+ const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
81
+ /**
82
+ * Strip `![alt](value)` where value is not a valid Feishu image key
83
+ * (`img_xxx`). Prevents CardKit error 200570.
84
+ *
85
+ * HTTP URLs are stripped as well — ImageResolver should have already
86
+ * replaced them with `img_xxx` keys before this point. This serves
87
+ * as a safety net for any unresolved URLs.
88
+ */
89
+ function stripInvalidImageKeys(text) {
90
+ if (!text.includes('!['))
91
+ return text;
92
+ return text.replace(IMAGE_RE, (fullMatch, _alt, value) => {
93
+ if (value.startsWith('img_'))
94
+ return fullMatch;
95
+ return ''; // strip all non-img_ image references (URLs, local paths, etc.)
96
+ });
97
+ }