@lark-project/openclaw-lark-project 2026.3.131
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 +80 -0
- package/README.zh.md +80 -0
- package/dist/index.js +172 -0
- package/dist/index.js.map +7 -0
- package/dist/skills/feishu-bitable/SKILL.md +248 -0
- package/dist/skills/feishu-bitable/references/examples.md +813 -0
- package/dist/skills/feishu-bitable/references/field-properties.md +763 -0
- package/dist/skills/feishu-bitable/references/record-values.md +911 -0
- package/dist/skills/feishu-calendar/SKILL.md +244 -0
- package/dist/skills/feishu-channel-rules/SKILL.md +18 -0
- package/dist/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
- package/dist/skills/feishu-create-doc/SKILL.md +719 -0
- package/dist/skills/feishu-fetch-doc/SKILL.md +93 -0
- package/dist/skills/feishu-im-read/SKILL.md +163 -0
- package/dist/skills/feishu-project/SKILL.md +122 -0
- package/dist/skills/feishu-task/SKILL.md +293 -0
- package/dist/skills/feishu-troubleshoot/SKILL.md +70 -0
- package/dist/skills/feishu-update-doc/SKILL.md +285 -0
- package/dist/src/card/builder.js +293 -0
- package/dist/src/card/builder.js.map +7 -0
- package/dist/src/card/cardkit.js +126 -0
- package/dist/src/card/cardkit.js.map +7 -0
- package/dist/src/card/flush-controller.js +107 -0
- package/dist/src/card/flush-controller.js.map +7 -0
- package/dist/src/card/markdown-style.js +57 -0
- package/dist/src/card/markdown-style.js.map +7 -0
- package/dist/src/card/reply-dispatcher-types.js +39 -0
- package/dist/src/card/reply-dispatcher-types.js.map +7 -0
- package/dist/src/card/reply-dispatcher.js +245 -0
- package/dist/src/card/reply-dispatcher.js.map +7 -0
- package/dist/src/card/reply-mode.js +29 -0
- package/dist/src/card/reply-mode.js.map +7 -0
- package/dist/src/card/streaming-card-controller.js +653 -0
- package/dist/src/card/streaming-card-controller.js.map +7 -0
- package/dist/src/card/unavailable-guard.js +76 -0
- package/dist/src/card/unavailable-guard.js.map +7 -0
- package/dist/src/channel/abort-detect.js +79 -0
- package/dist/src/channel/abort-detect.js.map +7 -0
- package/dist/src/channel/chat-queue.js +50 -0
- package/dist/src/channel/chat-queue.js.map +7 -0
- package/dist/src/channel/config-adapter.js +89 -0
- package/dist/src/channel/config-adapter.js.map +7 -0
- package/dist/src/channel/directory.js +133 -0
- package/dist/src/channel/directory.js.map +7 -0
- package/dist/src/channel/event-handlers.js +175 -0
- package/dist/src/channel/event-handlers.js.map +7 -0
- package/dist/src/channel/monitor.js +108 -0
- package/dist/src/channel/monitor.js.map +7 -0
- package/dist/src/channel/onboarding-config.js +76 -0
- package/dist/src/channel/onboarding-config.js.map +7 -0
- package/dist/src/channel/onboarding-migrate.js +55 -0
- package/dist/src/channel/onboarding-migrate.js.map +7 -0
- package/dist/src/channel/onboarding.js +285 -0
- package/dist/src/channel/onboarding.js.map +7 -0
- package/dist/src/channel/plugin.js +260 -0
- package/dist/src/channel/plugin.js.map +7 -0
- package/dist/src/channel/probe.js +14 -0
- package/dist/src/channel/probe.js.map +7 -0
- package/dist/src/channel/types.js +1 -0
- package/dist/src/channel/types.js.map +7 -0
- package/dist/src/commands/auth.js +73 -0
- package/dist/src/commands/auth.js.map +7 -0
- package/dist/src/commands/diagnose.js +658 -0
- package/dist/src/commands/diagnose.js.map +7 -0
- package/dist/src/commands/doctor.js +327 -0
- package/dist/src/commands/doctor.js.map +7 -0
- package/dist/src/commands/index.js +124 -0
- package/dist/src/commands/index.js.map +7 -0
- package/dist/src/core/accounts.js +129 -0
- package/dist/src/core/accounts.js.map +7 -0
- package/dist/src/core/agent-config.js +60 -0
- package/dist/src/core/agent-config.js.map +7 -0
- package/dist/src/core/api-error.js +55 -0
- package/dist/src/core/api-error.js.map +7 -0
- package/dist/src/core/app-owner-fallback.js +17 -0
- package/dist/src/core/app-owner-fallback.js.map +7 -0
- package/dist/src/core/app-scope-checker.js +95 -0
- package/dist/src/core/app-scope-checker.js.map +7 -0
- package/dist/src/core/auth-errors.js +120 -0
- package/dist/src/core/auth-errors.js.map +7 -0
- package/dist/src/core/chat-info-cache.js +102 -0
- package/dist/src/core/chat-info-cache.js.map +7 -0
- package/dist/src/core/config-schema.js +150 -0
- package/dist/src/core/config-schema.js.map +7 -0
- package/dist/src/core/device-flow.js +174 -0
- package/dist/src/core/device-flow.js.map +7 -0
- package/dist/src/core/feishu-fetch.js +12 -0
- package/dist/src/core/feishu-fetch.js.map +7 -0
- package/dist/src/core/footer-config.js +16 -0
- package/dist/src/core/footer-config.js.map +7 -0
- package/dist/src/core/lark-client.js +322 -0
- package/dist/src/core/lark-client.js.map +7 -0
- package/dist/src/core/lark-logger.js +92 -0
- package/dist/src/core/lark-logger.js.map +7 -0
- package/dist/src/core/lark-ticket.js +18 -0
- package/dist/src/core/lark-ticket.js.map +7 -0
- package/dist/src/core/message-unavailable.js +119 -0
- package/dist/src/core/message-unavailable.js.map +7 -0
- package/dist/src/core/owner-policy.js +25 -0
- package/dist/src/core/owner-policy.js.map +7 -0
- package/dist/src/core/permission-url.js +37 -0
- package/dist/src/core/permission-url.js.map +7 -0
- package/dist/src/core/project-auth.js +177 -0
- package/dist/src/core/project-auth.js.map +7 -0
- package/dist/src/core/project-oauth-flow.js +124 -0
- package/dist/src/core/project-oauth-flow.js.map +7 -0
- package/dist/src/core/project-token-store.js +172 -0
- package/dist/src/core/project-token-store.js.map +7 -0
- package/dist/src/core/raw-request.js +45 -0
- package/dist/src/core/raw-request.js.map +7 -0
- package/dist/src/core/scope-manager.js +62 -0
- package/dist/src/core/scope-manager.js.map +7 -0
- package/dist/src/core/security-check.js +118 -0
- package/dist/src/core/security-check.js.map +7 -0
- package/dist/src/core/shutdown-hooks.js +37 -0
- package/dist/src/core/shutdown-hooks.js.map +7 -0
- package/dist/src/core/targets.js +55 -0
- package/dist/src/core/targets.js.map +7 -0
- package/dist/src/core/token-store.js +215 -0
- package/dist/src/core/token-store.js.map +7 -0
- package/dist/src/core/tool-client.js +335 -0
- package/dist/src/core/tool-client.js.map +7 -0
- package/dist/src/core/tool-scopes.js +207 -0
- package/dist/src/core/tool-scopes.js.map +7 -0
- package/dist/src/core/tools-config.js +57 -0
- package/dist/src/core/tools-config.js.map +7 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/types.js.map +7 -0
- package/dist/src/core/uat-client.js +124 -0
- package/dist/src/core/uat-client.js.map +7 -0
- package/dist/src/core/version.js +27 -0
- package/dist/src/core/version.js.map +7 -0
- package/dist/src/messaging/converters/audio.js +19 -0
- package/dist/src/messaging/converters/audio.js.map +7 -0
- package/dist/src/messaging/converters/calendar.js +46 -0
- package/dist/src/messaging/converters/calendar.js.map +7 -0
- package/dist/src/messaging/converters/content-converter.js +61 -0
- package/dist/src/messaging/converters/content-converter.js.map +7 -0
- package/dist/src/messaging/converters/file.js +18 -0
- package/dist/src/messaging/converters/file.js.map +7 -0
- package/dist/src/messaging/converters/folder.js +18 -0
- package/dist/src/messaging/converters/folder.js.map +7 -0
- package/dist/src/messaging/converters/hongbao.js +14 -0
- package/dist/src/messaging/converters/hongbao.js.map +7 -0
- package/dist/src/messaging/converters/image.js +16 -0
- package/dist/src/messaging/converters/image.js.map +7 -0
- package/dist/src/messaging/converters/index.js +48 -0
- package/dist/src/messaging/converters/index.js.map +7 -0
- package/dist/src/messaging/converters/interactive/card-converter.js +1040 -0
- package/dist/src/messaging/converters/interactive/card-converter.js.map +7 -0
- package/dist/src/messaging/converters/interactive/card-utils.js +36 -0
- package/dist/src/messaging/converters/interactive/card-utils.js.map +7 -0
- package/dist/src/messaging/converters/interactive/index.js +19 -0
- package/dist/src/messaging/converters/interactive/index.js.map +7 -0
- package/dist/src/messaging/converters/interactive/legacy.js +53 -0
- package/dist/src/messaging/converters/interactive/legacy.js.map +7 -0
- package/dist/src/messaging/converters/interactive/types.js +23 -0
- package/dist/src/messaging/converters/interactive/types.js.map +7 -0
- package/dist/src/messaging/converters/location.js +17 -0
- package/dist/src/messaging/converters/location.js.map +7 -0
- package/dist/src/messaging/converters/merge-forward.js +143 -0
- package/dist/src/messaging/converters/merge-forward.js.map +7 -0
- package/dist/src/messaging/converters/post.js +113 -0
- package/dist/src/messaging/converters/post.js.map +7 -0
- package/dist/src/messaging/converters/share.js +22 -0
- package/dist/src/messaging/converters/share.js.map +7 -0
- package/dist/src/messaging/converters/sticker.js +16 -0
- package/dist/src/messaging/converters/sticker.js.map +7 -0
- package/dist/src/messaging/converters/system.js +25 -0
- package/dist/src/messaging/converters/system.js.map +7 -0
- package/dist/src/messaging/converters/text.js +12 -0
- package/dist/src/messaging/converters/text.js.map +7 -0
- package/dist/src/messaging/converters/todo.js +37 -0
- package/dist/src/messaging/converters/todo.js.map +7 -0
- package/dist/src/messaging/converters/types.js +1 -0
- package/dist/src/messaging/converters/types.js.map +7 -0
- package/dist/src/messaging/converters/unknown.js +13 -0
- package/dist/src/messaging/converters/unknown.js.map +7 -0
- package/dist/src/messaging/converters/utils.js +35 -0
- package/dist/src/messaging/converters/utils.js.map +7 -0
- package/dist/src/messaging/converters/video-chat.js +21 -0
- package/dist/src/messaging/converters/video-chat.js.map +7 -0
- package/dist/src/messaging/converters/video.js +30 -0
- package/dist/src/messaging/converters/video.js.map +7 -0
- package/dist/src/messaging/converters/vote.js +24 -0
- package/dist/src/messaging/converters/vote.js.map +7 -0
- package/dist/src/messaging/inbound/dedup.js +82 -0
- package/dist/src/messaging/inbound/dedup.js.map +7 -0
- package/dist/src/messaging/inbound/dispatch-builders.js +98 -0
- package/dist/src/messaging/inbound/dispatch-builders.js.map +7 -0
- package/dist/src/messaging/inbound/dispatch-commands.js +94 -0
- package/dist/src/messaging/inbound/dispatch-commands.js.map +7 -0
- package/dist/src/messaging/inbound/dispatch-context.js +96 -0
- package/dist/src/messaging/inbound/dispatch-context.js.map +7 -0
- package/dist/src/messaging/inbound/dispatch.js +150 -0
- package/dist/src/messaging/inbound/dispatch.js.map +7 -0
- package/dist/src/messaging/inbound/enrich.js +137 -0
- package/dist/src/messaging/inbound/enrich.js.map +7 -0
- package/dist/src/messaging/inbound/gate-effects.js +28 -0
- package/dist/src/messaging/inbound/gate-effects.js.map +7 -0
- package/dist/src/messaging/inbound/gate.js +163 -0
- package/dist/src/messaging/inbound/gate.js.map +7 -0
- package/dist/src/messaging/inbound/handler.js +132 -0
- package/dist/src/messaging/inbound/handler.js.map +7 -0
- package/dist/src/messaging/inbound/media-resolver.js +70 -0
- package/dist/src/messaging/inbound/media-resolver.js.map +7 -0
- package/dist/src/messaging/inbound/mention.js +50 -0
- package/dist/src/messaging/inbound/mention.js.map +7 -0
- package/dist/src/messaging/inbound/parse-io.js +41 -0
- package/dist/src/messaging/inbound/parse-io.js.map +7 -0
- package/dist/src/messaging/inbound/parse.js +79 -0
- package/dist/src/messaging/inbound/parse.js.map +7 -0
- package/dist/src/messaging/inbound/permission.js +30 -0
- package/dist/src/messaging/inbound/permission.js.map +7 -0
- package/dist/src/messaging/inbound/policy.js +83 -0
- package/dist/src/messaging/inbound/policy.js.map +7 -0
- package/dist/src/messaging/inbound/reaction-handler.js +162 -0
- package/dist/src/messaging/inbound/reaction-handler.js.map +7 -0
- package/dist/src/messaging/inbound/user-name-cache.js +172 -0
- package/dist/src/messaging/inbound/user-name-cache.js.map +7 -0
- package/dist/src/messaging/outbound/actions.js +239 -0
- package/dist/src/messaging/outbound/actions.js.map +7 -0
- package/dist/src/messaging/outbound/chat-manage.js +74 -0
- package/dist/src/messaging/outbound/chat-manage.js.map +7 -0
- package/dist/src/messaging/outbound/deliver.js +162 -0
- package/dist/src/messaging/outbound/deliver.js.map +7 -0
- package/dist/src/messaging/outbound/fetch.js +7 -0
- package/dist/src/messaging/outbound/fetch.js.map +7 -0
- package/dist/src/messaging/outbound/forward.js +31 -0
- package/dist/src/messaging/outbound/forward.js.map +7 -0
- package/dist/src/messaging/outbound/media-url-utils.js +101 -0
- package/dist/src/messaging/outbound/media-url-utils.js.map +7 -0
- package/dist/src/messaging/outbound/media.js +463 -0
- package/dist/src/messaging/outbound/media.js.map +7 -0
- package/dist/src/messaging/outbound/outbound.js +95 -0
- package/dist/src/messaging/outbound/outbound.js.map +7 -0
- package/dist/src/messaging/outbound/reactions.js +312 -0
- package/dist/src/messaging/outbound/reactions.js.map +7 -0
- package/dist/src/messaging/outbound/send.js +194 -0
- package/dist/src/messaging/outbound/send.js.map +7 -0
- package/dist/src/messaging/outbound/typing.js +77 -0
- package/dist/src/messaging/outbound/typing.js.map +7 -0
- package/dist/src/messaging/shared/message-lookup.js +84 -0
- package/dist/src/messaging/shared/message-lookup.js.map +7 -0
- package/dist/src/messaging/types.js +1 -0
- package/dist/src/messaging/types.js.map +7 -0
- package/dist/src/tools/auto-auth.js +714 -0
- package/dist/src/tools/auto-auth.js.map +7 -0
- package/dist/src/tools/helpers.js +133 -0
- package/dist/src/tools/helpers.js.map +7 -0
- package/dist/src/tools/mcp/doc/create.js +35 -0
- package/dist/src/tools/mcp/doc/create.js.map +7 -0
- package/dist/src/tools/mcp/doc/fetch.js +33 -0
- package/dist/src/tools/mcp/doc/fetch.js.map +7 -0
- package/dist/src/tools/mcp/doc/index.js +32 -0
- package/dist/src/tools/mcp/doc/index.js.map +7 -0
- package/dist/src/tools/mcp/doc/update.js +61 -0
- package/dist/src/tools/mcp/doc/update.js.map +7 -0
- package/dist/src/tools/mcp/project/endpoint.js +25 -0
- package/dist/src/tools/mcp/project/endpoint.js.map +7 -0
- package/dist/src/tools/mcp/project/index.js +27 -0
- package/dist/src/tools/mcp/project/index.js.map +7 -0
- package/dist/src/tools/mcp/project/tools.js +579 -0
- package/dist/src/tools/mcp/project/tools.js.map +7 -0
- package/dist/src/tools/mcp/shared.js +170 -0
- package/dist/src/tools/mcp/shared.js.map +7 -0
- package/dist/src/tools/oapi/bitable/app-table-field.js +244 -0
- package/dist/src/tools/oapi/bitable/app-table-field.js.map +7 -0
- package/dist/src/tools/oapi/bitable/app-table-record.js +501 -0
- package/dist/src/tools/oapi/bitable/app-table-record.js.map +7 -0
- package/dist/src/tools/oapi/bitable/app-table-view.js +226 -0
- package/dist/src/tools/oapi/bitable/app-table-view.js.map +7 -0
- package/dist/src/tools/oapi/bitable/app-table.js +278 -0
- package/dist/src/tools/oapi/bitable/app-table.js.map +7 -0
- package/dist/src/tools/oapi/bitable/app.js +200 -0
- package/dist/src/tools/oapi/bitable/app.js.map +7 -0
- package/dist/src/tools/oapi/bitable/index.js +13 -0
- package/dist/src/tools/oapi/bitable/index.js.map +7 -0
- package/dist/src/tools/oapi/calendar/calendar.js +131 -0
- package/dist/src/tools/oapi/calendar/calendar.js.map +7 -0
- package/dist/src/tools/oapi/calendar/event-attendee.js +301 -0
- package/dist/src/tools/oapi/calendar/event-attendee.js.map +7 -0
- package/dist/src/tools/oapi/calendar/event.js +834 -0
- package/dist/src/tools/oapi/calendar/event.js.map +7 -0
- package/dist/src/tools/oapi/calendar/freebusy.js +111 -0
- package/dist/src/tools/oapi/calendar/freebusy.js.map +7 -0
- package/dist/src/tools/oapi/calendar/index.js +11 -0
- package/dist/src/tools/oapi/calendar/index.js.map +7 -0
- package/dist/src/tools/oapi/chat/chat.js +132 -0
- package/dist/src/tools/oapi/chat/chat.js.map +7 -0
- package/dist/src/tools/oapi/chat/index.js +11 -0
- package/dist/src/tools/oapi/chat/index.js.map +7 -0
- package/dist/src/tools/oapi/chat/members.js +83 -0
- package/dist/src/tools/oapi/chat/members.js.map +7 -0
- package/dist/src/tools/oapi/common/get-user.js +95 -0
- package/dist/src/tools/oapi/common/get-user.js.map +7 -0
- package/dist/src/tools/oapi/common/index.js +7 -0
- package/dist/src/tools/oapi/common/index.js.map +7 -0
- package/dist/src/tools/oapi/common/search-user.js +67 -0
- package/dist/src/tools/oapi/common/search-user.js.map +7 -0
- package/dist/src/tools/oapi/drive/doc-comments.js +310 -0
- package/dist/src/tools/oapi/drive/doc-comments.js.map +7 -0
- package/dist/src/tools/oapi/drive/doc-media.js +314 -0
- package/dist/src/tools/oapi/drive/doc-media.js.map +7 -0
- package/dist/src/tools/oapi/drive/file.js +548 -0
- package/dist/src/tools/oapi/drive/file.js.map +7 -0
- package/dist/src/tools/oapi/drive/index.js +29 -0
- package/dist/src/tools/oapi/drive/index.js.map +7 -0
- package/dist/src/tools/oapi/helpers.js +199 -0
- package/dist/src/tools/oapi/helpers.js.map +7 -0
- package/dist/src/tools/oapi/im/format-messages.js +128 -0
- package/dist/src/tools/oapi/im/format-messages.js.map +7 -0
- package/dist/src/tools/oapi/im/index.js +15 -0
- package/dist/src/tools/oapi/im/index.js.map +7 -0
- package/dist/src/tools/oapi/im/message-read.js +404 -0
- package/dist/src/tools/oapi/im/message-read.js.map +7 -0
- package/dist/src/tools/oapi/im/message.js +179 -0
- package/dist/src/tools/oapi/im/message.js.map +7 -0
- package/dist/src/tools/oapi/im/resource.js +126 -0
- package/dist/src/tools/oapi/im/resource.js.map +7 -0
- package/dist/src/tools/oapi/im/time-utils.js +169 -0
- package/dist/src/tools/oapi/im/time-utils.js.map +7 -0
- package/dist/src/tools/oapi/im/user-name-uat.js +103 -0
- package/dist/src/tools/oapi/im/user-name-uat.js.map +7 -0
- package/dist/src/tools/oapi/index.js +56 -0
- package/dist/src/tools/oapi/index.js.map +7 -0
- package/dist/src/tools/oapi/sdk-types.js +1 -0
- package/dist/src/tools/oapi/sdk-types.js.map +7 -0
- package/dist/src/tools/oapi/search/doc-search.js +215 -0
- package/dist/src/tools/oapi/search/doc-search.js.map +7 -0
- package/dist/src/tools/oapi/search/index.js +25 -0
- package/dist/src/tools/oapi/search/index.js.map +7 -0
- package/dist/src/tools/oapi/sheets/index.js +25 -0
- package/dist/src/tools/oapi/sheets/index.js.map +7 -0
- package/dist/src/tools/oapi/sheets/sheet.js +652 -0
- package/dist/src/tools/oapi/sheets/sheet.js.map +7 -0
- package/dist/src/tools/oapi/task/comment.js +151 -0
- package/dist/src/tools/oapi/task/comment.js.map +7 -0
- package/dist/src/tools/oapi/task/index.js +11 -0
- package/dist/src/tools/oapi/task/index.js.map +7 -0
- package/dist/src/tools/oapi/task/subtask.js +175 -0
- package/dist/src/tools/oapi/task/subtask.js.map +7 -0
- package/dist/src/tools/oapi/task/task.js +405 -0
- package/dist/src/tools/oapi/task/task.js.map +7 -0
- package/dist/src/tools/oapi/task/tasklist.js +366 -0
- package/dist/src/tools/oapi/task/tasklist.js.map +7 -0
- package/dist/src/tools/oapi/wiki/index.js +27 -0
- package/dist/src/tools/oapi/wiki/index.js.map +7 -0
- package/dist/src/tools/oapi/wiki/space-node.js +311 -0
- package/dist/src/tools/oapi/wiki/space-node.js.map +7 -0
- package/dist/src/tools/oapi/wiki/space.js +148 -0
- package/dist/src/tools/oapi/wiki/space.js.map +7 -0
- package/dist/src/tools/oauth-batch-auth.js +125 -0
- package/dist/src/tools/oauth-batch-auth.js.map +7 -0
- package/dist/src/tools/oauth-cards.js +269 -0
- package/dist/src/tools/oauth-cards.js.map +7 -0
- package/dist/src/tools/oauth.js +538 -0
- package/dist/src/tools/oauth.js.map +7 -0
- package/dist/src/tools/onboarding-auth.js +101 -0
- package/dist/src/tools/onboarding-auth.js.map +7 -0
- package/dist/src/tools/project-oauth.js +305 -0
- package/dist/src/tools/project-oauth.js.map +7 -0
- package/dist/src/tools/tat/im/index.js +9 -0
- package/dist/src/tools/tat/im/index.js.map +7 -0
- package/dist/src/tools/tat/im/resource.js +123 -0
- package/dist/src/tools/tat/im/resource.js.map +7 -0
- package/package.json +64 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import * as dns from "node:dns/promises";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { LarkClient } from "../../core/lark-client";
|
|
8
|
+
import { normalizeFeishuTarget, resolveReceiveIdType } from "../../core/targets";
|
|
9
|
+
import {
|
|
10
|
+
isLocalMediaPath,
|
|
11
|
+
normalizeMediaUrlInput,
|
|
12
|
+
resolveFileNameFromMediaUrl,
|
|
13
|
+
safeFileUrlToPath,
|
|
14
|
+
validateLocalMediaRoots
|
|
15
|
+
} from "./media-url-utils";
|
|
16
|
+
import { larkLogger } from "../../core/lark-logger";
|
|
17
|
+
const log = larkLogger("outbound/media");
|
|
18
|
+
async function extractBufferFromResponse(response) {
|
|
19
|
+
if (Buffer.isBuffer(response)) {
|
|
20
|
+
return { buffer: response };
|
|
21
|
+
}
|
|
22
|
+
if (response instanceof ArrayBuffer) {
|
|
23
|
+
return { buffer: Buffer.from(response) };
|
|
24
|
+
}
|
|
25
|
+
if (response == null) {
|
|
26
|
+
throw new Error("[feishu-media] Received null/undefined response");
|
|
27
|
+
}
|
|
28
|
+
const resp = response;
|
|
29
|
+
const contentType = resp.headers?.["content-type"] ?? resp.contentType ?? void 0;
|
|
30
|
+
if (resp.data != null) {
|
|
31
|
+
if (Buffer.isBuffer(resp.data)) {
|
|
32
|
+
return { buffer: resp.data, contentType };
|
|
33
|
+
}
|
|
34
|
+
if (resp.data instanceof ArrayBuffer) {
|
|
35
|
+
return { buffer: Buffer.from(resp.data), contentType };
|
|
36
|
+
}
|
|
37
|
+
if (typeof resp.data.pipe === "function") {
|
|
38
|
+
const buf = await streamToBuffer(resp.data);
|
|
39
|
+
return { buffer: buf, contentType };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (typeof resp.getReadableStream === "function") {
|
|
43
|
+
const stream = await resp.getReadableStream();
|
|
44
|
+
const buf = await streamToBuffer(stream);
|
|
45
|
+
return { buffer: buf, contentType };
|
|
46
|
+
}
|
|
47
|
+
if (typeof resp.writeFile === "function") {
|
|
48
|
+
const tmpDir = os.tmpdir();
|
|
49
|
+
const tmpFile = path.join(tmpDir, `feishu-media-${Date.now()}`);
|
|
50
|
+
try {
|
|
51
|
+
await resp.writeFile(tmpFile);
|
|
52
|
+
const buf = fs.readFileSync(tmpFile);
|
|
53
|
+
return { buffer: buf, contentType };
|
|
54
|
+
} finally {
|
|
55
|
+
try {
|
|
56
|
+
fs.unlinkSync(tmpFile);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof resp[Symbol.asyncIterator] === "function" || typeof resp.next === "function") {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
const iterable = typeof resp[Symbol.asyncIterator] === "function" ? resp : asyncIteratorToIterable(resp);
|
|
64
|
+
for await (const chunk of iterable) {
|
|
65
|
+
chunks.push(Buffer.from(chunk));
|
|
66
|
+
}
|
|
67
|
+
return { buffer: Buffer.concat(chunks), contentType };
|
|
68
|
+
}
|
|
69
|
+
if (typeof resp.pipe === "function") {
|
|
70
|
+
const buf = await streamToBuffer(resp);
|
|
71
|
+
return { buffer: buf, contentType };
|
|
72
|
+
}
|
|
73
|
+
throw new Error("[feishu-media] Unable to extract binary data from response: unrecognised format");
|
|
74
|
+
}
|
|
75
|
+
function streamToBuffer(stream) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
stream.on("data", (chunk) => {
|
|
79
|
+
chunks.push(Buffer.from(chunk));
|
|
80
|
+
});
|
|
81
|
+
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
82
|
+
stream.on("error", reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function* asyncIteratorToIterable(iterator) {
|
|
86
|
+
while (true) {
|
|
87
|
+
const { value, done } = await iterator.next();
|
|
88
|
+
if (done) break;
|
|
89
|
+
yield value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function downloadMessageResourceFeishu(params) {
|
|
93
|
+
const { cfg, messageId, fileKey, type, accountId } = params;
|
|
94
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
95
|
+
const response = await client.im.messageResource.get({
|
|
96
|
+
path: {
|
|
97
|
+
message_id: messageId,
|
|
98
|
+
file_key: fileKey
|
|
99
|
+
},
|
|
100
|
+
params: {
|
|
101
|
+
type
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const { buffer, contentType } = await extractBufferFromResponse(response);
|
|
105
|
+
let fileName;
|
|
106
|
+
if (response && typeof response === "object") {
|
|
107
|
+
const resp = response;
|
|
108
|
+
const disposition = resp.headers?.["content-disposition"] ?? resp.headers?.["Content-Disposition"];
|
|
109
|
+
if (typeof disposition === "string") {
|
|
110
|
+
const match = disposition.match(/filename[*]?=(?:UTF-8'')?["']?([^"';\n]+)/i);
|
|
111
|
+
if (match) {
|
|
112
|
+
fileName = decodeURIComponent(match[1].trim());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { buffer, contentType, fileName };
|
|
117
|
+
}
|
|
118
|
+
async function uploadImageLark(params) {
|
|
119
|
+
const { cfg, image, imageType = "message", accountId } = params;
|
|
120
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
121
|
+
const imageStream = Buffer.isBuffer(image) ? Readable.from(image) : fs.createReadStream(image);
|
|
122
|
+
const response = await client.im.image.create({
|
|
123
|
+
data: { image_type: imageType, image: imageStream }
|
|
124
|
+
});
|
|
125
|
+
const imageKey = response?.data?.image_key ?? response?.image_key;
|
|
126
|
+
if (!imageKey) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`[feishu-media] Image upload failed: no image_key in response. Check that the image is a valid format (JPEG/PNG/GIF/BMP/WEBP). Response: ${JSON.stringify(response).slice(0, 200)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return { imageKey };
|
|
132
|
+
}
|
|
133
|
+
async function uploadFileLark(params) {
|
|
134
|
+
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
|
135
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
136
|
+
const fileStream = Buffer.isBuffer(file) ? Readable.from(file) : fs.createReadStream(file);
|
|
137
|
+
const response = await client.im.file.create({
|
|
138
|
+
data: {
|
|
139
|
+
file_type: fileType,
|
|
140
|
+
file_name: fileName,
|
|
141
|
+
file: fileStream,
|
|
142
|
+
...duration !== void 0 ? { duration: String(duration) } : {}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const fileKey = response?.data?.file_key ?? response?.file_key;
|
|
146
|
+
if (!fileKey) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[feishu-media] File upload failed: no file_key in response for "${fileName}" (type=${fileType}). Response: ${JSON.stringify(response).slice(0, 200)}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return { fileKey };
|
|
152
|
+
}
|
|
153
|
+
async function sendMediaMessage(params) {
|
|
154
|
+
const { client, to, content, msgType, replyToMessageId, replyInThread } = params;
|
|
155
|
+
if (replyToMessageId) {
|
|
156
|
+
const response2 = await client.im.message.reply({
|
|
157
|
+
path: { message_id: replyToMessageId },
|
|
158
|
+
data: { content, msg_type: msgType, reply_in_thread: replyInThread }
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
messageId: response2?.data?.message_id ?? "",
|
|
162
|
+
chatId: response2?.data?.chat_id ?? ""
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const target = normalizeFeishuTarget(to);
|
|
166
|
+
if (!target) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`[feishu-media] Cannot send ${msgType} message: "${to}" is not a valid target. Expected a chat_id (oc_*), open_id (ou_*), or user_id.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const receiveIdType = resolveReceiveIdType(target);
|
|
172
|
+
const response = await client.im.message.create({
|
|
173
|
+
params: { receive_id_type: receiveIdType },
|
|
174
|
+
data: { receive_id: target, msg_type: msgType, content }
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
messageId: response?.data?.message_id ?? "",
|
|
178
|
+
chatId: response?.data?.chat_id ?? ""
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function sendImageLark(params) {
|
|
182
|
+
const { cfg, to, imageKey, replyToMessageId, replyInThread, accountId } = params;
|
|
183
|
+
log.info(`sendImageLark: target=${to}, imageKey=${imageKey}`);
|
|
184
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
185
|
+
const content = JSON.stringify({ image_key: imageKey });
|
|
186
|
+
return sendMediaMessage({ client, to, content, msgType: "image", replyToMessageId, replyInThread });
|
|
187
|
+
}
|
|
188
|
+
async function sendFileLark(params) {
|
|
189
|
+
const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;
|
|
190
|
+
log.info(`sendFileLark: target=${to}, fileKey=${fileKey}`);
|
|
191
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
192
|
+
const content = JSON.stringify({ file_key: fileKey });
|
|
193
|
+
return sendMediaMessage({ client, to, content, msgType: "file", replyToMessageId, replyInThread });
|
|
194
|
+
}
|
|
195
|
+
async function sendVideoLark(params) {
|
|
196
|
+
const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;
|
|
197
|
+
log.info(`sendVideoLark: target=${to}, fileKey=${fileKey}`);
|
|
198
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
199
|
+
const content = JSON.stringify({ file_key: fileKey });
|
|
200
|
+
return sendMediaMessage({ client, to, content, msgType: "media", replyToMessageId, replyInThread });
|
|
201
|
+
}
|
|
202
|
+
async function sendAudioLark(params) {
|
|
203
|
+
const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;
|
|
204
|
+
log.info(`sendAudioLark: target=${to}, fileKey=${fileKey}`);
|
|
205
|
+
const client = LarkClient.fromCfg(cfg, accountId).sdk;
|
|
206
|
+
const content = JSON.stringify({ file_key: fileKey });
|
|
207
|
+
return sendMediaMessage({ client, to, content, msgType: "audio", replyToMessageId, replyInThread });
|
|
208
|
+
}
|
|
209
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif", ".heic"]);
|
|
210
|
+
const EXTENSION_TYPE_MAP = {
|
|
211
|
+
".opus": "opus",
|
|
212
|
+
".ogg": "opus",
|
|
213
|
+
".mp4": "mp4",
|
|
214
|
+
".mov": "mp4",
|
|
215
|
+
".avi": "mp4",
|
|
216
|
+
".mkv": "mp4",
|
|
217
|
+
".webm": "mp4",
|
|
218
|
+
".pdf": "pdf",
|
|
219
|
+
".doc": "doc",
|
|
220
|
+
".docx": "doc",
|
|
221
|
+
".xls": "xls",
|
|
222
|
+
".xlsx": "xls",
|
|
223
|
+
".csv": "xls",
|
|
224
|
+
".ppt": "ppt",
|
|
225
|
+
".pptx": "ppt"
|
|
226
|
+
};
|
|
227
|
+
function detectFileType(fileName) {
|
|
228
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
229
|
+
return EXTENSION_TYPE_MAP[ext] ?? "stream";
|
|
230
|
+
}
|
|
231
|
+
function parseOggOpusDuration(buffer) {
|
|
232
|
+
const OGGS = Buffer.from("OggS");
|
|
233
|
+
let offset = -1;
|
|
234
|
+
for (let i = buffer.length - OGGS.length; i >= 0; i--) {
|
|
235
|
+
if (buffer[i] === 79 && buffer.compare(OGGS, 0, 4, i, i + 4) === 0) {
|
|
236
|
+
offset = i;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (offset < 0) return void 0;
|
|
241
|
+
const granuleOffset = offset + 6;
|
|
242
|
+
if (granuleOffset + 8 > buffer.length) return void 0;
|
|
243
|
+
const lo = buffer.readUInt32LE(granuleOffset);
|
|
244
|
+
const hi = buffer.readUInt32LE(granuleOffset + 4);
|
|
245
|
+
const granule = hi * 4294967296 + lo;
|
|
246
|
+
if (granule <= 0) return void 0;
|
|
247
|
+
return Math.ceil(granule / 48e3) * 1e3;
|
|
248
|
+
}
|
|
249
|
+
function parseMp4Duration(buffer) {
|
|
250
|
+
const moovData = findBox(buffer, 0, buffer.length, "moov");
|
|
251
|
+
if (!moovData) return void 0;
|
|
252
|
+
const mvhdData = findBox(buffer, moovData.dataStart, moovData.dataEnd, "mvhd");
|
|
253
|
+
if (!mvhdData) return void 0;
|
|
254
|
+
const off = mvhdData.dataStart;
|
|
255
|
+
if (off + 1 > buffer.length) return void 0;
|
|
256
|
+
const version = buffer.readUInt8(off);
|
|
257
|
+
let timescale;
|
|
258
|
+
let duration;
|
|
259
|
+
if (version === 0) {
|
|
260
|
+
if (off + 20 > buffer.length) return void 0;
|
|
261
|
+
timescale = buffer.readUInt32BE(off + 12);
|
|
262
|
+
duration = buffer.readUInt32BE(off + 16);
|
|
263
|
+
} else {
|
|
264
|
+
if (off + 32 > buffer.length) return void 0;
|
|
265
|
+
timescale = buffer.readUInt32BE(off + 20);
|
|
266
|
+
const hi = buffer.readUInt32BE(off + 24);
|
|
267
|
+
const lo = buffer.readUInt32BE(off + 28);
|
|
268
|
+
duration = hi * 4294967296 + lo;
|
|
269
|
+
}
|
|
270
|
+
if (timescale <= 0 || duration <= 0) return void 0;
|
|
271
|
+
return Math.round(duration / timescale * 1e3);
|
|
272
|
+
}
|
|
273
|
+
function findBox(buffer, start, end, type) {
|
|
274
|
+
let offset = start;
|
|
275
|
+
while (offset + 8 <= end) {
|
|
276
|
+
const size = buffer.readUInt32BE(offset);
|
|
277
|
+
const boxType = buffer.toString("ascii", offset + 4, offset + 8);
|
|
278
|
+
let boxEnd;
|
|
279
|
+
let dataStart;
|
|
280
|
+
if (size === 0) {
|
|
281
|
+
boxEnd = end;
|
|
282
|
+
dataStart = offset + 8;
|
|
283
|
+
} else if (size === 1) {
|
|
284
|
+
if (offset + 16 > end) break;
|
|
285
|
+
const hi = buffer.readUInt32BE(offset + 8);
|
|
286
|
+
const lo = buffer.readUInt32BE(offset + 12);
|
|
287
|
+
boxEnd = offset + hi * 4294967296 + lo;
|
|
288
|
+
dataStart = offset + 16;
|
|
289
|
+
} else {
|
|
290
|
+
if (size < 8) break;
|
|
291
|
+
boxEnd = offset + size;
|
|
292
|
+
dataStart = offset + 8;
|
|
293
|
+
}
|
|
294
|
+
if (boxType === type) {
|
|
295
|
+
return { dataStart, dataEnd: Math.min(boxEnd, end) };
|
|
296
|
+
}
|
|
297
|
+
offset = boxEnd;
|
|
298
|
+
}
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
function isImageFileName(fileName) {
|
|
302
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
303
|
+
return IMAGE_EXTENSIONS.has(ext);
|
|
304
|
+
}
|
|
305
|
+
async function uploadAndSendMediaLark(params) {
|
|
306
|
+
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, replyInThread, accountId, mediaLocalRoots } = params;
|
|
307
|
+
log.info(
|
|
308
|
+
`uploadAndSendMediaLark: target=${to}, source=${mediaBuffer ? "buffer" : mediaUrl ?? "(none)"}, fileName=${fileName ?? "(auto)"}`
|
|
309
|
+
);
|
|
310
|
+
let buffer;
|
|
311
|
+
let resolvedFileName = fileName ?? "file";
|
|
312
|
+
if (mediaBuffer) {
|
|
313
|
+
buffer = mediaBuffer;
|
|
314
|
+
log.debug(`using provided buffer: ${buffer.length} bytes`);
|
|
315
|
+
} else if (mediaUrl) {
|
|
316
|
+
buffer = await fetchMediaBuffer(mediaUrl, mediaLocalRoots);
|
|
317
|
+
log.debug(`fetched media: ${buffer.length} bytes from "${mediaUrl}"`);
|
|
318
|
+
if (!fileName) {
|
|
319
|
+
const derivedFileName = resolveFileNameFromMediaUrl(mediaUrl);
|
|
320
|
+
if (derivedFileName) {
|
|
321
|
+
resolvedFileName = derivedFileName;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
throw new Error(
|
|
326
|
+
"[feishu-media] uploadAndSendMediaLark requires either mediaUrl or mediaBuffer. Provide a URL (http/https/file://) or a raw Buffer to send media."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const isImage = isImageFileName(resolvedFileName);
|
|
330
|
+
log.info(`resolved: fileName="${resolvedFileName}", type=${isImage ? "image" : "file"}, size=${buffer.length}`);
|
|
331
|
+
if (isImage) {
|
|
332
|
+
const { imageKey } = await uploadImageLark({
|
|
333
|
+
cfg,
|
|
334
|
+
image: buffer,
|
|
335
|
+
imageType: "message",
|
|
336
|
+
accountId
|
|
337
|
+
});
|
|
338
|
+
log.debug(`image uploaded: imageKey=${imageKey}`);
|
|
339
|
+
return sendImageLark({
|
|
340
|
+
cfg,
|
|
341
|
+
to,
|
|
342
|
+
imageKey,
|
|
343
|
+
replyToMessageId,
|
|
344
|
+
replyInThread,
|
|
345
|
+
accountId
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const fileType = detectFileType(resolvedFileName);
|
|
349
|
+
const isAudio = fileType === "opus";
|
|
350
|
+
const isVideo = fileType === "mp4";
|
|
351
|
+
const duration = isAudio ? parseOggOpusDuration(buffer) : isVideo ? parseMp4Duration(buffer) : void 0;
|
|
352
|
+
const { fileKey } = await uploadFileLark({
|
|
353
|
+
cfg,
|
|
354
|
+
file: buffer,
|
|
355
|
+
fileName: resolvedFileName,
|
|
356
|
+
fileType,
|
|
357
|
+
duration,
|
|
358
|
+
accountId
|
|
359
|
+
});
|
|
360
|
+
log.debug(
|
|
361
|
+
`file uploaded: fileKey=${fileKey}, fileType=${fileType}${isAudio || isVideo ? `, duration=${duration ?? "unknown"}ms` : ""}`
|
|
362
|
+
);
|
|
363
|
+
if (isAudio) {
|
|
364
|
+
return sendAudioLark({ cfg, to, fileKey, replyToMessageId, replyInThread, accountId });
|
|
365
|
+
}
|
|
366
|
+
if (isVideo) {
|
|
367
|
+
return sendVideoLark({ cfg, to, fileKey, replyToMessageId, replyInThread, accountId });
|
|
368
|
+
}
|
|
369
|
+
return sendFileLark({
|
|
370
|
+
cfg,
|
|
371
|
+
to,
|
|
372
|
+
fileKey,
|
|
373
|
+
replyToMessageId,
|
|
374
|
+
replyInThread,
|
|
375
|
+
accountId
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function isPrivateIP(ip) {
|
|
379
|
+
if (ip.startsWith("127.")) return true;
|
|
380
|
+
if (ip.startsWith("10.")) return true;
|
|
381
|
+
if (ip.startsWith("192.168.")) return true;
|
|
382
|
+
if (ip.startsWith("169.254.")) return true;
|
|
383
|
+
if (ip === "0.0.0.0") return true;
|
|
384
|
+
if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(ip)) return true;
|
|
385
|
+
if (ip === "::1" || ip === "::") return true;
|
|
386
|
+
if (ip.startsWith("fe80:")) return true;
|
|
387
|
+
if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
async function validateRemoteUrl(raw) {
|
|
391
|
+
const parsed = new URL(raw);
|
|
392
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`[feishu-media] Unsupported protocol "${parsed.protocol}" in URL "${raw}". Only http:// and https:// are allowed for remote media.`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
398
|
+
if (net.isIP(hostname)) {
|
|
399
|
+
if (isPrivateIP(hostname)) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`[feishu-media] Access to private/reserved IP "${hostname}" is denied (SSRF protection). URL: "${raw}"`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
try {
|
|
406
|
+
const addresses = await dns.resolve(hostname);
|
|
407
|
+
for (const addr of addresses) {
|
|
408
|
+
if (isPrivateIP(addr)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`[feishu-media] Domain "${hostname}" resolves to private/reserved IP "${addr}" (SSRF protection). URL: "${raw}"`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (err instanceof Error && err.message.includes("SSRF protection")) {
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
log.warn(`[feishu-media] DNS resolution failed for "${hostname}": ${err}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function fetchMediaBuffer(urlOrPath, localRoots) {
|
|
423
|
+
const raw = normalizeMediaUrlInput(urlOrPath);
|
|
424
|
+
if (isLocalMediaPath(raw)) {
|
|
425
|
+
const filePath = raw.startsWith("file://") ? safeFileUrlToPath(raw) : raw;
|
|
426
|
+
if (localRoots !== void 0) {
|
|
427
|
+
validateLocalMediaRoots(filePath, localRoots);
|
|
428
|
+
} else {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`[feishu-media] Local file access denied for "${filePath}": mediaLocalRoots is not configured. Configure mediaLocalRoots to explicitly allow local file access.`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const buf = fs.readFileSync(filePath);
|
|
434
|
+
log.debug(`local file read: "${filePath}", ${buf.length} bytes`);
|
|
435
|
+
return buf;
|
|
436
|
+
}
|
|
437
|
+
await validateRemoteUrl(raw);
|
|
438
|
+
const FETCH_TIMEOUT_MS = 3e4;
|
|
439
|
+
log.info(`fetching remote media: ${raw}`);
|
|
440
|
+
const response = await fetch(raw, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`[feishu-media] Failed to fetch media from "${raw}": HTTP ${response.status} ${response.statusText}. Verify the URL is accessible and returns a valid media resource.`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
447
|
+
log.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);
|
|
448
|
+
return Buffer.from(arrayBuffer);
|
|
449
|
+
}
|
|
450
|
+
export {
|
|
451
|
+
detectFileType,
|
|
452
|
+
downloadMessageResourceFeishu,
|
|
453
|
+
parseMp4Duration,
|
|
454
|
+
parseOggOpusDuration,
|
|
455
|
+
sendAudioLark,
|
|
456
|
+
sendFileLark,
|
|
457
|
+
sendImageLark,
|
|
458
|
+
sendVideoLark,
|
|
459
|
+
uploadAndSendMediaLark,
|
|
460
|
+
uploadFileLark,
|
|
461
|
+
uploadImageLark
|
|
462
|
+
};
|
|
463
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/messaging/outbound/media.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * Media handling for the Lark/Feishu channel plugin.\n *\n * Provides functions for downloading images and file resources from\n * Feishu messages, uploading media to the Feishu IM storage, and\n * sending image / file messages to chats.\n */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport * as dns from 'node:dns/promises';\nimport * as fs from 'node:fs';\nimport * as net from 'node:net';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { Readable } from 'node:stream';\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport { LarkClient } from '../../core/lark-client';\nimport { normalizeFeishuTarget, resolveReceiveIdType } from '../../core/targets';\nimport {\n isLocalMediaPath,\n normalizeMediaUrlInput,\n resolveFileNameFromMediaUrl,\n safeFileUrlToPath,\n validateLocalMediaRoots,\n} from './media-url-utils';\nimport { larkLogger } from '../../core/lark-logger';\n\nconst log = larkLogger('outbound/media');\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Result of downloading an image from Feishu.\n */\nexport interface DownloadImageResult {\n /** The raw image bytes. */\n buffer: Buffer;\n /** The MIME type of the image (e.g. \"image/png\"), if known. */\n contentType?: string;\n}\n\n/**\n * Result of downloading a message resource (image or file) from Feishu.\n */\nexport interface DownloadMessageResourceResult {\n /** The raw file bytes. */\n buffer: Buffer;\n /** The MIME type of the resource, if known. */\n contentType?: string;\n /** The original file name, if available. */\n fileName?: string;\n}\n\n/**\n * Result of uploading an image to Feishu.\n */\nexport interface UploadImageResult {\n /** The image_key assigned by Feishu, used to reference the image. */\n imageKey: string;\n}\n\n/**\n * Result of uploading a file to Feishu.\n */\nexport interface UploadFileResult {\n /** The file_key assigned by Feishu, used to reference the file. */\n fileKey: string;\n}\n\n/**\n * Result of sending a media (image or file) message.\n */\nexport interface SendMediaResult {\n /** Platform-assigned message ID. */\n messageId: string;\n /** Chat ID where the media was sent. */\n chatId: string;\n}\n\n// ---------------------------------------------------------------------------\n// Response extraction helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract a Buffer from various SDK response formats.\n *\n * The Feishu Node SDK can return binary data in several shapes depending\n * on the runtime environment and SDK version:\n * - A Buffer directly\n * - An ArrayBuffer\n * - A response object with a `.data` property\n * - A response object with `.getReadableStream()`\n * - A response object with `.writeFile(path)`\n * - An async iterable / iterator\n * - A Node.js Readable stream\n *\n * This helper normalises all of those into a single Buffer.\n */\nasync function extractBufferFromResponse(response: unknown): Promise<{ buffer: Buffer; contentType?: string }> {\n // Direct Buffer\n if (Buffer.isBuffer(response)) {\n return { buffer: response };\n }\n\n // ArrayBuffer\n if (response instanceof ArrayBuffer) {\n return { buffer: Buffer.from(response) };\n }\n\n // Null / undefined guard\n if (response == null) {\n throw new Error('[feishu-media] Received null/undefined response');\n }\n\n const resp = response as Record<string, any>;\n const contentType: string | undefined = resp.headers?.['content-type'] ?? resp.contentType ?? undefined;\n\n // Response with .data as Buffer or ArrayBuffer\n if (resp.data != null) {\n if (Buffer.isBuffer(resp.data)) {\n return { buffer: resp.data, contentType };\n }\n if (resp.data instanceof ArrayBuffer) {\n return { buffer: Buffer.from(resp.data), contentType };\n }\n // .data might itself be a readable stream\n if (typeof resp.data.pipe === 'function') {\n const buf = await streamToBuffer(resp.data as Readable);\n return { buffer: buf, contentType };\n }\n }\n\n // Response with .getReadableStream()\n if (typeof resp.getReadableStream === 'function') {\n const stream = await resp.getReadableStream();\n const buf = await streamToBuffer(stream);\n return { buffer: buf, contentType };\n }\n\n // Response with .writeFile(path) -- write to a temp file and read back.\n if (typeof resp.writeFile === 'function') {\n const tmpDir = os.tmpdir();\n const tmpFile = path.join(tmpDir, `feishu-media-${Date.now()}`);\n try {\n await resp.writeFile(tmpFile);\n const buf = fs.readFileSync(tmpFile);\n return { buffer: buf, contentType };\n } finally {\n // Clean up the temp file.\n try {\n fs.unlinkSync(tmpFile);\n } catch {\n // Ignore cleanup errors.\n }\n }\n }\n\n // Async iterable / iterator (e.g. response body chunks)\n if (typeof (resp as any)[Symbol.asyncIterator] === 'function' || typeof (resp as any).next === 'function') {\n const chunks: Buffer[] = [];\n const iterable =\n typeof (resp as any)[Symbol.asyncIterator] === 'function'\n ? (resp as AsyncIterable<Uint8Array>)\n : asyncIteratorToIterable(resp as AsyncIterator<Uint8Array>);\n\n for await (const chunk of iterable) {\n chunks.push(Buffer.from(chunk));\n }\n return { buffer: Buffer.concat(chunks), contentType };\n }\n\n // Node.js Readable stream\n if (typeof resp.pipe === 'function') {\n const buf = await streamToBuffer(resp as Readable);\n return { buffer: buf, contentType };\n }\n\n throw new Error('[feishu-media] Unable to extract binary data from response: unrecognised format');\n}\n\n/**\n * Consume a Readable stream into a Buffer.\n */\nfunction streamToBuffer(stream: Readable): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n const chunks: Buffer[] = [];\n stream.on('data', (chunk: Buffer | Uint8Array) => {\n chunks.push(Buffer.from(chunk));\n });\n stream.on('end', () => resolve(Buffer.concat(chunks)));\n stream.on('error', reject);\n });\n}\n\n/**\n * Wrap an AsyncIterator into an AsyncIterable.\n */\nasync function* asyncIteratorToIterable<T>(iterator: AsyncIterator<T>): AsyncIterable<T> {\n while (true) {\n const { value, done } = await iterator.next();\n if (done) break;\n yield value;\n }\n}\n\n// ---------------------------------------------------------------------------\n// downloadMessageResourceFeishu\n// ---------------------------------------------------------------------------\n\n/**\n * Download a resource (image or file) attached to a specific message.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.messageId - The message the resource belongs to.\n * @param params.fileKey - The file_key or image_key of the resource.\n * @param params.type - Whether the resource is an \"image\" or \"file\".\n * @param params.accountId - Optional account identifier.\n * @returns The resource buffer, content type, and file name.\n */\nexport async function downloadMessageResourceFeishu(params: {\n cfg: OpenClawConfig;\n messageId: string;\n fileKey: string;\n type: 'image' | 'file';\n accountId?: string;\n}): Promise<DownloadMessageResourceResult> {\n const { cfg, messageId, fileKey, type, accountId } = params;\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n\n const response = await client.im.messageResource.get({\n path: {\n message_id: messageId,\n file_key: fileKey,\n },\n params: {\n type,\n },\n });\n\n const { buffer, contentType } = await extractBufferFromResponse(response);\n\n // Attempt to extract file name from response headers.\n let fileName: string | undefined;\n if (response && typeof response === 'object') {\n const resp = response as Record<string, any>;\n const disposition = resp.headers?.['content-disposition'] ?? resp.headers?.['Content-Disposition'];\n if (typeof disposition === 'string') {\n const match = disposition.match(/filename[*]?=(?:UTF-8'')?[\"']?([^\"';\\n]+)/i);\n if (match) {\n fileName = decodeURIComponent(match[1].trim());\n }\n }\n }\n\n return { buffer, contentType, fileName };\n}\n\n// ---------------------------------------------------------------------------\n// uploadImageLark\n// ---------------------------------------------------------------------------\n\n/**\n * Upload an image to Feishu IM storage.\n *\n * Accepts either a Buffer containing the raw image bytes or a file\n * system path to read from.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.image - A Buffer or local file path for the image.\n * @param params.imageType - The image usage type: \"message\" (default) or \"avatar\".\n * @param params.accountId - Optional account identifier.\n * @returns The assigned image_key.\n */\nexport async function uploadImageLark(params: {\n cfg: OpenClawConfig;\n image: Buffer | string;\n imageType?: 'message' | 'avatar';\n accountId?: string;\n}): Promise<UploadImageResult> {\n const { cfg, image, imageType = 'message', accountId } = params;\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const imageStream = Buffer.isBuffer(image) ? Readable.from(image) : fs.createReadStream(image);\n\n const response = await client.im.image.create({\n data: { image_type: imageType, image: imageStream as any },\n });\n\n const imageKey = (response as any)?.data?.image_key ?? (response as any)?.image_key;\n if (!imageKey) {\n throw new Error(\n '[feishu-media] Image upload failed: no image_key in response. ' +\n `Check that the image is a valid format (JPEG/PNG/GIF/BMP/WEBP). ` +\n `Response: ${JSON.stringify(response).slice(0, 200)}`,\n );\n }\n\n return { imageKey };\n}\n\n// ---------------------------------------------------------------------------\n// uploadFileLark\n// ---------------------------------------------------------------------------\n\n/**\n * Upload a file to Feishu IM storage.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.file - A Buffer or local file path.\n * @param params.fileName - The display name of the file.\n * @param params.fileType - Feishu file type: \"opus\" | \"mp4\" | \"pdf\" | \"doc\" | \"xls\" | \"ppt\" | \"stream\".\n * @param params.duration - Duration in milliseconds (for audio/video files).\n * @param params.accountId - Optional account identifier.\n * @returns The assigned file_key.\n */\nexport async function uploadFileLark(params: {\n cfg: OpenClawConfig;\n file: Buffer | string;\n fileName: string;\n fileType: 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream';\n duration?: number;\n accountId?: string;\n}): Promise<UploadFileResult> {\n const { cfg, file, fileName, fileType, duration, accountId } = params;\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const fileStream = Buffer.isBuffer(file) ? Readable.from(file) : fs.createReadStream(file);\n\n const response = await client.im.file.create({\n data: {\n file_type: fileType,\n file_name: fileName,\n file: fileStream,\n ...(duration !== undefined ? { duration: String(duration) } : {}),\n } as any,\n });\n\n const fileKey = (response as any)?.data?.file_key ?? (response as any)?.file_key;\n if (!fileKey) {\n throw new Error(\n `[feishu-media] File upload failed: no file_key in response for \"${fileName}\" (type=${fileType}). ` +\n `Response: ${JSON.stringify(response).slice(0, 200)}`,\n );\n }\n\n return { fileKey };\n}\n\n// ---------------------------------------------------------------------------\n// Shared media message sender\n// ---------------------------------------------------------------------------\n\n/**\n * Unified media message sender \u2014 handles both reply and create paths for\n * image / file / audio `msg_type` values.\n *\n * Mirrors {@link sendImMessage} in `deliver.ts` (which covers \"post\" and\n * \"interactive\"), extracted here to avoid a cross-module dependency.\n */\nasync function sendMediaMessage(params: {\n client: ReturnType<typeof LarkClient.fromCfg>['sdk'];\n to: string;\n content: string;\n msgType: 'image' | 'file' | 'audio' | 'media';\n replyToMessageId?: string;\n replyInThread?: boolean;\n}): Promise<SendMediaResult> {\n const { client, to, content, msgType, replyToMessageId, replyInThread } = params;\n\n if (replyToMessageId) {\n const response = await client.im.message.reply({\n path: { message_id: replyToMessageId },\n data: { content, msg_type: msgType, reply_in_thread: replyInThread },\n });\n return {\n messageId: response?.data?.message_id ?? '',\n chatId: response?.data?.chat_id ?? '',\n };\n }\n\n const target = normalizeFeishuTarget(to);\n if (!target) {\n throw new Error(\n `[feishu-media] Cannot send ${msgType} message: \"${to}\" is not a valid target. ` +\n `Expected a chat_id (oc_*), open_id (ou_*), or user_id.`,\n );\n }\n\n const receiveIdType = resolveReceiveIdType(target);\n const response = await client.im.message.create({\n params: { receive_id_type: receiveIdType as any },\n data: { receive_id: target, msg_type: msgType, content },\n });\n\n return {\n messageId: response?.data?.message_id ?? '',\n chatId: response?.data?.chat_id ?? '',\n };\n}\n\n// ---------------------------------------------------------------------------\n// sendImageLark\n// ---------------------------------------------------------------------------\n\n/**\n * Send an image message to a chat or user.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.to - Target identifier.\n * @param params.imageKey - The image_key from a previous upload.\n * @param params.replyToMessageId - Optional message ID for threaded reply.\n * @param params.replyInThread - When true, reply appears in thread.\n * @param params.accountId - Optional account identifier.\n * @returns The send result.\n */\nexport async function sendImageLark(params: {\n cfg: OpenClawConfig;\n to: string;\n imageKey: string;\n replyToMessageId?: string;\n replyInThread?: boolean;\n accountId?: string;\n}): Promise<SendMediaResult> {\n const { cfg, to, imageKey, replyToMessageId, replyInThread, accountId } = params;\n log.info(`sendImageLark: target=${to}, imageKey=${imageKey}`);\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const content = JSON.stringify({ image_key: imageKey });\n return sendMediaMessage({ client, to, content, msgType: 'image', replyToMessageId, replyInThread });\n}\n\n// ---------------------------------------------------------------------------\n// sendFileLark\n// ---------------------------------------------------------------------------\n\n/**\n * Send a file message to a chat or user.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.to - Target identifier.\n * @param params.fileKey - The file_key from a previous upload.\n * @param params.replyToMessageId - Optional message ID for threaded reply.\n * @param params.replyInThread - When true, reply appears in thread.\n * @param params.accountId - Optional account identifier.\n * @returns The send result.\n */\nexport async function sendFileLark(params: {\n cfg: OpenClawConfig;\n to: string;\n fileKey: string;\n replyToMessageId?: string;\n replyInThread?: boolean;\n accountId?: string;\n}): Promise<SendMediaResult> {\n const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;\n log.info(`sendFileLark: target=${to}, fileKey=${fileKey}`);\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const content = JSON.stringify({ file_key: fileKey });\n return sendMediaMessage({ client, to, content, msgType: 'file', replyToMessageId, replyInThread });\n}\n\n// ---------------------------------------------------------------------------\n// sendVideoLark\n// ---------------------------------------------------------------------------\n\n/**\n * Send a video message to a chat or user.\n *\n * Uses `msg_type: \"media\"` so Feishu renders the message as a playable\n * video instead of a file attachment.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.to - Target identifier.\n * @param params.fileKey - The file_key from a previous upload.\n * @param params.replyToMessageId - Optional message ID for threaded reply.\n * @param params.replyInThread - When true, reply appears in thread.\n * @param params.accountId - Optional account identifier.\n * @returns The send result.\n */\nexport async function sendVideoLark(params: {\n cfg: OpenClawConfig;\n to: string;\n fileKey: string;\n replyToMessageId?: string;\n replyInThread?: boolean;\n accountId?: string;\n}): Promise<SendMediaResult> {\n const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;\n log.info(`sendVideoLark: target=${to}, fileKey=${fileKey}`);\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const content = JSON.stringify({ file_key: fileKey });\n return sendMediaMessage({ client, to, content, msgType: 'media', replyToMessageId, replyInThread });\n}\n\n// ---------------------------------------------------------------------------\n// sendAudioLark\n// ---------------------------------------------------------------------------\n\n/**\n * Send an audio message to a chat or user.\n *\n * Uses `msg_type: \"audio\"` so Feishu renders the message as a playable\n * voice bubble instead of a file attachment.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.to - Target identifier.\n * @param params.fileKey - The file_key from a previous upload.\n * @param params.replyToMessageId - Optional message ID for threaded reply.\n * @param params.replyInThread - When true, reply appears in thread.\n * @param params.accountId - Optional account identifier.\n * @returns The send result.\n */\nexport async function sendAudioLark(params: {\n cfg: OpenClawConfig;\n to: string;\n fileKey: string;\n replyToMessageId?: string;\n replyInThread?: boolean;\n accountId?: string;\n}): Promise<SendMediaResult> {\n const { cfg, to, fileKey, replyToMessageId, replyInThread, accountId } = params;\n log.info(`sendAudioLark: target=${to}, fileKey=${fileKey}`);\n\n const client = LarkClient.fromCfg(cfg, accountId).sdk;\n const content = JSON.stringify({ file_key: fileKey });\n return sendMediaMessage({ client, to, content, msgType: 'audio', replyToMessageId, replyInThread });\n}\n\n// ---------------------------------------------------------------------------\n// detectFileType\n// ---------------------------------------------------------------------------\n\n/** Known image extensions. */\nconst IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.ico', '.tiff', '.tif', '.heic']);\n\n/** Extension-to-Feishu-file-type mapping. */\nconst EXTENSION_TYPE_MAP: Record<string, 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream'> = {\n '.opus': 'opus',\n '.ogg': 'opus',\n '.mp4': 'mp4',\n '.mov': 'mp4',\n '.avi': 'mp4',\n '.mkv': 'mp4',\n '.webm': 'mp4',\n '.pdf': 'pdf',\n '.doc': 'doc',\n '.docx': 'doc',\n '.xls': 'xls',\n '.xlsx': 'xls',\n '.csv': 'xls',\n '.ppt': 'ppt',\n '.pptx': 'ppt',\n};\n\n/**\n * Detect the Feishu file type from a file name extension.\n *\n * Returns one of the Feishu-supported file type strings, or \"stream\"\n * as a catch-all for unrecognised extensions.\n *\n * @param fileName - The file name (with extension).\n * @returns The detected file type.\n */\nexport function detectFileType(fileName: string): 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream' {\n const ext = path.extname(fileName).toLowerCase();\n return EXTENSION_TYPE_MAP[ext] ?? 'stream';\n}\n\n/**\n * Parse the duration (in milliseconds) from an OGG/Opus audio buffer.\n *\n * Scans backward from the end of the buffer to find the last OggS page\n * header, reads the granule position (absolute sample count), and divides\n * by 48 000 (the Opus standard sample rate) then converts to milliseconds.\n *\n * Returns `undefined` when the buffer cannot be parsed (e.g. truncated or\n * not actually OGG). This is intentionally lenient so callers can fall\n * back gracefully.\n */\nexport function parseOggOpusDuration(buffer: Buffer): number | undefined {\n // OggS magic bytes: 0x4f 0x67 0x67 0x53\n const OGGS = Buffer.from('OggS');\n\n // Scan backwards for the last OggS sync word.\n let offset = -1;\n for (let i = buffer.length - OGGS.length; i >= 0; i--) {\n if (buffer[i] === 0x4f && buffer.compare(OGGS, 0, 4, i, i + 4) === 0) {\n offset = i;\n break;\n }\n }\n\n if (offset < 0) return undefined;\n\n // Granule position is at bytes 6..13 of the page header (8 bytes, little-endian).\n const granuleOffset = offset + 6;\n if (granuleOffset + 8 > buffer.length) return undefined;\n\n // Read as two 32-bit LE values and combine (avoids BigInt for portability).\n const lo = buffer.readUInt32LE(granuleOffset);\n const hi = buffer.readUInt32LE(granuleOffset + 4);\n const granule = hi * 0x1_0000_0000 + lo;\n\n if (granule <= 0) return undefined;\n\n return Math.ceil(granule / 48_000) * 1000;\n}\n\n/**\n * Parse the duration (in milliseconds) from an MP4 video buffer.\n *\n * Scans top-level boxes to locate the `moov` container, then finds the\n * `mvhd` (Movie Header) box inside it. The `mvhd` box stores:\n * - **timescale**: number of time-units per second\n * - **duration**: total duration in those time-units\n *\n * Supports both version-0 (32-bit fields) and version-1 (64-bit fields)\n * of the `mvhd` box.\n *\n * Returns `undefined` when the buffer cannot be parsed (e.g. truncated,\n * `moov` at end of a huge file not fully buffered, or not actually MP4).\n */\nexport function parseMp4Duration(buffer: Buffer): number | undefined {\n // Locate `moov` among top-level boxes.\n const moovData = findBox(buffer, 0, buffer.length, 'moov');\n if (!moovData) return undefined;\n\n // Locate `mvhd` inside `moov`.\n const mvhdData = findBox(buffer, moovData.dataStart, moovData.dataEnd, 'mvhd');\n if (!mvhdData) return undefined;\n\n const off = mvhdData.dataStart;\n if (off + 1 > buffer.length) return undefined;\n\n const version = buffer.readUInt8(off);\n\n let timescale: number;\n let duration: number;\n\n if (version === 0) {\n // version(1) + flags(3) + creation(4) + modification(4) + timescale(4) + duration(4) = 20 bytes\n if (off + 20 > buffer.length) return undefined;\n timescale = buffer.readUInt32BE(off + 12);\n duration = buffer.readUInt32BE(off + 16);\n } else {\n // version(1) + flags(3) + creation(8) + modification(8) + timescale(4) + duration(8) = 32 bytes\n if (off + 32 > buffer.length) return undefined;\n timescale = buffer.readUInt32BE(off + 20);\n // Read 64-bit duration as two 32-bit halves (avoids BigInt).\n const hi = buffer.readUInt32BE(off + 24);\n const lo = buffer.readUInt32BE(off + 28);\n duration = hi * 0x1_0000_0000 + lo;\n }\n\n if (timescale <= 0 || duration <= 0) return undefined;\n\n return Math.round((duration / timescale) * 1000);\n}\n\n/**\n * Find a box (atom) by its 4-character type within a range of the buffer.\n * Returns the data start/end offsets (after the 8-byte box header), or\n * `undefined` if not found.\n */\nfunction findBox(\n buffer: Buffer,\n start: number,\n end: number,\n type: string,\n): { dataStart: number; dataEnd: number } | undefined {\n let offset = start;\n while (offset + 8 <= end) {\n const size = buffer.readUInt32BE(offset);\n const boxType = buffer.toString('ascii', offset + 4, offset + 8);\n\n // size == 0 means box extends to the end; size == 1 means 64-bit extended size.\n let boxEnd: number;\n let dataStart: number;\n if (size === 0) {\n boxEnd = end;\n dataStart = offset + 8;\n } else if (size === 1) {\n if (offset + 16 > end) break;\n const hi = buffer.readUInt32BE(offset + 8);\n const lo = buffer.readUInt32BE(offset + 12);\n boxEnd = offset + hi * 0x1_0000_0000 + lo;\n dataStart = offset + 16;\n } else {\n if (size < 8) break; // invalid\n boxEnd = offset + size;\n dataStart = offset + 8;\n }\n\n if (boxType === type) {\n return { dataStart, dataEnd: Math.min(boxEnd, end) };\n }\n\n offset = boxEnd;\n }\n return undefined;\n}\n\n/**\n * Check whether a file name has an image extension.\n */\nfunction isImageFileName(fileName: string): boolean {\n const ext = path.extname(fileName).toLowerCase();\n return IMAGE_EXTENSIONS.has(ext);\n}\n\n// ---------------------------------------------------------------------------\n// uploadAndSendMediaLark\n// ---------------------------------------------------------------------------\n\n/**\n * Upload and send a media file (image or general file) in one step.\n *\n * Accepts either a URL (remote or local `file://`) or a raw Buffer.\n * The function determines whether the media is an image (by extension)\n * and uses the appropriate upload/send path.\n *\n * @param params.cfg - Plugin configuration.\n * @param params.to - Target identifier.\n * @param params.mediaUrl - URL of the media (http/https or local path).\n * @param params.mediaBuffer - Raw bytes of the media (alternative to URL).\n * @param params.fileName - File name (used for type detection and display).\n * @param params.replyToMessageId - Optional message ID for threaded reply.\n * @param params.accountId - Optional account identifier.\n * @returns The send result.\n */\nexport async function uploadAndSendMediaLark(params: {\n cfg: OpenClawConfig;\n to: string;\n mediaUrl?: string;\n mediaBuffer?: Buffer;\n fileName?: string;\n replyToMessageId?: string;\n replyInThread?: boolean;\n accountId?: string;\n /** Allowed root directories for local file access (SSRF prevention). */\n mediaLocalRoots?: readonly string[];\n}): Promise<SendMediaResult> {\n const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, replyInThread, accountId, mediaLocalRoots } =\n params;\n\n log.info(\n `uploadAndSendMediaLark: target=${to}, ` +\n `source=${mediaBuffer ? 'buffer' : (mediaUrl ?? '(none)')}, fileName=${fileName ?? '(auto)'}`,\n );\n\n // Resolve the media to a Buffer.\n let buffer: Buffer;\n let resolvedFileName = fileName ?? 'file';\n\n if (mediaBuffer) {\n buffer = mediaBuffer;\n log.debug(`using provided buffer: ${buffer.length} bytes`);\n } else if (mediaUrl) {\n buffer = await fetchMediaBuffer(mediaUrl, mediaLocalRoots);\n log.debug(`fetched media: ${buffer.length} bytes from \"${mediaUrl}\"`);\n\n // Derive a file name from the URL if none was provided.\n if (!fileName) {\n const derivedFileName = resolveFileNameFromMediaUrl(mediaUrl);\n if (derivedFileName) {\n resolvedFileName = derivedFileName;\n }\n }\n } else {\n throw new Error(\n '[feishu-media] uploadAndSendMediaLark requires either mediaUrl or mediaBuffer. ' +\n 'Provide a URL (http/https/file://) or a raw Buffer to send media.',\n );\n }\n\n // Decide whether to send as image or file based on the extension.\n const isImage = isImageFileName(resolvedFileName);\n log.info(`resolved: fileName=\"${resolvedFileName}\", ` + `type=${isImage ? 'image' : 'file'}, size=${buffer.length}`);\n\n if (isImage) {\n // Upload as image, then send image message.\n const { imageKey } = await uploadImageLark({\n cfg,\n image: buffer,\n imageType: 'message',\n accountId,\n });\n log.debug(`image uploaded: imageKey=${imageKey}`);\n\n return sendImageLark({\n cfg,\n to,\n imageKey,\n replyToMessageId,\n replyInThread,\n accountId,\n });\n }\n\n // Upload as file, then send as file or audio message.\n const fileType = detectFileType(resolvedFileName);\n const isAudio = fileType === 'opus';\n const isVideo = fileType === 'mp4';\n const duration = isAudio ? parseOggOpusDuration(buffer) : isVideo ? parseMp4Duration(buffer) : undefined;\n\n const { fileKey } = await uploadFileLark({\n cfg,\n file: buffer,\n fileName: resolvedFileName,\n fileType,\n duration,\n accountId,\n });\n log.debug(\n `file uploaded: fileKey=${fileKey}, ` +\n `fileType=${fileType}${isAudio || isVideo ? `, duration=${duration ?? 'unknown'}ms` : ''}`,\n );\n\n if (isAudio) {\n return sendAudioLark({ cfg, to, fileKey, replyToMessageId, replyInThread, accountId });\n }\n\n if (isVideo) {\n return sendVideoLark({ cfg, to, fileKey, replyToMessageId, replyInThread, accountId });\n }\n\n return sendFileLark({\n cfg,\n to,\n fileKey,\n replyToMessageId,\n replyInThread,\n accountId,\n });\n}\n\n// ---------------------------------------------------------------------------\n// SSRF protection \u2014 private/reserved IP filtering\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether an IP address belongs to a private or reserved range.\n *\n * Blocks: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,\n * 169.254.0.0/16 (link-local / cloud metadata), 0.0.0.0,\n * IPv6 loopback (::1), link-local (fe80::), ULA (fc/fd).\n */\nfunction isPrivateIP(ip: string): boolean {\n // IPv4 private / reserved ranges\n if (ip.startsWith('127.')) return true;\n if (ip.startsWith('10.')) return true;\n if (ip.startsWith('192.168.')) return true;\n if (ip.startsWith('169.254.')) return true;\n if (ip === '0.0.0.0') return true;\n if (/^172\\.(1[6-9]|2[0-9]|3[01])\\./.test(ip)) return true;\n\n // IPv6 private / reserved ranges\n if (ip === '::1' || ip === '::') return true;\n if (ip.startsWith('fe80:')) return true; // link-local\n if (ip.startsWith('fc') || ip.startsWith('fd')) return true; // ULA\n return false;\n}\n\n/**\n * Validate that a remote URL does not target private/reserved IP addresses.\n *\n * Resolves the hostname via DNS and checks all returned addresses.\n * Rejects URLs with non-http(s) protocols.\n */\nasync function validateRemoteUrl(raw: string): Promise<void> {\n const parsed = new URL(raw);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(\n `[feishu-media] Unsupported protocol \"${parsed.protocol}\" in URL \"${raw}\". ` +\n `Only http:// and https:// are allowed for remote media.`,\n );\n }\n\n const hostname = parsed.hostname.replace(/^\\[|\\]$/g, '');\n\n if (net.isIP(hostname)) {\n // URL contains a literal IP address \u2014 check it directly.\n if (isPrivateIP(hostname)) {\n throw new Error(\n `[feishu-media] Access to private/reserved IP \"${hostname}\" is denied (SSRF protection). ` +\n `URL: \"${raw}\"`,\n );\n }\n } else {\n // Resolve the domain and check every address it points to.\n try {\n const addresses = await dns.resolve(hostname);\n for (const addr of addresses) {\n if (isPrivateIP(addr)) {\n throw new Error(\n `[feishu-media] Domain \"${hostname}\" resolves to private/reserved IP \"${addr}\" (SSRF protection). ` +\n `URL: \"${raw}\"`,\n );\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.includes('SSRF protection')) {\n throw err;\n }\n // DNS failure is logged but not blocking \u2014 the subsequent fetch will\n // produce a clear network error if the host is truly unreachable.\n log.warn(`[feishu-media] DNS resolution failed for \"${hostname}\": ${err}`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// fetchMediaBuffer\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch media bytes from a URL or local file path.\n *\n * Supports:\n * - `http://` and `https://` URLs (fetched via the global `fetch` API)\n * - `file://` URLs and bare file system paths (read from disk, gated\n * by `localRoots` for path-traversal prevention)\n */\nasync function fetchMediaBuffer(urlOrPath: string, localRoots?: readonly string[]): Promise<Buffer> {\n const raw = normalizeMediaUrlInput(urlOrPath);\n\n // Local file path (absolute or relative, or file:// URL).\n if (isLocalMediaPath(raw)) {\n const filePath = raw.startsWith('file://') ? safeFileUrlToPath(raw) : raw;\n\n if (localRoots !== undefined) {\n // Explicit allowlist configured \u2014 enforce path restriction.\n validateLocalMediaRoots(filePath, localRoots);\n } else {\n // Deny by default: unconfigured mediaLocalRoots must not allow\n // arbitrary local file reads.\n throw new Error(\n `[feishu-media] Local file access denied for \"${filePath}\": ` +\n `mediaLocalRoots is not configured. ` +\n `Configure mediaLocalRoots to explicitly allow local file access.`,\n );\n }\n\n const buf = fs.readFileSync(filePath);\n log.debug(`local file read: \"${filePath}\", ${buf.length} bytes`);\n return buf;\n }\n\n // Remote URL \u2014 validate against SSRF before fetching.\n await validateRemoteUrl(raw);\n\n const FETCH_TIMEOUT_MS = 30_000;\n log.info(`fetching remote media: ${raw}`);\n const response = await fetch(raw, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n if (!response.ok) {\n throw new Error(\n `[feishu-media] Failed to fetch media from \"${raw}\": ` +\n `HTTP ${response.status} ${response.statusText}. ` +\n `Verify the URL is accessible and returns a valid media resource.`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n log.debug(`remote media fetched: ${raw}, ${arrayBuffer.byteLength} bytes`);\n return Buffer.from(arrayBuffer);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["response"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LarkClient } from "../../core/lark-client";
|
|
2
|
+
import { sendTextLark, sendMediaLark, sendCardLark } from "./deliver";
|
|
3
|
+
import { larkLogger } from "../../core/lark-logger";
|
|
4
|
+
const log = larkLogger("outbound/outbound");
|
|
5
|
+
function resolveFeishuSendContext(params) {
|
|
6
|
+
return {
|
|
7
|
+
cfg: params.cfg,
|
|
8
|
+
replyToMessageId: params.replyToId ?? void 0,
|
|
9
|
+
replyInThread: Boolean(params.threadId),
|
|
10
|
+
accountId: params.accountId ?? void 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const feishuOutbound = {
|
|
14
|
+
deliveryMode: "direct",
|
|
15
|
+
chunker: (text, limit) => LarkClient.runtime.channel.text.chunkMarkdownText(text, limit),
|
|
16
|
+
chunkerMode: "markdown",
|
|
17
|
+
textChunkLimit: 15e3,
|
|
18
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
19
|
+
log.info(`sendText: target=${to}, textLength=${text.length}`);
|
|
20
|
+
const ctx = resolveFeishuSendContext({ cfg, accountId, replyToId, threadId });
|
|
21
|
+
const result = await sendTextLark({ ...ctx, to, text });
|
|
22
|
+
return { channel: "feishu", ...result };
|
|
23
|
+
},
|
|
24
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, replyToId, threadId }) => {
|
|
25
|
+
log.info(`sendMedia: target=${to}, hasText=${Boolean(text?.trim())}, mediaUrl=${mediaUrl ?? "(none)"}`);
|
|
26
|
+
const ctx = resolveFeishuSendContext({ cfg, accountId, replyToId, threadId });
|
|
27
|
+
if (text?.trim()) {
|
|
28
|
+
await sendTextLark({ ...ctx, to, text });
|
|
29
|
+
}
|
|
30
|
+
if (!mediaUrl) {
|
|
31
|
+
log.info("sendMedia: no mediaUrl provided, falling back to text-only");
|
|
32
|
+
const result2 = await sendTextLark({ ...ctx, to, text: text ?? "" });
|
|
33
|
+
return { channel: "feishu", ...result2 };
|
|
34
|
+
}
|
|
35
|
+
const result = await sendMediaLark({ ...ctx, to, mediaUrl, mediaLocalRoots });
|
|
36
|
+
return {
|
|
37
|
+
channel: "feishu",
|
|
38
|
+
messageId: result.messageId,
|
|
39
|
+
chatId: result.chatId,
|
|
40
|
+
...result.warning ? { meta: { warnings: [result.warning] } } : {}
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
sendPayload: async ({ cfg, to, payload, mediaLocalRoots, accountId, replyToId, threadId }) => {
|
|
44
|
+
const ctx = resolveFeishuSendContext({ cfg, accountId, replyToId, threadId });
|
|
45
|
+
const feishuData = payload.channelData?.feishu;
|
|
46
|
+
const text = payload.text ?? "";
|
|
47
|
+
const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
48
|
+
log.info(
|
|
49
|
+
`sendPayload: target=${to}, textLength=${text.length}, mediaCount=${mediaUrls.length}, hasCard=${Boolean(feishuData?.card)}`
|
|
50
|
+
);
|
|
51
|
+
if (feishuData?.card) {
|
|
52
|
+
if (text.trim()) {
|
|
53
|
+
await sendTextLark({ ...ctx, to, text });
|
|
54
|
+
}
|
|
55
|
+
const cardResult = await sendCardLark({ ...ctx, to, card: feishuData.card });
|
|
56
|
+
const warnings2 = [];
|
|
57
|
+
for (const mediaUrl of mediaUrls) {
|
|
58
|
+
const mediaResult = await sendMediaLark({ ...ctx, to, mediaUrl, mediaLocalRoots });
|
|
59
|
+
if (mediaResult.warning) {
|
|
60
|
+
warnings2.push(mediaResult.warning);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
channel: "feishu",
|
|
65
|
+
messageId: cardResult.messageId,
|
|
66
|
+
chatId: cardResult.chatId,
|
|
67
|
+
...warnings2.length > 0 ? { meta: { warnings: warnings2 } } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (mediaUrls.length === 0) {
|
|
71
|
+
const result = await sendTextLark({ ...ctx, to, text });
|
|
72
|
+
return { channel: "feishu", ...result };
|
|
73
|
+
}
|
|
74
|
+
if (text.trim()) {
|
|
75
|
+
await sendTextLark({ ...ctx, to, text });
|
|
76
|
+
}
|
|
77
|
+
const warnings = [];
|
|
78
|
+
let lastResult;
|
|
79
|
+
for (const mediaUrl of mediaUrls) {
|
|
80
|
+
lastResult = await sendMediaLark({ ...ctx, to, mediaUrl, mediaLocalRoots });
|
|
81
|
+
if (lastResult.warning) {
|
|
82
|
+
warnings.push(lastResult.warning);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
channel: "feishu",
|
|
87
|
+
...lastResult ?? { messageId: "", chatId: "" },
|
|
88
|
+
...warnings.length > 0 ? { meta: { warnings } } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
export {
|
|
93
|
+
feishuOutbound
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=outbound.js.map
|