@larksuite/openclaw-lark 2025.7.15-beta.0

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 (426) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/bin/openclaw-lark.js +39 -0
  4. package/index.d.ts +36 -0
  5. package/index.js +188 -0
  6. package/openclaw.plugin.json +64 -0
  7. package/package.json +63 -0
  8. package/secret-contract-api.d.ts +15 -0
  9. package/secret-contract-api.js +78 -0
  10. package/skills/feishu-bitable/SKILL.md +248 -0
  11. package/skills/feishu-bitable/references/examples.md +813 -0
  12. package/skills/feishu-bitable/references/field-properties.md +763 -0
  13. package/skills/feishu-bitable/references/record-values.md +911 -0
  14. package/skills/feishu-calendar/SKILL.md +242 -0
  15. package/skills/feishu-channel-rules/SKILL.md +18 -0
  16. package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
  17. package/skills/feishu-create-doc/SKILL.md +719 -0
  18. package/skills/feishu-fetch-doc/SKILL.md +93 -0
  19. package/skills/feishu-im-read/SKILL.md +163 -0
  20. package/skills/feishu-task/SKILL.md +340 -0
  21. package/skills/feishu-troubleshoot/SKILL.md +70 -0
  22. package/skills/feishu-update-doc/SKILL.md +285 -0
  23. package/src/card/builder.d.ts +152 -0
  24. package/src/card/builder.js +794 -0
  25. package/src/card/card-error.d.ts +91 -0
  26. package/src/card/card-error.js +206 -0
  27. package/src/card/cardkit.d.ts +90 -0
  28. package/src/card/cardkit.js +203 -0
  29. package/src/card/flush-controller.d.ts +45 -0
  30. package/src/card/flush-controller.js +138 -0
  31. package/src/card/image-resolver.d.ts +45 -0
  32. package/src/card/image-resolver.js +116 -0
  33. package/src/card/markdown-style.d.ts +16 -0
  34. package/src/card/markdown-style.js +106 -0
  35. package/src/card/reasoning-utils.d.ts +14 -0
  36. package/src/card/reasoning-utils.js +64 -0
  37. package/src/card/reply-dispatcher-types.d.ts +132 -0
  38. package/src/card/reply-dispatcher-types.js +61 -0
  39. package/src/card/reply-dispatcher.d.ts +15 -0
  40. package/src/card/reply-dispatcher.js +447 -0
  41. package/src/card/reply-mode.d.ts +41 -0
  42. package/src/card/reply-mode.js +70 -0
  43. package/src/card/streaming-card-controller.d.ts +118 -0
  44. package/src/card/streaming-card-controller.js +1045 -0
  45. package/src/card/tool-use-config.d.ts +26 -0
  46. package/src/card/tool-use-config.js +76 -0
  47. package/src/card/tool-use-display.d.ts +37 -0
  48. package/src/card/tool-use-display.js +476 -0
  49. package/src/card/tool-use-trace-store.d.ts +51 -0
  50. package/src/card/tool-use-trace-store.js +271 -0
  51. package/src/card/unavailable-guard.d.ts +35 -0
  52. package/src/card/unavailable-guard.js +87 -0
  53. package/src/channel/abort-detect.d.ts +47 -0
  54. package/src/channel/abort-detect.js +219 -0
  55. package/src/channel/chat-queue.d.ts +41 -0
  56. package/src/channel/chat-queue.js +68 -0
  57. package/src/channel/config-adapter.d.ts +23 -0
  58. package/src/channel/config-adapter.js +107 -0
  59. package/src/channel/directory.d.ts +57 -0
  60. package/src/channel/directory.js +197 -0
  61. package/src/channel/event-handlers.d.ts +17 -0
  62. package/src/channel/event-handlers.js +380 -0
  63. package/src/channel/interactive-dispatch.d.ts +59 -0
  64. package/src/channel/interactive-dispatch.js +195 -0
  65. package/src/channel/monitor.d.ts +17 -0
  66. package/src/channel/monitor.js +140 -0
  67. package/src/channel/onboarding-config.d.ts +18 -0
  68. package/src/channel/onboarding-config.js +96 -0
  69. package/src/channel/onboarding-migrate.d.ts +25 -0
  70. package/src/channel/onboarding-migrate.js +70 -0
  71. package/src/channel/onboarding.d.ts +12 -0
  72. package/src/channel/onboarding.js +300 -0
  73. package/src/channel/plugin.d.ts +13 -0
  74. package/src/channel/plugin.js +310 -0
  75. package/src/channel/probe.d.ts +14 -0
  76. package/src/channel/probe.js +24 -0
  77. package/src/channel/types.d.ts +37 -0
  78. package/src/channel/types.js +8 -0
  79. package/src/commands/auth.d.ts +21 -0
  80. package/src/commands/auth.js +165 -0
  81. package/src/commands/diagnose.d.ts +69 -0
  82. package/src/commands/diagnose.js +848 -0
  83. package/src/commands/doctor.d.ts +27 -0
  84. package/src/commands/doctor.js +588 -0
  85. package/src/commands/index.d.ts +25 -0
  86. package/src/commands/index.js +219 -0
  87. package/src/commands/locale.d.ts +7 -0
  88. package/src/commands/locale.js +8 -0
  89. package/src/core/accounts.d.ts +51 -0
  90. package/src/core/accounts.js +219 -0
  91. package/src/core/agent-config.d.ts +100 -0
  92. package/src/core/agent-config.js +148 -0
  93. package/src/core/api-error.d.ts +48 -0
  94. package/src/core/api-error.js +117 -0
  95. package/src/core/app-owner-fallback.d.ts +22 -0
  96. package/src/core/app-owner-fallback.js +39 -0
  97. package/src/core/app-scope-checker.d.ts +87 -0
  98. package/src/core/app-scope-checker.js +198 -0
  99. package/src/core/auth-errors.d.ts +144 -0
  100. package/src/core/auth-errors.js +160 -0
  101. package/src/core/card-action-operator.d.ts +33 -0
  102. package/src/core/card-action-operator.js +30 -0
  103. package/src/core/chat-info-cache.d.ts +67 -0
  104. package/src/core/chat-info-cache.js +165 -0
  105. package/src/core/comment-target.d.ts +65 -0
  106. package/src/core/comment-target.js +100 -0
  107. package/src/core/config-schema.d.ts +490 -0
  108. package/src/core/config-schema.js +223 -0
  109. package/src/core/device-flow.d.ts +77 -0
  110. package/src/core/device-flow.js +217 -0
  111. package/src/core/domains.d.ts +18 -0
  112. package/src/core/domains.js +34 -0
  113. package/src/core/feishu-fetch.d.ts +18 -0
  114. package/src/core/feishu-fetch.js +28 -0
  115. package/src/core/footer-config.d.ts +24 -0
  116. package/src/core/footer-config.js +51 -0
  117. package/src/core/lark-client.d.ts +125 -0
  118. package/src/core/lark-client.js +468 -0
  119. package/src/core/lark-logger.d.ts +23 -0
  120. package/src/core/lark-logger.js +160 -0
  121. package/src/core/lark-ticket.d.ts +29 -0
  122. package/src/core/lark-ticket.js +40 -0
  123. package/src/core/message-unavailable.d.ts +53 -0
  124. package/src/core/message-unavailable.js +142 -0
  125. package/src/core/owner-policy.d.ts +32 -0
  126. package/src/core/owner-policy.js +55 -0
  127. package/src/core/permission-url.d.ts +22 -0
  128. package/src/core/permission-url.js +76 -0
  129. package/src/core/raw-request.d.ts +27 -0
  130. package/src/core/raw-request.js +90 -0
  131. package/src/core/runtime-store.d.ts +13 -0
  132. package/src/core/runtime-store.js +29 -0
  133. package/src/core/scope-manager.d.ts +168 -0
  134. package/src/core/scope-manager.js +224 -0
  135. package/src/core/sdk-compat.d.ts +20 -0
  136. package/src/core/sdk-compat.js +28 -0
  137. package/src/core/security-check.d.ts +72 -0
  138. package/src/core/security-check.js +184 -0
  139. package/src/core/shutdown-hooks.d.ts +22 -0
  140. package/src/core/shutdown-hooks.js +61 -0
  141. package/src/core/synthetic-target.d.ts +33 -0
  142. package/src/core/synthetic-target.js +40 -0
  143. package/src/core/targets.d.ts +60 -0
  144. package/src/core/targets.js +173 -0
  145. package/src/core/token-store.d.ts +54 -0
  146. package/src/core/token-store.js +326 -0
  147. package/src/core/tool-client.d.ts +176 -0
  148. package/src/core/tool-client.js +426 -0
  149. package/src/core/tool-scopes.d.ts +156 -0
  150. package/src/core/tool-scopes.js +344 -0
  151. package/src/core/tools-config.d.ts +55 -0
  152. package/src/core/tools-config.js +143 -0
  153. package/src/core/types.d.ts +91 -0
  154. package/src/core/types.js +12 -0
  155. package/src/core/uat-client.d.ts +46 -0
  156. package/src/core/uat-client.js +192 -0
  157. package/src/core/version.d.ts +31 -0
  158. package/src/core/version.js +69 -0
  159. package/src/messaging/converters/audio.d.ts +8 -0
  160. package/src/messaging/converters/audio.js +24 -0
  161. package/src/messaging/converters/calendar.d.ts +13 -0
  162. package/src/messaging/converters/calendar.js +56 -0
  163. package/src/messaging/converters/content-converter-helpers.d.ts +30 -0
  164. package/src/messaging/converters/content-converter-helpers.js +82 -0
  165. package/src/messaging/converters/content-converter.d.ts +24 -0
  166. package/src/messaging/converters/content-converter.js +40 -0
  167. package/src/messaging/converters/file.d.ts +8 -0
  168. package/src/messaging/converters/file.js +24 -0
  169. package/src/messaging/converters/folder.d.ts +8 -0
  170. package/src/messaging/converters/folder.js +24 -0
  171. package/src/messaging/converters/hongbao.d.ts +8 -0
  172. package/src/messaging/converters/hongbao.js +20 -0
  173. package/src/messaging/converters/image.d.ts +8 -0
  174. package/src/messaging/converters/image.js +22 -0
  175. package/src/messaging/converters/index.d.ts +8 -0
  176. package/src/messaging/converters/index.js +53 -0
  177. package/src/messaging/converters/interactive/card-converter.d.ts +76 -0
  178. package/src/messaging/converters/interactive/card-converter.js +1177 -0
  179. package/src/messaging/converters/interactive/card-utils.d.ts +9 -0
  180. package/src/messaging/converters/interactive/card-utils.js +47 -0
  181. package/src/messaging/converters/interactive/index.d.ts +8 -0
  182. package/src/messaging/converters/interactive/index.js +25 -0
  183. package/src/messaging/converters/interactive/legacy.d.ts +11 -0
  184. package/src/messaging/converters/interactive/legacy.js +60 -0
  185. package/src/messaging/converters/interactive/types.d.ts +23 -0
  186. package/src/messaging/converters/interactive/types.js +27 -0
  187. package/src/messaging/converters/location.d.ts +8 -0
  188. package/src/messaging/converters/location.js +23 -0
  189. package/src/messaging/converters/merge-forward.d.ts +32 -0
  190. package/src/messaging/converters/merge-forward.js +235 -0
  191. package/src/messaging/converters/post.d.ts +11 -0
  192. package/src/messaging/converters/post.js +235 -0
  193. package/src/messaging/converters/share.d.ts +9 -0
  194. package/src/messaging/converters/share.js +28 -0
  195. package/src/messaging/converters/sticker.d.ts +8 -0
  196. package/src/messaging/converters/sticker.js +22 -0
  197. package/src/messaging/converters/system.d.ts +12 -0
  198. package/src/messaging/converters/system.js +36 -0
  199. package/src/messaging/converters/text.d.ts +8 -0
  200. package/src/messaging/converters/text.js +18 -0
  201. package/src/messaging/converters/todo.d.ts +8 -0
  202. package/src/messaging/converters/todo.js +45 -0
  203. package/src/messaging/converters/types.d.ts +114 -0
  204. package/src/messaging/converters/types.js +8 -0
  205. package/src/messaging/converters/unknown.d.ts +8 -0
  206. package/src/messaging/converters/unknown.js +20 -0
  207. package/src/messaging/converters/utils.d.ts +22 -0
  208. package/src/messaging/converters/utils.js +57 -0
  209. package/src/messaging/converters/video-chat.d.ts +8 -0
  210. package/src/messaging/converters/video-chat.js +31 -0
  211. package/src/messaging/converters/video.d.ts +8 -0
  212. package/src/messaging/converters/video.js +35 -0
  213. package/src/messaging/converters/vote.d.ts +8 -0
  214. package/src/messaging/converters/vote.js +28 -0
  215. package/src/messaging/inbound/bot-content.d.ts +84 -0
  216. package/src/messaging/inbound/bot-content.js +117 -0
  217. package/src/messaging/inbound/bot-loop-guard.d.ts +48 -0
  218. package/src/messaging/inbound/bot-loop-guard.js +89 -0
  219. package/src/messaging/inbound/comment-context.d.ts +82 -0
  220. package/src/messaging/inbound/comment-context.js +353 -0
  221. package/src/messaging/inbound/comment-handler.d.ts +30 -0
  222. package/src/messaging/inbound/comment-handler.js +269 -0
  223. package/src/messaging/inbound/dedup.d.ts +59 -0
  224. package/src/messaging/inbound/dedup.js +121 -0
  225. package/src/messaging/inbound/dispatch-builders.d.ts +104 -0
  226. package/src/messaging/inbound/dispatch-builders.js +241 -0
  227. package/src/messaging/inbound/dispatch-commands.d.ts +22 -0
  228. package/src/messaging/inbound/dispatch-commands.js +131 -0
  229. package/src/messaging/inbound/dispatch-context.d.ts +67 -0
  230. package/src/messaging/inbound/dispatch-context.js +153 -0
  231. package/src/messaging/inbound/dispatch.d.ts +50 -0
  232. package/src/messaging/inbound/dispatch.js +477 -0
  233. package/src/messaging/inbound/enrich.d.ts +102 -0
  234. package/src/messaging/inbound/enrich.js +236 -0
  235. package/src/messaging/inbound/gate-effects.d.ts +23 -0
  236. package/src/messaging/inbound/gate-effects.js +46 -0
  237. package/src/messaging/inbound/gate.d.ts +91 -0
  238. package/src/messaging/inbound/gate.js +342 -0
  239. package/src/messaging/inbound/handler-registry.d.ts +25 -0
  240. package/src/messaging/inbound/handler-registry.js +23 -0
  241. package/src/messaging/inbound/handler.d.ts +37 -0
  242. package/src/messaging/inbound/handler.js +286 -0
  243. package/src/messaging/inbound/media-resolver.d.ts +32 -0
  244. package/src/messaging/inbound/media-resolver.js +91 -0
  245. package/src/messaging/inbound/mention-registry.d.ts +59 -0
  246. package/src/messaging/inbound/mention-registry.js +115 -0
  247. package/src/messaging/inbound/mention.d.ts +48 -0
  248. package/src/messaging/inbound/mention.js +102 -0
  249. package/src/messaging/inbound/parse-io.d.ts +50 -0
  250. package/src/messaging/inbound/parse-io.js +86 -0
  251. package/src/messaging/inbound/parse.d.ts +28 -0
  252. package/src/messaging/inbound/parse.js +128 -0
  253. package/src/messaging/inbound/permission.d.ts +17 -0
  254. package/src/messaging/inbound/permission.js +44 -0
  255. package/src/messaging/inbound/policy.d.ts +95 -0
  256. package/src/messaging/inbound/policy.js +168 -0
  257. package/src/messaging/inbound/reaction-handler.d.ts +62 -0
  258. package/src/messaging/inbound/reaction-handler.js +259 -0
  259. package/src/messaging/inbound/sentinel-store.d.ts +37 -0
  260. package/src/messaging/inbound/sentinel-store.js +98 -0
  261. package/src/messaging/inbound/synthetic-message.d.ts +26 -0
  262. package/src/messaging/inbound/synthetic-message.js +59 -0
  263. package/src/messaging/inbound/user-name-cache-store.d.ts +62 -0
  264. package/src/messaging/inbound/user-name-cache-store.js +236 -0
  265. package/src/messaging/inbound/user-name-cache.d.ts +78 -0
  266. package/src/messaging/inbound/user-name-cache.js +306 -0
  267. package/src/messaging/inbound/vc-meeting-invited-handler.d.ts +20 -0
  268. package/src/messaging/inbound/vc-meeting-invited-handler.js +236 -0
  269. package/src/messaging/inbound/vc-sender.d.ts +41 -0
  270. package/src/messaging/inbound/vc-sender.js +53 -0
  271. package/src/messaging/outbound/actions.d.ts +16 -0
  272. package/src/messaging/outbound/actions.js +329 -0
  273. package/src/messaging/outbound/bot-peer-context.d.ts +42 -0
  274. package/src/messaging/outbound/bot-peer-context.js +40 -0
  275. package/src/messaging/outbound/chat-manage.d.ts +64 -0
  276. package/src/messaging/outbound/chat-manage.js +117 -0
  277. package/src/messaging/outbound/deliver.d.ts +186 -0
  278. package/src/messaging/outbound/deliver.js +410 -0
  279. package/src/messaging/outbound/fetch.d.ts +12 -0
  280. package/src/messaging/outbound/fetch.js +17 -0
  281. package/src/messaging/outbound/forward.d.ts +26 -0
  282. package/src/messaging/outbound/forward.js +51 -0
  283. package/src/messaging/outbound/media-url-utils.d.ts +29 -0
  284. package/src/messaging/outbound/media-url-utils.js +172 -0
  285. package/src/messaging/outbound/media.d.ts +260 -0
  286. package/src/messaging/outbound/media.js +865 -0
  287. package/src/messaging/outbound/normalize-mentions.d.ts +50 -0
  288. package/src/messaging/outbound/normalize-mentions.js +166 -0
  289. package/src/messaging/outbound/outbound-mention.d.ts +41 -0
  290. package/src/messaging/outbound/outbound-mention.js +112 -0
  291. package/src/messaging/outbound/outbound.d.ts +79 -0
  292. package/src/messaging/outbound/outbound.js +195 -0
  293. package/src/messaging/outbound/reactions.d.ts +124 -0
  294. package/src/messaging/outbound/reactions.js +384 -0
  295. package/src/messaging/outbound/send.d.ts +153 -0
  296. package/src/messaging/outbound/send.js +417 -0
  297. package/src/messaging/outbound/typing.d.ts +60 -0
  298. package/src/messaging/outbound/typing.js +139 -0
  299. package/src/messaging/shared/message-lookup.d.ts +54 -0
  300. package/src/messaging/shared/message-lookup.js +120 -0
  301. package/src/messaging/types.d.ts +316 -0
  302. package/src/messaging/types.js +11 -0
  303. package/src/tools/ask-user-question.d.ts +32 -0
  304. package/src/tools/ask-user-question.js +922 -0
  305. package/src/tools/auto-auth.d.ts +57 -0
  306. package/src/tools/auto-auth.js +904 -0
  307. package/src/tools/helpers.d.ts +264 -0
  308. package/src/tools/helpers.js +389 -0
  309. package/src/tools/mcp/doc/create.d.ts +12 -0
  310. package/src/tools/mcp/doc/create.js +47 -0
  311. package/src/tools/mcp/doc/fetch.d.ts +12 -0
  312. package/src/tools/mcp/doc/fetch.js +39 -0
  313. package/src/tools/mcp/doc/index.d.ts +12 -0
  314. package/src/tools/mcp/doc/index.js +50 -0
  315. package/src/tools/mcp/doc/update.d.ts +12 -0
  316. package/src/tools/mcp/doc/update.js +64 -0
  317. package/src/tools/mcp/shared.d.ts +59 -0
  318. package/src/tools/mcp/shared.js +239 -0
  319. package/src/tools/oapi/bitable/app-table-field.d.ts +16 -0
  320. package/src/tools/oapi/bitable/app-table-field.js +225 -0
  321. package/src/tools/oapi/bitable/app-table-record.d.ts +20 -0
  322. package/src/tools/oapi/bitable/app-table-record.js +439 -0
  323. package/src/tools/oapi/bitable/app-table-view.d.ts +16 -0
  324. package/src/tools/oapi/bitable/app-table-view.js +172 -0
  325. package/src/tools/oapi/bitable/app-table.d.ts +17 -0
  326. package/src/tools/oapi/bitable/app-table.js +195 -0
  327. package/src/tools/oapi/bitable/app.d.ts +18 -0
  328. package/src/tools/oapi/bitable/app.js +189 -0
  329. package/src/tools/oapi/bitable/index.d.ts +9 -0
  330. package/src/tools/oapi/bitable/index.js +17 -0
  331. package/src/tools/oapi/calendar/calendar.d.ts +15 -0
  332. package/src/tools/oapi/calendar/calendar.js +125 -0
  333. package/src/tools/oapi/calendar/event-attendee.d.ts +14 -0
  334. package/src/tools/oapi/calendar/event-attendee.js +161 -0
  335. package/src/tools/oapi/calendar/event.d.ts +16 -0
  336. package/src/tools/oapi/calendar/event.js +712 -0
  337. package/src/tools/oapi/calendar/freebusy.d.ts +13 -0
  338. package/src/tools/oapi/calendar/freebusy.js +114 -0
  339. package/src/tools/oapi/calendar/index.d.ts +8 -0
  340. package/src/tools/oapi/calendar/index.js +15 -0
  341. package/src/tools/oapi/chat/chat.d.ts +16 -0
  342. package/src/tools/oapi/chat/chat.js +127 -0
  343. package/src/tools/oapi/chat/index.d.ts +10 -0
  344. package/src/tools/oapi/chat/index.js +23 -0
  345. package/src/tools/oapi/chat/members.d.ts +11 -0
  346. package/src/tools/oapi/chat/members.js +84 -0
  347. package/src/tools/oapi/common/get-user.d.ts +12 -0
  348. package/src/tools/oapi/common/get-user.js +109 -0
  349. package/src/tools/oapi/common/index.d.ts +6 -0
  350. package/src/tools/oapi/common/index.js +11 -0
  351. package/src/tools/oapi/common/search-user.d.ts +11 -0
  352. package/src/tools/oapi/common/search-user.js +76 -0
  353. package/src/tools/oapi/drive/doc-comments.d.ts +15 -0
  354. package/src/tools/oapi/drive/doc-comments.js +351 -0
  355. package/src/tools/oapi/drive/doc-media.d.ts +19 -0
  356. package/src/tools/oapi/drive/doc-media.js +371 -0
  357. package/src/tools/oapi/drive/file.d.ts +19 -0
  358. package/src/tools/oapi/drive/file.js +519 -0
  359. package/src/tools/oapi/drive/index.d.ts +12 -0
  360. package/src/tools/oapi/drive/index.js +45 -0
  361. package/src/tools/oapi/helpers.d.ts +183 -0
  362. package/src/tools/oapi/helpers.js +385 -0
  363. package/src/tools/oapi/im/format-messages.d.ts +50 -0
  364. package/src/tools/oapi/im/format-messages.js +169 -0
  365. package/src/tools/oapi/im/index.d.ts +10 -0
  366. package/src/tools/oapi/im/index.js +25 -0
  367. package/src/tools/oapi/im/message-read.d.ts +13 -0
  368. package/src/tools/oapi/im/message-read.js +419 -0
  369. package/src/tools/oapi/im/message.d.ts +16 -0
  370. package/src/tools/oapi/im/message.js +152 -0
  371. package/src/tools/oapi/im/resource.d.ts +13 -0
  372. package/src/tools/oapi/im/resource.js +186 -0
  373. package/src/tools/oapi/im/time-utils.d.ts +46 -0
  374. package/src/tools/oapi/im/time-utils.js +212 -0
  375. package/src/tools/oapi/im/user-name-uat.d.ts +26 -0
  376. package/src/tools/oapi/im/user-name-uat.js +143 -0
  377. package/src/tools/oapi/index.d.ts +11 -0
  378. package/src/tools/oapi/index.js +64 -0
  379. package/src/tools/oapi/sdk-types.d.ts +96 -0
  380. package/src/tools/oapi/sdk-types.js +13 -0
  381. package/src/tools/oapi/search/doc-search.d.ts +13 -0
  382. package/src/tools/oapi/search/doc-search.js +194 -0
  383. package/src/tools/oapi/search/index.d.ts +12 -0
  384. package/src/tools/oapi/search/index.js +37 -0
  385. package/src/tools/oapi/sheets/index.d.ts +12 -0
  386. package/src/tools/oapi/sheets/index.js +35 -0
  387. package/src/tools/oapi/sheets/sheet.d.ts +16 -0
  388. package/src/tools/oapi/sheets/sheet.js +688 -0
  389. package/src/tools/oapi/task/attachment.d.ts +18 -0
  390. package/src/tools/oapi/task/attachment.js +107 -0
  391. package/src/tools/oapi/task/comment.d.ts +15 -0
  392. package/src/tools/oapi/task/comment.js +149 -0
  393. package/src/tools/oapi/task/index.d.ts +11 -0
  394. package/src/tools/oapi/task/index.js +21 -0
  395. package/src/tools/oapi/task/section.d.ts +17 -0
  396. package/src/tools/oapi/task/section.js +293 -0
  397. package/src/tools/oapi/task/subtask.d.ts +14 -0
  398. package/src/tools/oapi/task/subtask.js +171 -0
  399. package/src/tools/oapi/task/task.d.ts +19 -0
  400. package/src/tools/oapi/task/task.js +512 -0
  401. package/src/tools/oapi/task/task_agent.d.ts +14 -0
  402. package/src/tools/oapi/task/task_agent.js +108 -0
  403. package/src/tools/oapi/task/tasklist.d.ts +19 -0
  404. package/src/tools/oapi/task/tasklist.js +276 -0
  405. package/src/tools/oapi/wiki/index.d.ts +12 -0
  406. package/src/tools/oapi/wiki/index.js +42 -0
  407. package/src/tools/oapi/wiki/space-node.d.ts +17 -0
  408. package/src/tools/oapi/wiki/space-node.js +233 -0
  409. package/src/tools/oapi/wiki/space.d.ts +15 -0
  410. package/src/tools/oapi/wiki/space.js +133 -0
  411. package/src/tools/oauth-batch-auth.d.ts +11 -0
  412. package/src/tools/oauth-batch-auth.js +150 -0
  413. package/src/tools/oauth-cards.d.ts +39 -0
  414. package/src/tools/oauth-cards.js +324 -0
  415. package/src/tools/oauth.d.ts +47 -0
  416. package/src/tools/oauth.js +592 -0
  417. package/src/tools/onboarding-auth.d.ts +27 -0
  418. package/src/tools/onboarding-auth.js +133 -0
  419. package/src/tools/tat/im/index.d.ts +15 -0
  420. package/src/tools/tat/im/index.js +22 -0
  421. package/src/tools/tat/im/resource.d.ts +15 -0
  422. package/src/tools/tat/im/resource.js +192 -0
  423. package/tsdown.config.d.ts +2 -0
  424. package/tsdown.config.js +25 -0
  425. package/vitest.config.d.ts +2 -0
  426. package/vitest.config.js +12 -0
@@ -0,0 +1,922 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * AskUserQuestion tool — AI agent 主动向用户提问并等待回答。
7
+ *
8
+ * 流程(非阻塞,遵循 auto-auth synthetic message 模式):
9
+ * 1. AI 调用 AskUserQuestion 工具,传入问题和选项
10
+ * 2. 发送 form 交互式飞书卡片
11
+ * 3. 工具 execute() 立即返回 { status: 'pending' }
12
+ * 4. 用户填写表单并点击提交,form_value 一次性回传
13
+ * 5. handleAskUserAction 解析答案,注入 synthetic message
14
+ * 6. AI 在新一轮对话中收到用户答案
15
+ *
16
+ * 所有卡片统一使用 form 容器,交互组件在本地缓存值,
17
+ * 提交时通过 form_value 一次性回调,避免独立回调导致的 loading 闪烁。
18
+ *
19
+ * 注意:提交按钮必须设置 value 字段。飞书客户端会校验交互组件是否携带 value,
20
+ * 缺失时直接报 200340 错误,回调不会发送到服务端。虽然 form_submit 场景下
21
+ * form_value 是主要数据载体,但 value 作为按钮的合法性标记仍需存在。
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.handleAskUserAction = handleAskUserAction;
25
+ exports.registerAskUserQuestionTool = registerAskUserQuestionTool;
26
+ const node_crypto_1 = require("node:crypto");
27
+ const typebox_1 = require("@sinclair/typebox");
28
+ const lark_ticket_1 = require("../core/lark-ticket.js");
29
+ const card_action_operator_1 = require("../core/card-action-operator.js");
30
+ const lark_logger_1 = require("../core/lark-logger.js");
31
+ const cardkit_1 = require("../card/cardkit.js");
32
+ const chat_queue_1 = require("../channel/chat-queue.js");
33
+ const handler_1 = require("../messaging/inbound/handler.js");
34
+ const helpers_1 = require("./helpers.js");
35
+ const log = (0, lark_logger_1.larkLogger)('tools/ask-user-question');
36
+ // ---------------------------------------------------------------------------
37
+ // Constants
38
+ // ---------------------------------------------------------------------------
39
+ const ACTION_SUBMIT = 'ask_user_submit';
40
+ /** TTL for pending questions: auto-expire after 5 minutes. */
41
+ const PENDING_QUESTION_TTL_MS = 5 * 60 * 1000;
42
+ /** Max retries for synthetic message injection. */
43
+ const INJECT_MAX_RETRIES = 2;
44
+ /** Delay between retry attempts (ms). */
45
+ const INJECT_RETRY_DELAY_MS = 2000;
46
+ /** Field name used for text input inside forms. */
47
+ const INPUT_FIELD_NAME = 'answer';
48
+ /** Field name used for select components inside forms. */
49
+ const SELECT_FIELD_NAME = 'selection';
50
+ /** Prefix for submit button name — questionId is appended for identification. */
51
+ const SUBMIT_BUTTON_PREFIX = 'ask_user_submit_';
52
+ /** Shared V2 card config */
53
+ const V2_CONFIG = { wide_screen_mode: true, update_multi: true, locales: ['zh_cn', 'en_us'] };
54
+ // ---------------------------------------------------------------------------
55
+ // Pending Question Registry
56
+ // ---------------------------------------------------------------------------
57
+ const pendingQuestions = new Map();
58
+ /**
59
+ * Secondary index: chatKey → Set<questionId> for fallback lookup when
60
+ * operationId is missing. Uses the base key (accountId:chatId, without
61
+ * threadId) because card action callbacks typically lack thread context.
62
+ * Stores a Set so multiple pending questions in the same chat don't
63
+ * overwrite each other's fallback entry.
64
+ */
65
+ const byChatContext = new Map();
66
+ /** Arm (or re-arm) the TTL expiry timer for a pending question. */
67
+ function armTtlTimer(ctx, delayMs) {
68
+ clearTimeout(ctx.ttlTimer);
69
+ ctx.ttlTimer = setTimeout(() => {
70
+ if (!pendingQuestions.has(ctx.questionId))
71
+ return; // already consumed
72
+ if (ctx.submitted)
73
+ return; // user already submitted, injection in progress
74
+ log.info(`question ${ctx.questionId} expired (TTL ${delayMs}ms)`);
75
+ consumePendingQuestion(ctx.questionId);
76
+ // Update card to expired state (fire-and-forget)
77
+ setImmediate(async () => {
78
+ try {
79
+ await updateCardToExpired(ctx);
80
+ }
81
+ catch (err) {
82
+ log.warn(`failed to update card to expired state: ${err}`);
83
+ }
84
+ });
85
+ }, delayMs);
86
+ }
87
+ function storePendingQuestion(init) {
88
+ const ctx = init;
89
+ pendingQuestions.set(ctx.questionId, ctx);
90
+ const baseKey = (0, chat_queue_1.buildQueueKey)(ctx.accountId, ctx.chatId);
91
+ let set = byChatContext.get(baseKey);
92
+ if (!set) {
93
+ set = new Set();
94
+ byChatContext.set(baseKey, set);
95
+ }
96
+ set.add(ctx.questionId);
97
+ armTtlTimer(ctx, PENDING_QUESTION_TTL_MS);
98
+ }
99
+ function consumePendingQuestion(questionId) {
100
+ const ctx = pendingQuestions.get(questionId);
101
+ if (ctx) {
102
+ clearTimeout(ctx.ttlTimer);
103
+ pendingQuestions.delete(questionId);
104
+ const baseKey = (0, chat_queue_1.buildQueueKey)(ctx.accountId, ctx.chatId);
105
+ const set = byChatContext.get(baseKey);
106
+ if (set) {
107
+ set.delete(questionId);
108
+ if (set.size === 0)
109
+ byChatContext.delete(baseKey);
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Targeted chat-scoped fallback: exact accountId:chatId match via secondary index.
115
+ * Used when operationId cannot be extracted from the card callback.
116
+ *
117
+ * Only returns a result when exactly one non-submitted pending question
118
+ * exists for this chat — refuses to guess when ambiguous.
119
+ */
120
+ function findQuestionByChat(accountId, chatId) {
121
+ const baseKey = (0, chat_queue_1.buildQueueKey)(accountId, chatId);
122
+ const set = byChatContext.get(baseKey);
123
+ if (!set)
124
+ return undefined;
125
+ let match;
126
+ for (const qid of set) {
127
+ const ctx = pendingQuestions.get(qid);
128
+ if (ctx && !ctx.submitted) {
129
+ if (match) {
130
+ // Ambiguous: more than one non-submitted question in this chat.
131
+ // Refuse to guess — operationId is required to disambiguate.
132
+ log.warn(`chat-scoped fallback ambiguous: multiple pending questions in ${baseKey}`);
133
+ return undefined;
134
+ }
135
+ match = ctx;
136
+ }
137
+ }
138
+ return match;
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Field name helpers
142
+ // ---------------------------------------------------------------------------
143
+ function getInputFieldName(questionIndex) {
144
+ return `${INPUT_FIELD_NAME}_${questionIndex}`;
145
+ }
146
+ function getSelectFieldName(questionIndex) {
147
+ return `${SELECT_FIELD_NAME}_${questionIndex}`;
148
+ }
149
+ function getCheckerFieldName(questionIndex, optionIndex) {
150
+ return `${SELECT_FIELD_NAME}_${questionIndex}_${optionIndex}`;
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // Card Action Handler (used by event-handlers.ts)
154
+ // ---------------------------------------------------------------------------
155
+ /**
156
+ * 处理 form 表单提交事件。
157
+ *
158
+ * 统一使用 form 后,所有值通过 form_value 一次性提交。
159
+ * 解析答案后注入 synthetic message,AI 在新一轮对话中收到答案。
160
+ *
161
+ * @returns 卡片回调响应,或 undefined 表示非本模块的 action
162
+ */
163
+ function handleAskUserAction(data, _cfg, accountId) {
164
+ let action;
165
+ let operationId;
166
+ let senderOpenId;
167
+ let formValue;
168
+ let openChatId;
169
+ try {
170
+ const event = data;
171
+ senderOpenId = (0, card_action_operator_1.resolveCardCallbackOperatorId)(event.operator);
172
+ // open_chat_id may be at top level or inside context (form submit callbacks use context)
173
+ openChatId = event.open_chat_id ?? event.context?.open_chat_id;
174
+ const actionTag = event.action?.tag;
175
+ const actionName = event.action?.name;
176
+ const formName = event.action?.form_name;
177
+ formValue = event.action?.form_value;
178
+ log.info(`card action received: tag=${actionTag}, name=${actionName}, chat=${openChatId}, ` +
179
+ `sender=${senderOpenId}, hasFormValue=${!!formValue}, hasValue=${!!event.action?.value}`);
180
+ // Extract action/operationId from button value (may not propagate for form submit)
181
+ const val = event.action?.value;
182
+ if (val && typeof val === 'object') {
183
+ const valueAction = val.action;
184
+ action = valueAction === ACTION_SUBMIT ? ACTION_SUBMIT : undefined;
185
+ operationId = val.operation_id;
186
+ }
187
+ // Detect form submit by button name
188
+ if (!action && actionName?.startsWith(SUBMIT_BUTTON_PREFIX)) {
189
+ action = ACTION_SUBMIT;
190
+ // Extract questionId from button name: ask_user_submit_<questionId>
191
+ if (!operationId) {
192
+ operationId = actionName.slice(SUBMIT_BUTTON_PREFIX.length);
193
+ }
194
+ }
195
+ // Detect ask-user form submit by tag + formValue. Business plugins also use
196
+ // Feishu forms, so only consume buttons generated by this tool.
197
+ if (!action && actionTag === 'button' && formValue && actionName?.startsWith(SUBMIT_BUTTON_PREFIX)) {
198
+ action = ACTION_SUBMIT;
199
+ }
200
+ // Some SDK versions emit tag='form_submit' without button value/name.
201
+ // Business plugins also use form_submit; a form_name means it belongs to
202
+ // the named business form, while absent form_name keeps the old AskUser
203
+ // chat-scoped fallback for legacy callbacks.
204
+ if (!action &&
205
+ actionTag === 'form_submit' &&
206
+ !formName &&
207
+ (!actionName || actionName.startsWith(SUBMIT_BUTTON_PREFIX))) {
208
+ action = ACTION_SUBMIT;
209
+ if (!formValue && event.action) {
210
+ formValue = event.action;
211
+ }
212
+ }
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
217
+ if (action !== ACTION_SUBMIT)
218
+ return undefined;
219
+ // Look up pending question: try operationId first, then chat-scoped fallback
220
+ let ctx;
221
+ if (operationId) {
222
+ ctx = pendingQuestions.get(operationId);
223
+ }
224
+ if (!ctx && openChatId) {
225
+ // Targeted fallback: exact accountId:chatId match via secondary index
226
+ ctx = findQuestionByChat(accountId, openChatId);
227
+ if (ctx) {
228
+ log.info(`resolved question via chat-scoped fallback: ${ctx.questionId}`);
229
+ }
230
+ }
231
+ if (!ctx) {
232
+ if (operationId) {
233
+ log.warn(`ask-user action: question ${operationId} not found (expired or already handled)`);
234
+ }
235
+ return operationId ? { toast: { type: 'info', content: '该问题已过期或已被回答' } } : undefined;
236
+ }
237
+ if (ctx.submitted) {
238
+ return { toast: { type: 'info', content: '该问题已提交,请等待处理' } };
239
+ }
240
+ if (senderOpenId && ctx.senderOpenId && senderOpenId !== ctx.senderOpenId) {
241
+ return { toast: { type: 'warning', content: '只有被提问的用户可以回答此问题' } };
242
+ }
243
+ if (!formValue) {
244
+ log.warn(`ask-user submit without form_value for question ${operationId}`);
245
+ return { toast: { type: 'error', content: '表单数据丢失,请重试' } };
246
+ }
247
+ log.info(`form_value: ${JSON.stringify(formValue)}`);
248
+ // ---- Parse form_value → answers ----
249
+ const answers = {};
250
+ const unanswered = [];
251
+ for (let i = 0; i < ctx.questions.length; i++) {
252
+ const q = ctx.questions[i];
253
+ let answer;
254
+ if (q.options.length === 0) {
255
+ // Free-text input
256
+ answer = readFormTextField(formValue, getInputFieldName(i));
257
+ }
258
+ else if (q.multiSelect) {
259
+ // Multi-select — either a dropdown (multi_select_static) or a set of
260
+ // independent checker rows, depending on selectStyle.
261
+ const selected = q.selectStyle === 'checkbox'
262
+ ? readFormCheckers(formValue, i, q.options)
263
+ : readFormMultiSelect(formValue, getSelectFieldName(i));
264
+ if (selected.length > 0) {
265
+ answer = selected.join(', ');
266
+ }
267
+ }
268
+ else {
269
+ // Single-select
270
+ answer = readFormTextField(formValue, getSelectFieldName(i));
271
+ }
272
+ if (answer) {
273
+ answers[q.question] = answer;
274
+ }
275
+ else {
276
+ unanswered.push(q.header);
277
+ }
278
+ }
279
+ if (unanswered.length > 0) {
280
+ return {
281
+ toast: { type: 'warning', content: `请先完成: ${unanswered.join(', ')}` },
282
+ };
283
+ }
284
+ // Mark as submitted (guard against double-submit & TTL expiry).
285
+ ctx.submitted = true;
286
+ // Build the intermediate "processing" card for immediate visual feedback.
287
+ const processingCard = buildProcessingCard(ctx.questions, answers);
288
+ // Inject synthetic message with answers. On success, updates card to
289
+ // "answered" and consumes context. On failure, reverts card to submittable
290
+ // form state and resets submitted flag so user can re-submit.
291
+ setImmediate(() => {
292
+ injectAnswerSyntheticMessage(ctx, answers).catch((err) => {
293
+ log.error(`unhandled error in injectAnswerSyntheticMessage: ${err}`);
294
+ });
295
+ });
296
+ log.info(`question ${operationId} submitted, synthetic message will be injected`);
297
+ // Return immediate visual feedback via Feishu callback response:
298
+ // - toast: ephemeral success message for the clicking user
299
+ // - card: replaces card content immediately for the clicking user
300
+ // Note: callback-return card does NOT consume a cardSequence number.
301
+ return {
302
+ toast: {
303
+ type: 'success',
304
+ content: '已收到回答,正在处理...',
305
+ },
306
+ card: {
307
+ type: 'raw',
308
+ data: processingCard,
309
+ },
310
+ };
311
+ }
312
+ // ---------------------------------------------------------------------------
313
+ // Synthetic Message Injection
314
+ // ---------------------------------------------------------------------------
315
+ /**
316
+ * Inject a synthetic message carrying the user's answers so the AI receives
317
+ * them in a new turn. Follows the same pattern as oauth.ts for auth-complete
318
+ * synthetic messages. Retries on failure to prevent answer loss.
319
+ */
320
+ async function injectAnswerSyntheticMessage(ctx, answers) {
321
+ const syntheticMsgId = `${ctx.messageId}:ask-user-answer:${ctx.questionId}`;
322
+ // Format answers as readable text for the AI
323
+ const answerLines = Object.entries(answers)
324
+ .map(([q, a]) => `- ${q}: ${a}`)
325
+ .join('\n');
326
+ const text = `用户回答了你的问题:\n${answerLines}`;
327
+ const syntheticEvent = {
328
+ sender: { sender_id: { open_id: ctx.senderOpenId } },
329
+ message: {
330
+ message_id: syntheticMsgId,
331
+ chat_id: ctx.chatId,
332
+ chat_type: ctx.chatType ?? 'p2p',
333
+ message_type: 'text',
334
+ content: JSON.stringify({ text }),
335
+ thread_id: ctx.threadId,
336
+ },
337
+ };
338
+ const syntheticRuntime = {
339
+ log: (msg) => log.info(msg),
340
+ error: (msg) => log.error(msg),
341
+ };
342
+ // Update card to "processing" state via API so ALL viewers see it (not just
343
+ // the clicking user — the callback-return card field only updates for the
344
+ // clicker). Per Feishu docs, delayed update must execute AFTER the callback
345
+ // response — since we're in setImmediate, the callback has already returned.
346
+ try {
347
+ const processingCard = buildProcessingCard(ctx.questions, answers);
348
+ ctx.cardSequence++;
349
+ await (0, cardkit_1.updateCardKitCard)({
350
+ cfg: ctx.cfg,
351
+ cardId: ctx.cardId,
352
+ card: processingCard,
353
+ sequence: ctx.cardSequence,
354
+ accountId: ctx.accountId,
355
+ });
356
+ }
357
+ catch (err) {
358
+ log.warn(`failed to update card to processing state via API: ${err}`);
359
+ // Non-fatal: the clicking user already sees the processing state via callback return.
360
+ }
361
+ let lastError;
362
+ for (let attempt = 0; attempt <= INJECT_MAX_RETRIES; attempt++) {
363
+ if (attempt > 0) {
364
+ log.info(`retrying synthetic message injection (attempt ${attempt + 1}) for question ${ctx.questionId}`);
365
+ await new Promise((r) => setTimeout(r, INJECT_RETRY_DELAY_MS));
366
+ }
367
+ try {
368
+ const { status, promise } = (0, chat_queue_1.enqueueFeishuChatTask)({
369
+ accountId: ctx.accountId,
370
+ chatId: ctx.chatId,
371
+ threadId: ctx.threadId,
372
+ task: async () => {
373
+ await (0, lark_ticket_1.withTicket)({
374
+ messageId: syntheticMsgId,
375
+ chatId: ctx.chatId,
376
+ accountId: ctx.accountId,
377
+ startTime: Date.now(),
378
+ senderOpenId: ctx.senderOpenId,
379
+ chatType: ctx.chatType,
380
+ threadId: ctx.threadId,
381
+ }, () => (0, handler_1.handleFeishuMessage)({
382
+ cfg: ctx.cfg,
383
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
384
+ event: syntheticEvent,
385
+ accountId: ctx.accountId,
386
+ forceMention: true,
387
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
388
+ runtime: syntheticRuntime,
389
+ replyToMessageId: ctx.messageId,
390
+ }));
391
+ },
392
+ });
393
+ // Wait for the task to actually execute (not just enqueue)
394
+ await promise;
395
+ consumePendingQuestion(ctx.questionId);
396
+ log.info(`synthetic answer message dispatched (${status}) for question ${ctx.questionId}`);
397
+ // Update card to answered state only after synthetic message succeeds.
398
+ // This ensures the card stays submittable for retry if injection fails.
399
+ try {
400
+ await updateCardToAnswered(ctx, answers);
401
+ }
402
+ catch (err) {
403
+ log.warn(`failed to update card to answered state: ${err}`);
404
+ }
405
+ return; // success
406
+ }
407
+ catch (err) {
408
+ lastError = err;
409
+ log.warn(`synthetic message injection attempt ${attempt + 1} failed: ${err}`);
410
+ }
411
+ }
412
+ // All retries exhausted — reset submitted flag so user can retry via card,
413
+ // revert card to submittable form state, and re-arm TTL.
414
+ ctx.submitted = false;
415
+ armTtlTimer(ctx, PENDING_QUESTION_TTL_MS);
416
+ log.error(`synthetic message injection failed after ${INJECT_MAX_RETRIES + 1} attempts for question ${ctx.questionId}: ${lastError}`);
417
+ // Revert card from "processing" back to interactive form so user can retry.
418
+ try {
419
+ await updateCardToSubmittable(ctx);
420
+ log.info(`reverted card to submittable state for question ${ctx.questionId}`);
421
+ }
422
+ catch (err) {
423
+ log.warn(`failed to revert card to submittable state: ${err}`);
424
+ }
425
+ }
426
+ // ---------------------------------------------------------------------------
427
+ // Form value readers
428
+ // ---------------------------------------------------------------------------
429
+ function readFormTextField(formValue, fieldName) {
430
+ const value = formValue[fieldName];
431
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
432
+ }
433
+ function readFormMultiSelect(formValue, fieldName) {
434
+ const raw = formValue[fieldName];
435
+ if (Array.isArray(raw)) {
436
+ return raw.filter((v) => typeof v === 'string' && v.trim().length > 0);
437
+ }
438
+ if (typeof raw === 'string' && raw.trim()) {
439
+ try {
440
+ const parsed = JSON.parse(raw.trim());
441
+ if (Array.isArray(parsed)) {
442
+ return parsed.filter((v) => typeof v === 'string' && v.trim().length > 0);
443
+ }
444
+ }
445
+ catch {
446
+ // not JSON
447
+ }
448
+ return [raw.trim()];
449
+ }
450
+ return [];
451
+ }
452
+ /**
453
+ * Read the selected labels from a set of independent checker (checkbox)
454
+ * components — used when a multi-select question uses selectStyle 'checkbox'.
455
+ * Each option has its own boolean field (see getCheckerFieldName); Feishu may
456
+ * report the checked state as a boolean or a string.
457
+ */
458
+ function readFormCheckers(formValue, questionIndex, options) {
459
+ const selected = [];
460
+ for (let optIndex = 0; optIndex < options.length; optIndex++) {
461
+ const raw = formValue[getCheckerFieldName(questionIndex, optIndex)];
462
+ if (raw === true || raw === 'true') {
463
+ selected.push(options[optIndex].label);
464
+ }
465
+ }
466
+ return selected;
467
+ }
468
+ // ---------------------------------------------------------------------------
469
+ // Card Builders — unified form layout
470
+ // ---------------------------------------------------------------------------
471
+ /**
472
+ * Build a left-right row: label on left, control on right.
473
+ */
474
+ function buildLabeledRow(label, control) {
475
+ return {
476
+ tag: 'column_set',
477
+ flex_mode: 'stretch',
478
+ horizontal_spacing: '8px',
479
+ margin: '12px 0 0 0',
480
+ columns: [
481
+ {
482
+ tag: 'column',
483
+ width: 'weighted',
484
+ weight: 1,
485
+ vertical_align: 'center',
486
+ elements: [label],
487
+ },
488
+ {
489
+ tag: 'column',
490
+ width: 'weighted',
491
+ weight: 3,
492
+ vertical_align: 'center',
493
+ elements: [control],
494
+ },
495
+ ],
496
+ };
497
+ }
498
+ /**
499
+ * Build form elements for a single question.
500
+ *
501
+ * All controls use `name` for form_value collection. No `value` property
502
+ * is set on interactive components — they do not fire individual callbacks.
503
+ */
504
+ function buildQuestionFormElements(q, questionIndex) {
505
+ const elems = [];
506
+ const labelMd = { tag: 'markdown', content: `**${q.header}**` };
507
+ // Question description as subtitle
508
+ if (q.question && q.question !== q.header) {
509
+ elems.push({ tag: 'markdown', content: q.question, text_size: 'notation' });
510
+ }
511
+ if (q.options.length === 0) {
512
+ // ---- Free-text input ----
513
+ elems.push(buildLabeledRow(labelMd, {
514
+ tag: 'input',
515
+ name: getInputFieldName(questionIndex),
516
+ placeholder: {
517
+ tag: 'plain_text',
518
+ content: '请输入...',
519
+ i18n_content: { zh_cn: '请输入...', en_us: 'Type your answer...' },
520
+ },
521
+ }));
522
+ return elems;
523
+ }
524
+ // ---- Build option list ----
525
+ const selectOptions = q.options.map((opt) => ({
526
+ text: { tag: 'plain_text', content: opt.label },
527
+ value: opt.label,
528
+ }));
529
+ if (q.multiSelect && q.selectStyle === 'checkbox') {
530
+ // ---- Multi-select as independent checkbox rows ----
531
+ // Each option is its own checker so the user sees every selection at a
532
+ // glance before submitting (vs. the compact dropdown collapsed state).
533
+ // Note: checker requires a `value` field to pass Feishu's client-side
534
+ // interactive-component validation (otherwise code 200340).
535
+ elems.push(labelMd);
536
+ q.options.forEach((opt, optIndex) => {
537
+ elems.push({
538
+ tag: 'checker',
539
+ name: getCheckerFieldName(questionIndex, optIndex),
540
+ checked: false,
541
+ text: { tag: 'plain_text', content: opt.label },
542
+ value: { option: opt.label },
543
+ });
544
+ });
545
+ }
546
+ else if (q.multiSelect) {
547
+ // ---- Multi-select dropdown ----
548
+ elems.push(buildLabeledRow(labelMd, {
549
+ tag: 'multi_select_static',
550
+ name: getSelectFieldName(questionIndex),
551
+ placeholder: {
552
+ tag: 'plain_text',
553
+ content: '请选择...',
554
+ i18n_content: { zh_cn: '请选择...', en_us: 'Select options...' },
555
+ },
556
+ options: selectOptions,
557
+ }));
558
+ }
559
+ else {
560
+ // ---- Single-select dropdown ----
561
+ elems.push(buildLabeledRow(labelMd, {
562
+ tag: 'select_static',
563
+ name: getSelectFieldName(questionIndex),
564
+ placeholder: {
565
+ tag: 'plain_text',
566
+ content: '请选择...',
567
+ i18n_content: { zh_cn: '请选择...', en_us: 'Select an option...' },
568
+ },
569
+ options: selectOptions,
570
+ }));
571
+ }
572
+ // ---- Option descriptions ----
573
+ const descLines = q.options.filter((opt) => opt.description).map((opt) => `• **${opt.label}**: ${opt.description}`);
574
+ if (descLines.length > 0) {
575
+ elems.push({ tag: 'markdown', content: descLines.join('\n'), text_size: 'notation' });
576
+ }
577
+ return elems;
578
+ }
579
+ /**
580
+ * Build the full interactive ask-user card.
581
+ *
582
+ * All elements are wrapped in a single `form` container.
583
+ * Submit button uses `form_action_type: "submit"` to collect all values.
584
+ */
585
+ function buildAskUserCard(questions, questionId) {
586
+ const formElements = [];
587
+ for (let i = 0; i < questions.length; i++) {
588
+ if (i > 0) {
589
+ formElements.push({ tag: 'hr' });
590
+ }
591
+ formElements.push(...buildQuestionFormElements(questions[i], i));
592
+ }
593
+ // Submit button.
594
+ // questionId is encoded in both button name and value for resilience:
595
+ // - `value` is the primary carrier and is included in card.action.trigger
596
+ // callbacks per Feishu's documented event structure.
597
+ // - `name` acts as a fallback in case the SDK version strips value
598
+ // from form_submit events.
599
+ // Both are needed because a button without a `value` field fails
600
+ // Feishu's client-side interactive-component validation with code 200340.
601
+ formElements.push({ tag: 'hr' });
602
+ formElements.push({
603
+ tag: 'button',
604
+ name: `${SUBMIT_BUTTON_PREFIX}${questionId}`,
605
+ value: { action: 'ask_user_submit', operation_id: questionId },
606
+ text: {
607
+ tag: 'plain_text',
608
+ content: '📮 提交',
609
+ i18n_content: { zh_cn: '📮 提交', en_us: '📮 Submit' },
610
+ },
611
+ type: 'primary',
612
+ form_action_type: 'submit',
613
+ });
614
+ return {
615
+ schema: '2.0',
616
+ config: V2_CONFIG,
617
+ header: {
618
+ title: {
619
+ tag: 'plain_text',
620
+ content: '需要你的确认',
621
+ i18n_content: { zh_cn: '需要你的确认', en_us: 'Your Input Needed' },
622
+ },
623
+ subtitle: {
624
+ tag: 'plain_text',
625
+ content: `共 ${questions.length} 个问题`,
626
+ i18n_content: {
627
+ zh_cn: `共 ${questions.length} 个问题`,
628
+ en_us: `${questions.length} question${questions.length > 1 ? 's' : ''}`,
629
+ },
630
+ },
631
+ text_tag_list: [
632
+ {
633
+ tag: 'text_tag',
634
+ text: { tag: 'plain_text', content: '待回答' },
635
+ color: 'blue',
636
+ },
637
+ ],
638
+ template: 'blue',
639
+ },
640
+ body: {
641
+ elements: [
642
+ {
643
+ tag: 'form',
644
+ name: 'ask_user_form',
645
+ elements: formElements,
646
+ },
647
+ ],
648
+ },
649
+ };
650
+ }
651
+ function buildAnsweredCard(questions, answers) {
652
+ const elements = [];
653
+ for (let i = 0; i < questions.length; i++) {
654
+ const q = questions[i];
655
+ const answer = answers[q.question] ?? '(no answer)';
656
+ if (i > 0) {
657
+ elements.push({ tag: 'hr' });
658
+ }
659
+ elements.push(buildLabeledRow({ tag: 'markdown', content: `**${q.header}**` }, { tag: 'markdown', content: `✅ **${answer}**` }));
660
+ }
661
+ return {
662
+ schema: '2.0',
663
+ config: V2_CONFIG,
664
+ header: {
665
+ title: {
666
+ tag: 'plain_text',
667
+ content: '已收到回答',
668
+ i18n_content: { zh_cn: '已收到回答', en_us: 'Response Received' },
669
+ },
670
+ subtitle: {
671
+ tag: 'plain_text',
672
+ content: `共 ${questions.length} 个问题`,
673
+ i18n_content: {
674
+ zh_cn: `共 ${questions.length} 个问题`,
675
+ en_us: `${questions.length} question${questions.length > 1 ? 's' : ''}`,
676
+ },
677
+ },
678
+ text_tag_list: [
679
+ {
680
+ tag: 'text_tag',
681
+ text: { tag: 'plain_text', content: '已完成' },
682
+ color: 'green',
683
+ },
684
+ ],
685
+ template: 'green',
686
+ },
687
+ body: { elements },
688
+ };
689
+ }
690
+ function buildProcessingCard(questions, answers) {
691
+ const elements = [];
692
+ for (let i = 0; i < questions.length; i++) {
693
+ const q = questions[i];
694
+ const answer = answers[q.question] ?? '(no answer)';
695
+ if (i > 0) {
696
+ elements.push({ tag: 'hr' });
697
+ }
698
+ elements.push(buildLabeledRow({ tag: 'markdown', content: `**${q.header}**` }, { tag: 'markdown', content: `⏳ **${answer}**` }));
699
+ }
700
+ elements.push({
701
+ tag: 'markdown',
702
+ content: '正在处理你的回答...',
703
+ text_size: 'notation',
704
+ });
705
+ return {
706
+ schema: '2.0',
707
+ config: V2_CONFIG,
708
+ header: {
709
+ title: {
710
+ tag: 'plain_text',
711
+ content: '已提交回答',
712
+ i18n_content: { zh_cn: '已提交回答', en_us: 'Response Submitted' },
713
+ },
714
+ subtitle: {
715
+ tag: 'plain_text',
716
+ content: `共 ${questions.length} 个问题 · 正在处理`,
717
+ i18n_content: {
718
+ zh_cn: `共 ${questions.length} 个问题 · 正在处理`,
719
+ en_us: `${questions.length} question${questions.length > 1 ? 's' : ''} · Processing`,
720
+ },
721
+ },
722
+ text_tag_list: [
723
+ {
724
+ tag: 'text_tag',
725
+ text: { tag: 'plain_text', content: '处理中' },
726
+ color: 'turquoise',
727
+ },
728
+ ],
729
+ template: 'turquoise',
730
+ },
731
+ body: { elements },
732
+ };
733
+ }
734
+ function buildExpiredCard(questions) {
735
+ const elements = [];
736
+ for (let i = 0; i < questions.length; i++) {
737
+ const q = questions[i];
738
+ if (i > 0) {
739
+ elements.push({ tag: 'hr' });
740
+ }
741
+ elements.push(buildLabeledRow({ tag: 'markdown', content: `**${q.header}**` }, { tag: 'markdown', content: q.question }));
742
+ }
743
+ elements.push({
744
+ tag: 'markdown',
745
+ content: '⏱ 该问题已过期',
746
+ i18n_content: { zh_cn: '⏱ 该问题已过期', en_us: '⏱ This question has expired' },
747
+ text_size: 'notation',
748
+ });
749
+ return {
750
+ schema: '2.0',
751
+ config: V2_CONFIG,
752
+ header: {
753
+ title: {
754
+ tag: 'plain_text',
755
+ content: '问题已过期',
756
+ i18n_content: { zh_cn: '问题已过期', en_us: 'Question Expired' },
757
+ },
758
+ subtitle: {
759
+ tag: 'plain_text',
760
+ content: '未在规定时间内回答',
761
+ i18n_content: { zh_cn: '未在规定时间内回答', en_us: 'No response within time limit' },
762
+ },
763
+ text_tag_list: [
764
+ {
765
+ tag: 'text_tag',
766
+ text: { tag: 'plain_text', content: '已过期' },
767
+ color: 'neutral',
768
+ },
769
+ ],
770
+ template: 'grey',
771
+ },
772
+ body: { elements },
773
+ };
774
+ }
775
+ // ---------------------------------------------------------------------------
776
+ // Card Update Helpers
777
+ // ---------------------------------------------------------------------------
778
+ async function updateCardToAnswered(ctx, answers) {
779
+ const card = buildAnsweredCard(ctx.questions, answers);
780
+ ctx.cardSequence++;
781
+ await (0, cardkit_1.updateCardKitCard)({
782
+ cfg: ctx.cfg,
783
+ cardId: ctx.cardId,
784
+ card,
785
+ sequence: ctx.cardSequence,
786
+ accountId: ctx.accountId,
787
+ });
788
+ }
789
+ async function updateCardToExpired(ctx) {
790
+ const card = buildExpiredCard(ctx.questions);
791
+ ctx.cardSequence++;
792
+ await (0, cardkit_1.updateCardKitCard)({
793
+ cfg: ctx.cfg,
794
+ cardId: ctx.cardId,
795
+ card,
796
+ sequence: ctx.cardSequence,
797
+ accountId: ctx.accountId,
798
+ });
799
+ }
800
+ async function updateCardToSubmittable(ctx) {
801
+ const card = buildAskUserCard(ctx.questions, ctx.questionId);
802
+ ctx.cardSequence++;
803
+ await (0, cardkit_1.updateCardKitCard)({
804
+ cfg: ctx.cfg,
805
+ cardId: ctx.cardId,
806
+ card,
807
+ sequence: ctx.cardSequence,
808
+ accountId: ctx.accountId,
809
+ });
810
+ }
811
+ // ---------------------------------------------------------------------------
812
+ // Tool Schema
813
+ // ---------------------------------------------------------------------------
814
+ const AskUserQuestionSchema = typebox_1.Type.Object({
815
+ questions: typebox_1.Type.Array(typebox_1.Type.Object({
816
+ question: typebox_1.Type.String({ description: 'The question to ask the user' }),
817
+ header: typebox_1.Type.String({ description: 'Short label for the question (max 12 chars)' }),
818
+ options: typebox_1.Type.Array(typebox_1.Type.Object({
819
+ label: typebox_1.Type.String({ description: 'Display text for this option' }),
820
+ description: typebox_1.Type.String({ description: 'Explanation of what this option means' }),
821
+ }), {
822
+ description: 'Available choices. Renders as a dropdown. ' +
823
+ 'Leave empty ([]) for free-text input — the user will see a text field instead.',
824
+ maxItems: 10,
825
+ }),
826
+ multiSelect: typebox_1.Type.Boolean({
827
+ description: 'Whether multiple options can be selected (ignored when options is empty)',
828
+ }),
829
+ selectStyle: typebox_1.Type.Optional(typebox_1.Type.Union([typebox_1.Type.Literal('dropdown'), typebox_1.Type.Literal('checkbox')], {
830
+ description: 'For multi-select questions only: "dropdown" (default, compact multi-select dropdown) or "checkbox" (each option shown as a separate checkbox row, so the user can see all selected options at a glance before submitting). Ignored for single-select and free-text questions.',
831
+ })),
832
+ }), {
833
+ description: 'Questions to ask the user (1-6 questions)',
834
+ minItems: 1,
835
+ maxItems: 6,
836
+ }),
837
+ });
838
+ // ---------------------------------------------------------------------------
839
+ // Tool Registration
840
+ // ---------------------------------------------------------------------------
841
+ function registerAskUserQuestionTool(api) {
842
+ const toolName = 'feishu_ask_user_question';
843
+ if (!(0, helpers_1.checkToolRegistration)(api, toolName))
844
+ return;
845
+ const cfg = api.config;
846
+ api.registerTool({
847
+ name: toolName,
848
+ label: 'Ask User Question',
849
+ description: 'Ask the user a question via an interactive Feishu card. ' +
850
+ 'Returns immediately after sending the card. ' +
851
+ "The user's answers will arrive as a new message in the conversation. " +
852
+ 'Do NOT poll or re-call this tool — just wait for the response message. ' +
853
+ 'For selection questions, provide options (renders as dropdown). ' +
854
+ 'For free-text input, set options to an empty array.',
855
+ parameters: AskUserQuestionSchema,
856
+ async execute(_toolCallId, params) {
857
+ const { questions } = params;
858
+ const ticket = (0, lark_ticket_1.getTicket)();
859
+ if (!ticket) {
860
+ return (0, helpers_1.formatToolError)('AskUserQuestion can only be used in a Feishu message context');
861
+ }
862
+ const { chatId, accountId, senderOpenId, threadId } = ticket;
863
+ if (!senderOpenId) {
864
+ return (0, helpers_1.formatToolError)('Cannot determine the target user (no senderOpenId in ticket)');
865
+ }
866
+ const questionId = (0, node_crypto_1.randomUUID)();
867
+ log.info(`creating ask-user-question: id=${questionId}, questions=${questions.length}, chat=${chatId}`);
868
+ // 1. Build and send card
869
+ const card = buildAskUserCard(questions, questionId);
870
+ let cardId;
871
+ try {
872
+ cardId = await (0, cardkit_1.createCardEntity)({ cfg, card, accountId });
873
+ }
874
+ catch (err) {
875
+ log.error(`failed to create card entity: ${err}`);
876
+ return (0, helpers_1.formatToolError)(`Failed to create question card: ${err}`);
877
+ }
878
+ if (!cardId) {
879
+ return (0, helpers_1.formatToolError)('Failed to create question card: no card_id returned');
880
+ }
881
+ try {
882
+ await (0, cardkit_1.sendCardByCardId)({
883
+ cfg,
884
+ to: chatId,
885
+ cardId,
886
+ replyToMessageId: ticket.messageId,
887
+ replyInThread: Boolean(threadId),
888
+ accountId,
889
+ });
890
+ }
891
+ catch (err) {
892
+ log.error(`failed to send card: ${err}`);
893
+ return (0, helpers_1.formatToolError)(`Failed to send question card: ${err}`);
894
+ }
895
+ // 2. Store context for card action handler to inject synthetic message
896
+ storePendingQuestion({
897
+ questionId,
898
+ chatId,
899
+ accountId,
900
+ senderOpenId,
901
+ cardId,
902
+ cfg,
903
+ questions,
904
+ threadId,
905
+ chatType: ticket.chatType,
906
+ messageId: ticket.messageId,
907
+ cardSequence: 1,
908
+ submitted: false,
909
+ });
910
+ // 3. Return immediately — answers will arrive via synthetic message
911
+ log.info(`question ${questionId} card sent, returning pending status`);
912
+ return (0, helpers_1.formatToolResult)({
913
+ status: 'pending',
914
+ questionId,
915
+ message: 'Question card sent to the user. Their answers will arrive as a follow-up message ' +
916
+ 'in this conversation. Do NOT call this tool again for the same question — just wait ' +
917
+ 'for the response message.',
918
+ });
919
+ },
920
+ });
921
+ api.logger.debug?.(`${toolName}: registered tool`);
922
+ }