@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/openclaw-lark.js +39 -0
- package/index.d.ts +36 -0
- package/index.js +188 -0
- package/openclaw.plugin.json +64 -0
- package/package.json +63 -0
- package/secret-contract-api.d.ts +15 -0
- package/secret-contract-api.js +78 -0
- package/skills/feishu-bitable/SKILL.md +248 -0
- package/skills/feishu-bitable/references/examples.md +813 -0
- package/skills/feishu-bitable/references/field-properties.md +763 -0
- package/skills/feishu-bitable/references/record-values.md +911 -0
- package/skills/feishu-calendar/SKILL.md +242 -0
- package/skills/feishu-channel-rules/SKILL.md +18 -0
- package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
- package/skills/feishu-create-doc/SKILL.md +719 -0
- package/skills/feishu-fetch-doc/SKILL.md +93 -0
- package/skills/feishu-im-read/SKILL.md +163 -0
- package/skills/feishu-task/SKILL.md +340 -0
- package/skills/feishu-troubleshoot/SKILL.md +70 -0
- package/skills/feishu-update-doc/SKILL.md +285 -0
- package/src/card/builder.d.ts +152 -0
- package/src/card/builder.js +794 -0
- package/src/card/card-error.d.ts +91 -0
- package/src/card/card-error.js +206 -0
- package/src/card/cardkit.d.ts +90 -0
- package/src/card/cardkit.js +203 -0
- package/src/card/flush-controller.d.ts +45 -0
- package/src/card/flush-controller.js +138 -0
- package/src/card/image-resolver.d.ts +45 -0
- package/src/card/image-resolver.js +116 -0
- package/src/card/markdown-style.d.ts +16 -0
- package/src/card/markdown-style.js +106 -0
- package/src/card/reasoning-utils.d.ts +14 -0
- package/src/card/reasoning-utils.js +64 -0
- package/src/card/reply-dispatcher-types.d.ts +132 -0
- package/src/card/reply-dispatcher-types.js +61 -0
- package/src/card/reply-dispatcher.d.ts +15 -0
- package/src/card/reply-dispatcher.js +447 -0
- package/src/card/reply-mode.d.ts +41 -0
- package/src/card/reply-mode.js +70 -0
- package/src/card/streaming-card-controller.d.ts +118 -0
- package/src/card/streaming-card-controller.js +1045 -0
- package/src/card/tool-use-config.d.ts +26 -0
- package/src/card/tool-use-config.js +76 -0
- package/src/card/tool-use-display.d.ts +37 -0
- package/src/card/tool-use-display.js +476 -0
- package/src/card/tool-use-trace-store.d.ts +51 -0
- package/src/card/tool-use-trace-store.js +271 -0
- package/src/card/unavailable-guard.d.ts +35 -0
- package/src/card/unavailable-guard.js +87 -0
- package/src/channel/abort-detect.d.ts +47 -0
- package/src/channel/abort-detect.js +219 -0
- package/src/channel/chat-queue.d.ts +41 -0
- package/src/channel/chat-queue.js +68 -0
- package/src/channel/config-adapter.d.ts +23 -0
- package/src/channel/config-adapter.js +107 -0
- package/src/channel/directory.d.ts +57 -0
- package/src/channel/directory.js +197 -0
- package/src/channel/event-handlers.d.ts +17 -0
- package/src/channel/event-handlers.js +380 -0
- package/src/channel/interactive-dispatch.d.ts +59 -0
- package/src/channel/interactive-dispatch.js +195 -0
- package/src/channel/monitor.d.ts +17 -0
- package/src/channel/monitor.js +140 -0
- package/src/channel/onboarding-config.d.ts +18 -0
- package/src/channel/onboarding-config.js +96 -0
- package/src/channel/onboarding-migrate.d.ts +25 -0
- package/src/channel/onboarding-migrate.js +70 -0
- package/src/channel/onboarding.d.ts +12 -0
- package/src/channel/onboarding.js +300 -0
- package/src/channel/plugin.d.ts +13 -0
- package/src/channel/plugin.js +310 -0
- package/src/channel/probe.d.ts +14 -0
- package/src/channel/probe.js +24 -0
- package/src/channel/types.d.ts +37 -0
- package/src/channel/types.js +8 -0
- package/src/commands/auth.d.ts +21 -0
- package/src/commands/auth.js +165 -0
- package/src/commands/diagnose.d.ts +69 -0
- package/src/commands/diagnose.js +848 -0
- package/src/commands/doctor.d.ts +27 -0
- package/src/commands/doctor.js +588 -0
- package/src/commands/index.d.ts +25 -0
- package/src/commands/index.js +219 -0
- package/src/commands/locale.d.ts +7 -0
- package/src/commands/locale.js +8 -0
- package/src/core/accounts.d.ts +51 -0
- package/src/core/accounts.js +219 -0
- package/src/core/agent-config.d.ts +100 -0
- package/src/core/agent-config.js +148 -0
- package/src/core/api-error.d.ts +48 -0
- package/src/core/api-error.js +117 -0
- package/src/core/app-owner-fallback.d.ts +22 -0
- package/src/core/app-owner-fallback.js +39 -0
- package/src/core/app-scope-checker.d.ts +87 -0
- package/src/core/app-scope-checker.js +198 -0
- package/src/core/auth-errors.d.ts +144 -0
- package/src/core/auth-errors.js +160 -0
- package/src/core/card-action-operator.d.ts +33 -0
- package/src/core/card-action-operator.js +30 -0
- package/src/core/chat-info-cache.d.ts +67 -0
- package/src/core/chat-info-cache.js +165 -0
- package/src/core/comment-target.d.ts +65 -0
- package/src/core/comment-target.js +100 -0
- package/src/core/config-schema.d.ts +490 -0
- package/src/core/config-schema.js +223 -0
- package/src/core/device-flow.d.ts +77 -0
- package/src/core/device-flow.js +217 -0
- package/src/core/domains.d.ts +18 -0
- package/src/core/domains.js +34 -0
- package/src/core/feishu-fetch.d.ts +18 -0
- package/src/core/feishu-fetch.js +28 -0
- package/src/core/footer-config.d.ts +24 -0
- package/src/core/footer-config.js +51 -0
- package/src/core/lark-client.d.ts +125 -0
- package/src/core/lark-client.js +468 -0
- package/src/core/lark-logger.d.ts +23 -0
- package/src/core/lark-logger.js +160 -0
- package/src/core/lark-ticket.d.ts +29 -0
- package/src/core/lark-ticket.js +40 -0
- package/src/core/message-unavailable.d.ts +53 -0
- package/src/core/message-unavailable.js +142 -0
- package/src/core/owner-policy.d.ts +32 -0
- package/src/core/owner-policy.js +55 -0
- package/src/core/permission-url.d.ts +22 -0
- package/src/core/permission-url.js +76 -0
- package/src/core/raw-request.d.ts +27 -0
- package/src/core/raw-request.js +90 -0
- package/src/core/runtime-store.d.ts +13 -0
- package/src/core/runtime-store.js +29 -0
- package/src/core/scope-manager.d.ts +168 -0
- package/src/core/scope-manager.js +224 -0
- package/src/core/sdk-compat.d.ts +20 -0
- package/src/core/sdk-compat.js +28 -0
- package/src/core/security-check.d.ts +72 -0
- package/src/core/security-check.js +184 -0
- package/src/core/shutdown-hooks.d.ts +22 -0
- package/src/core/shutdown-hooks.js +61 -0
- package/src/core/synthetic-target.d.ts +33 -0
- package/src/core/synthetic-target.js +40 -0
- package/src/core/targets.d.ts +60 -0
- package/src/core/targets.js +173 -0
- package/src/core/token-store.d.ts +54 -0
- package/src/core/token-store.js +326 -0
- package/src/core/tool-client.d.ts +176 -0
- package/src/core/tool-client.js +426 -0
- package/src/core/tool-scopes.d.ts +156 -0
- package/src/core/tool-scopes.js +344 -0
- package/src/core/tools-config.d.ts +55 -0
- package/src/core/tools-config.js +143 -0
- package/src/core/types.d.ts +91 -0
- package/src/core/types.js +12 -0
- package/src/core/uat-client.d.ts +46 -0
- package/src/core/uat-client.js +192 -0
- package/src/core/version.d.ts +31 -0
- package/src/core/version.js +69 -0
- package/src/messaging/converters/audio.d.ts +8 -0
- package/src/messaging/converters/audio.js +24 -0
- package/src/messaging/converters/calendar.d.ts +13 -0
- package/src/messaging/converters/calendar.js +56 -0
- package/src/messaging/converters/content-converter-helpers.d.ts +30 -0
- package/src/messaging/converters/content-converter-helpers.js +82 -0
- package/src/messaging/converters/content-converter.d.ts +24 -0
- package/src/messaging/converters/content-converter.js +40 -0
- package/src/messaging/converters/file.d.ts +8 -0
- package/src/messaging/converters/file.js +24 -0
- package/src/messaging/converters/folder.d.ts +8 -0
- package/src/messaging/converters/folder.js +24 -0
- package/src/messaging/converters/hongbao.d.ts +8 -0
- package/src/messaging/converters/hongbao.js +20 -0
- package/src/messaging/converters/image.d.ts +8 -0
- package/src/messaging/converters/image.js +22 -0
- package/src/messaging/converters/index.d.ts +8 -0
- package/src/messaging/converters/index.js +53 -0
- package/src/messaging/converters/interactive/card-converter.d.ts +76 -0
- package/src/messaging/converters/interactive/card-converter.js +1177 -0
- package/src/messaging/converters/interactive/card-utils.d.ts +9 -0
- package/src/messaging/converters/interactive/card-utils.js +47 -0
- package/src/messaging/converters/interactive/index.d.ts +8 -0
- package/src/messaging/converters/interactive/index.js +25 -0
- package/src/messaging/converters/interactive/legacy.d.ts +11 -0
- package/src/messaging/converters/interactive/legacy.js +60 -0
- package/src/messaging/converters/interactive/types.d.ts +23 -0
- package/src/messaging/converters/interactive/types.js +27 -0
- package/src/messaging/converters/location.d.ts +8 -0
- package/src/messaging/converters/location.js +23 -0
- package/src/messaging/converters/merge-forward.d.ts +32 -0
- package/src/messaging/converters/merge-forward.js +235 -0
- package/src/messaging/converters/post.d.ts +11 -0
- package/src/messaging/converters/post.js +235 -0
- package/src/messaging/converters/share.d.ts +9 -0
- package/src/messaging/converters/share.js +28 -0
- package/src/messaging/converters/sticker.d.ts +8 -0
- package/src/messaging/converters/sticker.js +22 -0
- package/src/messaging/converters/system.d.ts +12 -0
- package/src/messaging/converters/system.js +36 -0
- package/src/messaging/converters/text.d.ts +8 -0
- package/src/messaging/converters/text.js +18 -0
- package/src/messaging/converters/todo.d.ts +8 -0
- package/src/messaging/converters/todo.js +45 -0
- package/src/messaging/converters/types.d.ts +114 -0
- package/src/messaging/converters/types.js +8 -0
- package/src/messaging/converters/unknown.d.ts +8 -0
- package/src/messaging/converters/unknown.js +20 -0
- package/src/messaging/converters/utils.d.ts +22 -0
- package/src/messaging/converters/utils.js +57 -0
- package/src/messaging/converters/video-chat.d.ts +8 -0
- package/src/messaging/converters/video-chat.js +31 -0
- package/src/messaging/converters/video.d.ts +8 -0
- package/src/messaging/converters/video.js +35 -0
- package/src/messaging/converters/vote.d.ts +8 -0
- package/src/messaging/converters/vote.js +28 -0
- package/src/messaging/inbound/bot-content.d.ts +84 -0
- package/src/messaging/inbound/bot-content.js +117 -0
- package/src/messaging/inbound/bot-loop-guard.d.ts +48 -0
- package/src/messaging/inbound/bot-loop-guard.js +89 -0
- package/src/messaging/inbound/comment-context.d.ts +82 -0
- package/src/messaging/inbound/comment-context.js +353 -0
- package/src/messaging/inbound/comment-handler.d.ts +30 -0
- package/src/messaging/inbound/comment-handler.js +269 -0
- package/src/messaging/inbound/dedup.d.ts +59 -0
- package/src/messaging/inbound/dedup.js +121 -0
- package/src/messaging/inbound/dispatch-builders.d.ts +104 -0
- package/src/messaging/inbound/dispatch-builders.js +241 -0
- package/src/messaging/inbound/dispatch-commands.d.ts +22 -0
- package/src/messaging/inbound/dispatch-commands.js +131 -0
- package/src/messaging/inbound/dispatch-context.d.ts +67 -0
- package/src/messaging/inbound/dispatch-context.js +153 -0
- package/src/messaging/inbound/dispatch.d.ts +50 -0
- package/src/messaging/inbound/dispatch.js +477 -0
- package/src/messaging/inbound/enrich.d.ts +102 -0
- package/src/messaging/inbound/enrich.js +236 -0
- package/src/messaging/inbound/gate-effects.d.ts +23 -0
- package/src/messaging/inbound/gate-effects.js +46 -0
- package/src/messaging/inbound/gate.d.ts +91 -0
- package/src/messaging/inbound/gate.js +342 -0
- package/src/messaging/inbound/handler-registry.d.ts +25 -0
- package/src/messaging/inbound/handler-registry.js +23 -0
- package/src/messaging/inbound/handler.d.ts +37 -0
- package/src/messaging/inbound/handler.js +286 -0
- package/src/messaging/inbound/media-resolver.d.ts +32 -0
- package/src/messaging/inbound/media-resolver.js +91 -0
- package/src/messaging/inbound/mention-registry.d.ts +59 -0
- package/src/messaging/inbound/mention-registry.js +115 -0
- package/src/messaging/inbound/mention.d.ts +48 -0
- package/src/messaging/inbound/mention.js +102 -0
- package/src/messaging/inbound/parse-io.d.ts +50 -0
- package/src/messaging/inbound/parse-io.js +86 -0
- package/src/messaging/inbound/parse.d.ts +28 -0
- package/src/messaging/inbound/parse.js +128 -0
- package/src/messaging/inbound/permission.d.ts +17 -0
- package/src/messaging/inbound/permission.js +44 -0
- package/src/messaging/inbound/policy.d.ts +95 -0
- package/src/messaging/inbound/policy.js +168 -0
- package/src/messaging/inbound/reaction-handler.d.ts +62 -0
- package/src/messaging/inbound/reaction-handler.js +259 -0
- package/src/messaging/inbound/sentinel-store.d.ts +37 -0
- package/src/messaging/inbound/sentinel-store.js +98 -0
- package/src/messaging/inbound/synthetic-message.d.ts +26 -0
- package/src/messaging/inbound/synthetic-message.js +59 -0
- package/src/messaging/inbound/user-name-cache-store.d.ts +62 -0
- package/src/messaging/inbound/user-name-cache-store.js +236 -0
- package/src/messaging/inbound/user-name-cache.d.ts +78 -0
- package/src/messaging/inbound/user-name-cache.js +306 -0
- package/src/messaging/inbound/vc-meeting-invited-handler.d.ts +20 -0
- package/src/messaging/inbound/vc-meeting-invited-handler.js +236 -0
- package/src/messaging/inbound/vc-sender.d.ts +41 -0
- package/src/messaging/inbound/vc-sender.js +53 -0
- package/src/messaging/outbound/actions.d.ts +16 -0
- package/src/messaging/outbound/actions.js +329 -0
- package/src/messaging/outbound/bot-peer-context.d.ts +42 -0
- package/src/messaging/outbound/bot-peer-context.js +40 -0
- package/src/messaging/outbound/chat-manage.d.ts +64 -0
- package/src/messaging/outbound/chat-manage.js +117 -0
- package/src/messaging/outbound/deliver.d.ts +186 -0
- package/src/messaging/outbound/deliver.js +410 -0
- package/src/messaging/outbound/fetch.d.ts +12 -0
- package/src/messaging/outbound/fetch.js +17 -0
- package/src/messaging/outbound/forward.d.ts +26 -0
- package/src/messaging/outbound/forward.js +51 -0
- package/src/messaging/outbound/media-url-utils.d.ts +29 -0
- package/src/messaging/outbound/media-url-utils.js +172 -0
- package/src/messaging/outbound/media.d.ts +260 -0
- package/src/messaging/outbound/media.js +865 -0
- package/src/messaging/outbound/normalize-mentions.d.ts +50 -0
- package/src/messaging/outbound/normalize-mentions.js +166 -0
- package/src/messaging/outbound/outbound-mention.d.ts +41 -0
- package/src/messaging/outbound/outbound-mention.js +112 -0
- package/src/messaging/outbound/outbound.d.ts +79 -0
- package/src/messaging/outbound/outbound.js +195 -0
- package/src/messaging/outbound/reactions.d.ts +124 -0
- package/src/messaging/outbound/reactions.js +384 -0
- package/src/messaging/outbound/send.d.ts +153 -0
- package/src/messaging/outbound/send.js +417 -0
- package/src/messaging/outbound/typing.d.ts +60 -0
- package/src/messaging/outbound/typing.js +139 -0
- package/src/messaging/shared/message-lookup.d.ts +54 -0
- package/src/messaging/shared/message-lookup.js +120 -0
- package/src/messaging/types.d.ts +316 -0
- package/src/messaging/types.js +11 -0
- package/src/tools/ask-user-question.d.ts +32 -0
- package/src/tools/ask-user-question.js +922 -0
- package/src/tools/auto-auth.d.ts +57 -0
- package/src/tools/auto-auth.js +904 -0
- package/src/tools/helpers.d.ts +264 -0
- package/src/tools/helpers.js +389 -0
- package/src/tools/mcp/doc/create.d.ts +12 -0
- package/src/tools/mcp/doc/create.js +47 -0
- package/src/tools/mcp/doc/fetch.d.ts +12 -0
- package/src/tools/mcp/doc/fetch.js +39 -0
- package/src/tools/mcp/doc/index.d.ts +12 -0
- package/src/tools/mcp/doc/index.js +50 -0
- package/src/tools/mcp/doc/update.d.ts +12 -0
- package/src/tools/mcp/doc/update.js +64 -0
- package/src/tools/mcp/shared.d.ts +59 -0
- package/src/tools/mcp/shared.js +239 -0
- package/src/tools/oapi/bitable/app-table-field.d.ts +16 -0
- package/src/tools/oapi/bitable/app-table-field.js +225 -0
- package/src/tools/oapi/bitable/app-table-record.d.ts +20 -0
- package/src/tools/oapi/bitable/app-table-record.js +439 -0
- package/src/tools/oapi/bitable/app-table-view.d.ts +16 -0
- package/src/tools/oapi/bitable/app-table-view.js +172 -0
- package/src/tools/oapi/bitable/app-table.d.ts +17 -0
- package/src/tools/oapi/bitable/app-table.js +195 -0
- package/src/tools/oapi/bitable/app.d.ts +18 -0
- package/src/tools/oapi/bitable/app.js +189 -0
- package/src/tools/oapi/bitable/index.d.ts +9 -0
- package/src/tools/oapi/bitable/index.js +17 -0
- package/src/tools/oapi/calendar/calendar.d.ts +15 -0
- package/src/tools/oapi/calendar/calendar.js +125 -0
- package/src/tools/oapi/calendar/event-attendee.d.ts +14 -0
- package/src/tools/oapi/calendar/event-attendee.js +161 -0
- package/src/tools/oapi/calendar/event.d.ts +16 -0
- package/src/tools/oapi/calendar/event.js +712 -0
- package/src/tools/oapi/calendar/freebusy.d.ts +13 -0
- package/src/tools/oapi/calendar/freebusy.js +114 -0
- package/src/tools/oapi/calendar/index.d.ts +8 -0
- package/src/tools/oapi/calendar/index.js +15 -0
- package/src/tools/oapi/chat/chat.d.ts +16 -0
- package/src/tools/oapi/chat/chat.js +127 -0
- package/src/tools/oapi/chat/index.d.ts +10 -0
- package/src/tools/oapi/chat/index.js +23 -0
- package/src/tools/oapi/chat/members.d.ts +11 -0
- package/src/tools/oapi/chat/members.js +84 -0
- package/src/tools/oapi/common/get-user.d.ts +12 -0
- package/src/tools/oapi/common/get-user.js +109 -0
- package/src/tools/oapi/common/index.d.ts +6 -0
- package/src/tools/oapi/common/index.js +11 -0
- package/src/tools/oapi/common/search-user.d.ts +11 -0
- package/src/tools/oapi/common/search-user.js +76 -0
- package/src/tools/oapi/drive/doc-comments.d.ts +15 -0
- package/src/tools/oapi/drive/doc-comments.js +351 -0
- package/src/tools/oapi/drive/doc-media.d.ts +19 -0
- package/src/tools/oapi/drive/doc-media.js +371 -0
- package/src/tools/oapi/drive/file.d.ts +19 -0
- package/src/tools/oapi/drive/file.js +519 -0
- package/src/tools/oapi/drive/index.d.ts +12 -0
- package/src/tools/oapi/drive/index.js +45 -0
- package/src/tools/oapi/helpers.d.ts +183 -0
- package/src/tools/oapi/helpers.js +385 -0
- package/src/tools/oapi/im/format-messages.d.ts +50 -0
- package/src/tools/oapi/im/format-messages.js +169 -0
- package/src/tools/oapi/im/index.d.ts +10 -0
- package/src/tools/oapi/im/index.js +25 -0
- package/src/tools/oapi/im/message-read.d.ts +13 -0
- package/src/tools/oapi/im/message-read.js +419 -0
- package/src/tools/oapi/im/message.d.ts +16 -0
- package/src/tools/oapi/im/message.js +152 -0
- package/src/tools/oapi/im/resource.d.ts +13 -0
- package/src/tools/oapi/im/resource.js +186 -0
- package/src/tools/oapi/im/time-utils.d.ts +46 -0
- package/src/tools/oapi/im/time-utils.js +212 -0
- package/src/tools/oapi/im/user-name-uat.d.ts +26 -0
- package/src/tools/oapi/im/user-name-uat.js +143 -0
- package/src/tools/oapi/index.d.ts +11 -0
- package/src/tools/oapi/index.js +64 -0
- package/src/tools/oapi/sdk-types.d.ts +96 -0
- package/src/tools/oapi/sdk-types.js +13 -0
- package/src/tools/oapi/search/doc-search.d.ts +13 -0
- package/src/tools/oapi/search/doc-search.js +194 -0
- package/src/tools/oapi/search/index.d.ts +12 -0
- package/src/tools/oapi/search/index.js +37 -0
- package/src/tools/oapi/sheets/index.d.ts +12 -0
- package/src/tools/oapi/sheets/index.js +35 -0
- package/src/tools/oapi/sheets/sheet.d.ts +16 -0
- package/src/tools/oapi/sheets/sheet.js +688 -0
- package/src/tools/oapi/task/attachment.d.ts +18 -0
- package/src/tools/oapi/task/attachment.js +107 -0
- package/src/tools/oapi/task/comment.d.ts +15 -0
- package/src/tools/oapi/task/comment.js +149 -0
- package/src/tools/oapi/task/index.d.ts +11 -0
- package/src/tools/oapi/task/index.js +21 -0
- package/src/tools/oapi/task/section.d.ts +17 -0
- package/src/tools/oapi/task/section.js +293 -0
- package/src/tools/oapi/task/subtask.d.ts +14 -0
- package/src/tools/oapi/task/subtask.js +171 -0
- package/src/tools/oapi/task/task.d.ts +19 -0
- package/src/tools/oapi/task/task.js +512 -0
- package/src/tools/oapi/task/task_agent.d.ts +14 -0
- package/src/tools/oapi/task/task_agent.js +108 -0
- package/src/tools/oapi/task/tasklist.d.ts +19 -0
- package/src/tools/oapi/task/tasklist.js +276 -0
- package/src/tools/oapi/wiki/index.d.ts +12 -0
- package/src/tools/oapi/wiki/index.js +42 -0
- package/src/tools/oapi/wiki/space-node.d.ts +17 -0
- package/src/tools/oapi/wiki/space-node.js +233 -0
- package/src/tools/oapi/wiki/space.d.ts +15 -0
- package/src/tools/oapi/wiki/space.js +133 -0
- package/src/tools/oauth-batch-auth.d.ts +11 -0
- package/src/tools/oauth-batch-auth.js +150 -0
- package/src/tools/oauth-cards.d.ts +39 -0
- package/src/tools/oauth-cards.js +324 -0
- package/src/tools/oauth.d.ts +47 -0
- package/src/tools/oauth.js +592 -0
- package/src/tools/onboarding-auth.d.ts +27 -0
- package/src/tools/onboarding-auth.js +133 -0
- package/src/tools/tat/im/index.d.ts +15 -0
- package/src/tools/tat/im/index.js +22 -0
- package/src/tools/tat/im/resource.d.ts +15 -0
- package/src/tools/tat/im/resource.js +192 -0
- package/tsdown.config.d.ts +2 -0
- package/tsdown.config.js +25 -0
- package/vitest.config.d.ts +2 -0
- 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
|
+
}
|