@openclaw/discord 2026.3.13 → 2026.5.2-beta.1
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/account-inspect-api.ts +6 -0
- package/action-runtime-api.ts +1 -0
- package/api.ts +132 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/configured-state.ts +6 -0
- package/contract-api.ts +21 -0
- package/directory-contract-api.ts +4 -0
- package/doctor-contract-api.ts +1 -0
- package/index.test.ts +13 -0
- package/index.ts +18 -13
- package/openclaw.plugin.json +3326 -1
- package/package.json +68 -2
- package/runtime-api.actions.ts +15 -0
- package/runtime-api.lookup.ts +22 -0
- package/runtime-api.monitor.ts +50 -0
- package/runtime-api.send.ts +79 -0
- package/runtime-api.threads.ts +30 -0
- package/runtime-api.ts +180 -0
- package/runtime-setter-api.ts +3 -0
- package/secret-contract-api.ts +4 -0
- package/security-audit-contract-api.ts +1 -0
- package/security-contract-api.ts +4 -0
- package/session-key-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-inspect.test.ts +126 -0
- package/src/account-inspect.ts +132 -0
- package/src/accounts.test.ts +247 -0
- package/src/accounts.ts +196 -0
- package/src/actions/handle-action.guild-admin.ts +411 -0
- package/src/actions/handle-action.test.ts +306 -0
- package/src/actions/handle-action.ts +372 -0
- package/src/actions/runtime.guild.ts +446 -0
- package/src/actions/runtime.messaging.messages.ts +205 -0
- package/src/actions/runtime.messaging.reactions.ts +67 -0
- package/src/actions/runtime.messaging.runtime.ts +69 -0
- package/src/actions/runtime.messaging.send.ts +248 -0
- package/src/actions/runtime.messaging.shared.ts +97 -0
- package/src/actions/runtime.messaging.ts +37 -0
- package/src/actions/runtime.moderation-shared.ts +48 -0
- package/src/actions/runtime.moderation.authz.test.ts +151 -0
- package/src/actions/runtime.moderation.ts +116 -0
- package/src/actions/runtime.presence.test.ts +160 -0
- package/src/actions/runtime.presence.ts +117 -0
- package/src/actions/runtime.shared.ts +83 -0
- package/src/actions/runtime.test.ts +1087 -0
- package/src/actions/runtime.ts +87 -0
- package/src/api-barrel.test.ts +80 -0
- package/src/api.test.ts +130 -0
- package/src/api.ts +169 -0
- package/src/approval-handler.runtime.test.ts +41 -0
- package/src/approval-handler.runtime.ts +632 -0
- package/src/approval-native.test.ts +330 -0
- package/src/approval-native.ts +219 -0
- package/src/approval-runtime.ts +14 -0
- package/src/approval-shared.ts +53 -0
- package/src/audit-core.ts +141 -0
- package/src/audit.test.ts +145 -0
- package/src/audit.ts +32 -0
- package/src/channel-actions.contract.test.ts +45 -0
- package/src/channel-actions.runtime.ts +1 -0
- package/src/channel-actions.test.ts +275 -0
- package/src/channel-actions.ts +203 -0
- package/src/channel-api.ts +29 -0
- package/src/channel.conversation.ts +159 -0
- package/src/channel.loaders.ts +47 -0
- package/src/channel.runtime.ts +1 -0
- package/src/channel.setup.ts +12 -0
- package/src/channel.test.ts +547 -12
- package/src/channel.ts +597 -430
- package/src/chunk.test.ts +157 -0
- package/src/chunk.ts +321 -0
- package/src/client.proxy.test.ts +176 -0
- package/src/client.test.ts +76 -0
- package/src/client.ts +132 -0
- package/src/component-custom-id.ts +72 -0
- package/src/components-registry.ts +356 -0
- package/src/components.builders.ts +409 -0
- package/src/components.modal.ts +124 -0
- package/src/components.parse.ts +407 -0
- package/src/components.test.ts +312 -0
- package/src/components.ts +54 -0
- package/src/components.types.ts +187 -0
- package/src/config-schema.test.ts +325 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +249 -0
- package/src/conversation-identity.ts +58 -0
- package/src/delivery-retry.ts +56 -0
- package/src/directory-cache.ts +116 -0
- package/src/directory-config.ts +58 -0
- package/src/directory-contract.test.ts +129 -0
- package/src/directory-live.test.ts +126 -0
- package/src/directory-live.ts +135 -0
- package/src/doctor-contract.ts +477 -0
- package/src/doctor-shared.ts +5 -0
- package/src/doctor.test.ts +405 -0
- package/src/doctor.ts +340 -0
- package/src/draft-chunking.test.ts +64 -0
- package/src/draft-chunking.ts +43 -0
- package/src/draft-stream.test.ts +159 -0
- package/src/draft-stream.ts +154 -0
- package/src/error-body.ts +38 -0
- package/src/exec-approvals.test.ts +88 -0
- package/src/exec-approvals.ts +110 -0
- package/src/gateway-logging.test.ts +98 -0
- package/src/gateway-logging.ts +67 -0
- package/src/group-policy.ts +113 -0
- package/src/guilds.ts +29 -0
- package/src/inbound-context.contract.test.ts +11 -0
- package/src/interactive-dispatch.ts +104 -0
- package/src/internal/api.commands.ts +51 -0
- package/src/internal/api.guild.ts +164 -0
- package/src/internal/api.interactions.ts +53 -0
- package/src/internal/api.messages.ts +113 -0
- package/src/internal/api.reactions.ts +38 -0
- package/src/internal/api.test.ts +262 -0
- package/src/internal/api.ts +61 -0
- package/src/internal/api.users.ts +19 -0
- package/src/internal/api.webhooks.ts +13 -0
- package/src/internal/client.test.ts +408 -0
- package/src/internal/client.ts +308 -0
- package/src/internal/command-deploy.ts +237 -0
- package/src/internal/commands.ts +188 -0
- package/src/internal/components.base.ts +65 -0
- package/src/internal/components.message.ts +279 -0
- package/src/internal/components.modal.ts +95 -0
- package/src/internal/components.ts +31 -0
- package/src/internal/discord.ts +11 -0
- package/src/internal/embeds.ts +35 -0
- package/src/internal/entity-cache.ts +98 -0
- package/src/internal/event-queue.ts +162 -0
- package/src/internal/gateway-close-codes.ts +25 -0
- package/src/internal/gateway-dispatch.ts +96 -0
- package/src/internal/gateway-identify-limiter.ts +26 -0
- package/src/internal/gateway-lifecycle.ts +61 -0
- package/src/internal/gateway-rate-limit.ts +104 -0
- package/src/internal/gateway.test.ts +603 -0
- package/src/internal/gateway.ts +476 -0
- package/src/internal/interaction-dispatch.test.ts +148 -0
- package/src/internal/interaction-dispatch.ts +162 -0
- package/src/internal/interaction-options.ts +98 -0
- package/src/internal/interaction-response.ts +53 -0
- package/src/internal/interactions.test.ts +325 -0
- package/src/internal/interactions.ts +378 -0
- package/src/internal/listeners.ts +85 -0
- package/src/internal/live-smoke.live.test.ts +26 -0
- package/src/internal/modal-fields.ts +95 -0
- package/src/internal/payload.ts +69 -0
- package/src/internal/rest-body.ts +115 -0
- package/src/internal/rest-errors.ts +88 -0
- package/src/internal/rest-routes.ts +50 -0
- package/src/internal/rest-scheduler.ts +557 -0
- package/src/internal/rest.test.ts +673 -0
- package/src/internal/rest.ts +322 -0
- package/src/internal/schemas.ts +36 -0
- package/src/internal/structures.test.ts +43 -0
- package/src/internal/structures.ts +280 -0
- package/src/internal/test-builders.test-support.ts +163 -0
- package/src/internal/voice.ts +49 -0
- package/src/media-detection.ts +28 -0
- package/src/mentions.test.ts +111 -0
- package/src/mentions.ts +147 -0
- package/src/monitor/access-groups.ts +55 -0
- package/src/monitor/ack-reactions.ts +70 -0
- package/src/monitor/acp-bind-here.integration.test.ts +211 -0
- package/src/monitor/agent-components-auth.ts +7 -0
- package/src/monitor/agent-components-context.ts +154 -0
- package/src/monitor/agent-components-data.ts +224 -0
- package/src/monitor/agent-components-dm-auth.ts +221 -0
- package/src/monitor/agent-components-guild-auth.ts +322 -0
- package/src/monitor/agent-components-helpers.runtime.ts +5 -0
- package/src/monitor/agent-components-helpers.ts +34 -0
- package/src/monitor/agent-components-reply.ts +10 -0
- package/src/monitor/agent-components.deps.runtime.ts +2 -0
- package/src/monitor/agent-components.dispatch.ts +366 -0
- package/src/monitor/agent-components.handlers.ts +303 -0
- package/src/monitor/agent-components.modal.ts +160 -0
- package/src/monitor/agent-components.plugin-interactive.ts +187 -0
- package/src/monitor/agent-components.runtime.ts +14 -0
- package/src/monitor/agent-components.system-controls.ts +211 -0
- package/src/monitor/agent-components.ts +70 -0
- package/src/monitor/agent-components.types.ts +58 -0
- package/src/monitor/agent-components.wildcard-controls.ts +168 -0
- package/src/monitor/agent-components.wildcard.test.ts +71 -0
- package/src/monitor/allow-list.test.ts +14 -0
- package/src/monitor/allow-list.ts +633 -0
- package/src/monitor/auto-presence.test.ts +156 -0
- package/src/monitor/auto-presence.ts +356 -0
- package/src/monitor/channel-access.test.ts +99 -0
- package/src/monitor/channel-access.ts +102 -0
- package/src/monitor/commands.test.ts +24 -0
- package/src/monitor/commands.ts +9 -0
- package/src/monitor/dm-command-auth.test.ts +197 -0
- package/src/monitor/dm-command-auth.ts +158 -0
- package/src/monitor/dm-command-decision.test.ts +113 -0
- package/src/monitor/dm-command-decision.ts +49 -0
- package/src/monitor/exec-approvals.test.ts +226 -0
- package/src/monitor/exec-approvals.ts +158 -0
- package/src/monitor/format.ts +45 -0
- package/src/monitor/gateway-handle.ts +34 -0
- package/src/monitor/gateway-metadata.test.ts +29 -0
- package/src/monitor/gateway-metadata.ts +298 -0
- package/src/monitor/gateway-plugin.test.ts +297 -0
- package/src/monitor/gateway-plugin.ts +294 -0
- package/src/monitor/gateway-registry.ts +37 -0
- package/src/monitor/gateway-supervisor.test.ts +150 -0
- package/src/monitor/gateway-supervisor.ts +206 -0
- package/src/monitor/inbound-context.test-helpers.ts +37 -0
- package/src/monitor/inbound-context.test.ts +106 -0
- package/src/monitor/inbound-context.ts +103 -0
- package/src/monitor/inbound-dedupe.ts +79 -0
- package/src/monitor/inbound-job.test.ts +203 -0
- package/src/monitor/inbound-job.ts +118 -0
- package/src/monitor/listeners.queue.ts +91 -0
- package/src/monitor/listeners.reactions.ts +610 -0
- package/src/monitor/listeners.test.ts +200 -0
- package/src/monitor/listeners.ts +150 -0
- package/src/monitor/message-channel-info.ts +96 -0
- package/src/monitor/message-forwarded.ts +107 -0
- package/src/monitor/message-handler.batch-gate.test.ts +22 -0
- package/src/monitor/message-handler.batch-gate.ts +19 -0
- package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
- package/src/monitor/message-handler.context.ts +406 -0
- package/src/monitor/message-handler.dm-preflight.ts +123 -0
- package/src/monitor/message-handler.draft-preview.ts +246 -0
- package/src/monitor/message-handler.hydration.test.ts +80 -0
- package/src/monitor/message-handler.hydration.ts +198 -0
- package/src/monitor/message-handler.inbound-context.test.ts +59 -0
- package/src/monitor/message-handler.module-test-helpers.ts +31 -0
- package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
- package/src/monitor/message-handler.preflight-channel-context.test.ts +18 -0
- package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
- package/src/monitor/message-handler.preflight-context.ts +54 -0
- package/src/monitor/message-handler.preflight-helpers.ts +164 -0
- package/src/monitor/message-handler.preflight-history.ts +23 -0
- package/src/monitor/message-handler.preflight-logging.ts +36 -0
- package/src/monitor/message-handler.preflight-pluralkit.ts +26 -0
- package/src/monitor/message-handler.preflight-runtime.ts +28 -0
- package/src/monitor/message-handler.preflight-thread.ts +49 -0
- package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
- package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
- package/src/monitor/message-handler.preflight.test.ts +1623 -0
- package/src/monitor/message-handler.preflight.ts +679 -0
- package/src/monitor/message-handler.preflight.types.ts +110 -0
- package/src/monitor/message-handler.process.test.ts +1369 -0
- package/src/monitor/message-handler.process.ts +686 -0
- package/src/monitor/message-handler.queue.test.ts +496 -0
- package/src/monitor/message-handler.routing-preflight.ts +112 -0
- package/src/monitor/message-handler.test-harness.ts +99 -0
- package/src/monitor/message-handler.test-helpers.ts +75 -0
- package/src/monitor/message-handler.ts +274 -0
- package/src/monitor/message-media.ts +509 -0
- package/src/monitor/message-run-queue.ts +101 -0
- package/src/monitor/message-text.ts +171 -0
- package/src/monitor/message-utils.test.ts +1157 -0
- package/src/monitor/message-utils.ts +32 -0
- package/src/monitor/model-picker-preferences.test.ts +67 -0
- package/src/monitor/model-picker-preferences.ts +184 -0
- package/src/monitor/model-picker.state.ts +364 -0
- package/src/monitor/model-picker.test-utils.ts +26 -0
- package/src/monitor/model-picker.test.ts +794 -0
- package/src/monitor/model-picker.ts +38 -0
- package/src/monitor/model-picker.view.ts +695 -0
- package/src/monitor/monitor.agent-components.test.ts +375 -0
- package/src/monitor/monitor.test.ts +849 -0
- package/src/monitor/monitor.threading-utils.test.ts +598 -0
- package/src/monitor/native-command-agent-reply.ts +125 -0
- package/src/monitor/native-command-arg-ui.ts +233 -0
- package/src/monitor/native-command-auth.ts +308 -0
- package/src/monitor/native-command-bypass.ts +13 -0
- package/src/monitor/native-command-context.test.ts +98 -0
- package/src/monitor/native-command-context.ts +103 -0
- package/src/monitor/native-command-dispatch.ts +35 -0
- package/src/monitor/native-command-model-picker-apply.ts +177 -0
- package/src/monitor/native-command-model-picker-interaction.ts +461 -0
- package/src/monitor/native-command-model-picker-ui.ts +368 -0
- package/src/monitor/native-command-reply.test.ts +68 -0
- package/src/monitor/native-command-reply.ts +185 -0
- package/src/monitor/native-command-route.ts +91 -0
- package/src/monitor/native-command-status.ts +76 -0
- package/src/monitor/native-command-ui.ts +26 -0
- package/src/monitor/native-command-ui.types.ts +20 -0
- package/src/monitor/native-command.args.ts +45 -0
- package/src/monitor/native-command.command-arg.test.ts +99 -0
- package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
- package/src/monitor/native-command.model-picker.test.ts +767 -0
- package/src/monitor/native-command.options.test.ts +369 -0
- package/src/monitor/native-command.options.ts +153 -0
- package/src/monitor/native-command.plugin-dispatch.test.ts +961 -0
- package/src/monitor/native-command.runtime.ts +50 -0
- package/src/monitor/native-command.status-direct.test.ts +272 -0
- package/src/monitor/native-command.test-helpers.ts +64 -0
- package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
- package/src/monitor/native-command.ts +700 -0
- package/src/monitor/native-command.types.ts +9 -0
- package/src/monitor/native-interaction-channel-context.ts +50 -0
- package/src/monitor/preflight-audio.runtime.ts +9 -0
- package/src/monitor/preflight-audio.test.ts +157 -0
- package/src/monitor/preflight-audio.ts +130 -0
- package/src/monitor/presence-cache.ts +61 -0
- package/src/monitor/presence.test.ts +44 -0
- package/src/monitor/presence.ts +50 -0
- package/src/monitor/provider-session.runtime.ts +12 -0
- package/src/monitor/provider.acp.ts +89 -0
- package/src/monitor/provider.allowlist.test.ts +149 -0
- package/src/monitor/provider.allowlist.ts +394 -0
- package/src/monitor/provider.cleanup.ts +41 -0
- package/src/monitor/provider.commands.ts +129 -0
- package/src/monitor/provider.config-log.ts +45 -0
- package/src/monitor/provider.deploy-errors.ts +362 -0
- package/src/monitor/provider.deploy.ts +221 -0
- package/src/monitor/provider.interactions.ts +160 -0
- package/src/monitor/provider.lifecycle.test.ts +713 -0
- package/src/monitor/provider.lifecycle.ts +552 -0
- package/src/monitor/provider.proxy.test.ts +745 -0
- package/src/monitor/provider.rest-proxy.test.ts +121 -0
- package/src/monitor/provider.runtime.ts +1 -0
- package/src/monitor/provider.skill-dedupe.test.ts +42 -0
- package/src/monitor/provider.startup-log.ts +32 -0
- package/src/monitor/provider.startup.test.ts +426 -0
- package/src/monitor/provider.startup.ts +323 -0
- package/src/monitor/provider.test.ts +1111 -0
- package/src/monitor/provider.ts +713 -0
- package/src/monitor/reply-context.ts +64 -0
- package/src/monitor/reply-delivery.test.ts +244 -0
- package/src/monitor/reply-delivery.ts +203 -0
- package/src/monitor/rest-fetch.ts +43 -0
- package/src/monitor/route-resolution.test.ts +204 -0
- package/src/monitor/route-resolution.ts +140 -0
- package/src/monitor/sender-identity.ts +81 -0
- package/src/monitor/startup-status.test.ts +30 -0
- package/src/monitor/startup-status.ts +10 -0
- package/src/monitor/status.ts +22 -0
- package/src/monitor/system-events.ts +55 -0
- package/src/monitor/thread-bindings.config.ts +35 -0
- package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
- package/src/monitor/thread-bindings.discord-api.ts +310 -0
- package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
- package/src/monitor/thread-bindings.lifecycle.ts +354 -0
- package/src/monitor/thread-bindings.manager.ts +553 -0
- package/src/monitor/thread-bindings.messages.ts +6 -0
- package/src/monitor/thread-bindings.persona.test.ts +34 -0
- package/src/monitor/thread-bindings.persona.ts +25 -0
- package/src/monitor/thread-bindings.session-adapter.ts +229 -0
- package/src/monitor/thread-bindings.session-shared.ts +59 -0
- package/src/monitor/thread-bindings.session-updates.ts +35 -0
- package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
- package/src/monitor/thread-bindings.state.ts +540 -0
- package/src/monitor/thread-bindings.ts +48 -0
- package/src/monitor/thread-bindings.types.ts +83 -0
- package/src/monitor/thread-channel-context.ts +112 -0
- package/src/monitor/thread-session-close.test.ts +180 -0
- package/src/monitor/thread-session-close.ts +63 -0
- package/src/monitor/thread-title.generate.test.ts +197 -0
- package/src/monitor/thread-title.test.ts +31 -0
- package/src/monitor/thread-title.ts +181 -0
- package/src/monitor/threading.auto-thread.test.ts +327 -0
- package/src/monitor/threading.auto-thread.ts +287 -0
- package/src/monitor/threading.cache.ts +45 -0
- package/src/monitor/threading.parent-info.test.ts +156 -0
- package/src/monitor/threading.starter.test.ts +260 -0
- package/src/monitor/threading.starter.ts +287 -0
- package/src/monitor/threading.ts +20 -0
- package/src/monitor/threading.types.ts +102 -0
- package/src/monitor/timeouts.ts +84 -0
- package/src/monitor/typing.test.ts +42 -0
- package/src/monitor/typing.ts +17 -0
- package/src/monitor.gateway.test.ts +187 -0
- package/src/monitor.gateway.ts +75 -0
- package/src/monitor.test.ts +1397 -0
- package/src/monitor.ts +28 -0
- package/src/normalize.test.ts +56 -0
- package/src/normalize.ts +86 -0
- package/src/outbound-adapter.interactive-order.test.ts +64 -0
- package/src/outbound-adapter.test-harness.ts +207 -0
- package/src/outbound-adapter.test.ts +696 -0
- package/src/outbound-adapter.ts +291 -0
- package/src/outbound-approval.ts +29 -0
- package/src/outbound-components.ts +81 -0
- package/src/outbound-payload.contract.test.ts +38 -0
- package/src/outbound-payload.ts +134 -0
- package/src/outbound-send-context.ts +92 -0
- package/src/outbound-session-route.test.ts +34 -0
- package/src/outbound-session-route.ts +72 -0
- package/src/pluralkit.test.ts +67 -0
- package/src/pluralkit.ts +58 -0
- package/src/preview-streaming.ts +32 -0
- package/src/probe.intents.test.ts +94 -0
- package/src/probe.parse-token.test.ts +43 -0
- package/src/probe.runtime.ts +1 -0
- package/src/probe.ts +237 -0
- package/src/proxy-fetch.ts +92 -0
- package/src/proxy-request-client.test.ts +78 -0
- package/src/proxy-request-client.ts +21 -0
- package/src/recipient-resolution.ts +39 -0
- package/src/resolve-allowlist-common.test.ts +36 -0
- package/src/resolve-allowlist-common.ts +39 -0
- package/src/resolve-channels.test.ts +340 -0
- package/src/resolve-channels.ts +369 -0
- package/src/resolve-users.test.ts +222 -0
- package/src/resolve-users.ts +184 -0
- package/src/retry.test.ts +83 -0
- package/src/retry.ts +98 -0
- package/src/runtime-api.ts +64 -0
- package/src/runtime.ts +22 -5
- package/src/secret-config-contract.ts +140 -0
- package/src/security-audit.runtime.ts +1 -0
- package/src/security-audit.test.ts +246 -0
- package/src/security-audit.ts +208 -0
- package/src/security-contract.ts +47 -0
- package/src/security-doctor.test.ts +25 -0
- package/src/security-doctor.ts +20 -0
- package/src/security.ts +60 -0
- package/src/send-target-parsing.ts +14 -0
- package/src/send.channels.ts +139 -0
- package/src/send.components.test.ts +275 -0
- package/src/send.components.ts +381 -0
- package/src/send.creates-thread.test.ts +643 -0
- package/src/send.emojis-stickers.ts +57 -0
- package/src/send.guild.ts +170 -0
- package/src/send.message-request.ts +97 -0
- package/src/send.messages.test.ts +53 -0
- package/src/send.messages.ts +225 -0
- package/src/send.outbound.ts +413 -0
- package/src/send.permissions.authz.test.ts +188 -0
- package/src/send.permissions.ts +283 -0
- package/src/send.reactions.ts +155 -0
- package/src/send.sends-basic-channel-messages.test.ts +941 -0
- package/src/send.shared.ts +447 -0
- package/src/send.test-harness.ts +56 -0
- package/src/send.ts +82 -0
- package/src/send.types.ts +188 -0
- package/src/send.typing.test.ts +41 -0
- package/src/send.typing.ts +9 -0
- package/src/send.voice.ts +134 -0
- package/src/send.webhook-activity.test.ts +105 -0
- package/src/send.webhook.proxy.test.ts +191 -0
- package/src/send.webhook.ts +133 -0
- package/src/session-contract.ts +3 -0
- package/src/session-key-normalization.test.ts +44 -0
- package/src/session-key-normalization.ts +47 -0
- package/src/setup-account-state.test.ts +91 -0
- package/src/setup-account-state.ts +144 -0
- package/src/setup-adapter.ts +12 -0
- package/src/setup-core.ts +212 -0
- package/src/setup-runtime-helpers.ts +10 -0
- package/src/setup-surface.test.ts +137 -0
- package/src/setup-surface.ts +129 -0
- package/src/shared-interactive.test.ts +153 -0
- package/src/shared-interactive.ts +124 -0
- package/src/shared.test.ts +165 -0
- package/src/shared.ts +190 -0
- package/src/status-issues.test.ts +70 -0
- package/src/status-issues.ts +169 -0
- package/src/subagent-hooks.test.ts +130 -81
- package/src/subagent-hooks.ts +184 -122
- package/src/target-parsing.ts +53 -0
- package/src/target-resolver.ts +129 -0
- package/src/targets.test.ts +367 -0
- package/src/targets.ts +12 -0
- package/src/test-http-helpers.ts +10 -0
- package/src/test-support/component-runtime.ts +190 -0
- package/src/test-support/config.ts +7 -0
- package/src/test-support/configured-binding-runtime.ts +29 -0
- package/src/test-support/partial-channel.ts +26 -0
- package/src/test-support/provider.test-support.ts +545 -0
- package/src/token.test.ts +107 -0
- package/src/token.ts +60 -0
- package/src/ui-colors.ts +27 -0
- package/src/ui.ts +20 -0
- package/src/voice/access.test.ts +217 -0
- package/src/voice/access.ts +124 -0
- package/src/voice/audio.ts +173 -0
- package/src/voice/capture-state.test.ts +48 -0
- package/src/voice/capture-state.ts +120 -0
- package/src/voice/command.test.ts +164 -0
- package/src/voice/command.ts +283 -0
- package/src/voice/config.ts +8 -0
- package/src/voice/manager.e2e.test.ts +928 -0
- package/src/voice/manager.ready-listener.test.ts +37 -0
- package/src/voice/manager.runtime.ts +11 -0
- package/src/voice/manager.ts +691 -0
- package/src/voice/prompt.test.ts +16 -0
- package/src/voice/prompt.ts +17 -0
- package/src/voice/receive-recovery.test.ts +79 -0
- package/src/voice/receive-recovery.ts +159 -0
- package/src/voice/sanitize.test.ts +34 -0
- package/src/voice/sanitize.ts +32 -0
- package/src/voice/sdk-runtime.ts +14 -0
- package/src/voice/segment.ts +156 -0
- package/src/voice/session.ts +50 -0
- package/src/voice/speaker-context.ts +127 -0
- package/src/voice/tts.ts +125 -0
- package/src/voice-message.test.ts +234 -0
- package/src/voice-message.ts +444 -0
- package/subagent-hooks-api.ts +27 -0
- package/test-api.ts +4 -0
- package/thread-binding-api.ts +1 -0
- package/timeouts.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
let DiscordMessageListener: typeof import("./listeners.js").DiscordMessageListener;
|
|
4
|
+
let DiscordInteractionListener: typeof import("./listeners.js").DiscordInteractionListener;
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
({ DiscordMessageListener, DiscordInteractionListener } = await import("./listeners.js"));
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function createLogger() {
|
|
11
|
+
return {
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
warn: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fakeEvent(channelId: string) {
|
|
18
|
+
return { channel_id: channelId } as never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createDeferred() {
|
|
22
|
+
let resolve: (() => void) | undefined;
|
|
23
|
+
const promise = new Promise<void>((r) => {
|
|
24
|
+
resolve = r;
|
|
25
|
+
});
|
|
26
|
+
return { promise, resolve };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function flushAsyncWork() {
|
|
30
|
+
await Promise.resolve();
|
|
31
|
+
await Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("DiscordMessageListener", () => {
|
|
35
|
+
it("returns immediately without awaiting handler completion", async () => {
|
|
36
|
+
let resolveHandler: (() => void) | undefined;
|
|
37
|
+
const handlerDone = new Promise<void>((resolve) => {
|
|
38
|
+
resolveHandler = resolve;
|
|
39
|
+
});
|
|
40
|
+
const handler = vi.fn(async () => {
|
|
41
|
+
await handlerDone;
|
|
42
|
+
});
|
|
43
|
+
const logger = createLogger();
|
|
44
|
+
const listener = new DiscordMessageListener(handler as never, logger as never);
|
|
45
|
+
|
|
46
|
+
await expect(listener.handle(fakeEvent("ch-1"), {} as never)).resolves.toBeUndefined();
|
|
47
|
+
// Handler was dispatched but may not have been called yet (fire-and-forget).
|
|
48
|
+
// Wait for the microtask to flush so the handler starts.
|
|
49
|
+
await flushAsyncWork();
|
|
50
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
52
|
+
|
|
53
|
+
resolveHandler?.();
|
|
54
|
+
await handlerDone;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("runs handlers for the same channel concurrently (no per-channel serialization)", async () => {
|
|
58
|
+
const order: string[] = [];
|
|
59
|
+
const deferredA = createDeferred();
|
|
60
|
+
const deferredB = createDeferred();
|
|
61
|
+
let callCount = 0;
|
|
62
|
+
const handler = vi.fn(async () => {
|
|
63
|
+
callCount += 1;
|
|
64
|
+
const id = callCount;
|
|
65
|
+
order.push(`start:${id}`);
|
|
66
|
+
if (id === 1) {
|
|
67
|
+
await deferredA.promise;
|
|
68
|
+
} else {
|
|
69
|
+
await deferredB.promise;
|
|
70
|
+
}
|
|
71
|
+
order.push(`end:${id}`);
|
|
72
|
+
});
|
|
73
|
+
const listener = new DiscordMessageListener(handler as never, createLogger() as never);
|
|
74
|
+
|
|
75
|
+
// Both messages target the same channel — previously serialized, now concurrent.
|
|
76
|
+
await listener.handle(fakeEvent("ch-1"), {} as never);
|
|
77
|
+
await listener.handle(fakeEvent("ch-1"), {} as never);
|
|
78
|
+
|
|
79
|
+
await flushAsyncWork();
|
|
80
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
81
|
+
// Both handlers started without waiting for the first to finish.
|
|
82
|
+
expect(order).toContain("start:1");
|
|
83
|
+
expect(order).toContain("start:2");
|
|
84
|
+
|
|
85
|
+
deferredB.resolve?.();
|
|
86
|
+
await flushAsyncWork();
|
|
87
|
+
expect(order).toContain("end:2");
|
|
88
|
+
// First handler is still running — no serialization.
|
|
89
|
+
expect(order).not.toContain("end:1");
|
|
90
|
+
|
|
91
|
+
deferredA.resolve?.();
|
|
92
|
+
await flushAsyncWork();
|
|
93
|
+
expect(order).toContain("end:1");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("runs handlers for different channels in parallel", async () => {
|
|
97
|
+
const deferredA = createDeferred();
|
|
98
|
+
const deferredB = createDeferred();
|
|
99
|
+
const order: string[] = [];
|
|
100
|
+
const handler = vi.fn(async (data: { channel_id: string }) => {
|
|
101
|
+
order.push(`start:${data.channel_id}`);
|
|
102
|
+
if (data.channel_id === "ch-a") {
|
|
103
|
+
await deferredA.promise;
|
|
104
|
+
} else {
|
|
105
|
+
await deferredB.promise;
|
|
106
|
+
}
|
|
107
|
+
order.push(`end:${data.channel_id}`);
|
|
108
|
+
});
|
|
109
|
+
const listener = new DiscordMessageListener(handler as never, createLogger() as never);
|
|
110
|
+
|
|
111
|
+
await listener.handle(fakeEvent("ch-a"), {} as never);
|
|
112
|
+
await listener.handle(fakeEvent("ch-b"), {} as never);
|
|
113
|
+
|
|
114
|
+
await flushAsyncWork();
|
|
115
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
116
|
+
expect(order).toContain("start:ch-a");
|
|
117
|
+
expect(order).toContain("start:ch-b");
|
|
118
|
+
|
|
119
|
+
deferredB.resolve?.();
|
|
120
|
+
await flushAsyncWork();
|
|
121
|
+
expect(order).toContain("end:ch-b");
|
|
122
|
+
expect(order).not.toContain("end:ch-a");
|
|
123
|
+
|
|
124
|
+
deferredA.resolve?.();
|
|
125
|
+
await flushAsyncWork();
|
|
126
|
+
expect(order).toContain("end:ch-a");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("logs async handler failures", async () => {
|
|
130
|
+
const handler = vi.fn(async () => {
|
|
131
|
+
throw new Error("boom");
|
|
132
|
+
});
|
|
133
|
+
const logger = createLogger();
|
|
134
|
+
const listener = new DiscordMessageListener(handler as never, logger as never);
|
|
135
|
+
|
|
136
|
+
await expect(listener.handle(fakeEvent("ch-1"), {} as never)).resolves.toBeUndefined();
|
|
137
|
+
await flushAsyncWork();
|
|
138
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining("discord handler failed: Error: boom"),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("calls onEvent callback for each message", async () => {
|
|
144
|
+
const handler = vi.fn(async () => {});
|
|
145
|
+
const onEvent = vi.fn();
|
|
146
|
+
const listener = new DiscordMessageListener(handler as never, undefined, onEvent);
|
|
147
|
+
|
|
148
|
+
await listener.handle(fakeEvent("ch-1"), {} as never);
|
|
149
|
+
await listener.handle(fakeEvent("ch-2"), {} as never);
|
|
150
|
+
|
|
151
|
+
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("DiscordInteractionListener", () => {
|
|
156
|
+
it("returns immediately without awaiting Discord interaction handling", async () => {
|
|
157
|
+
const handlerDone = createDeferred();
|
|
158
|
+
const handleInteraction = vi.fn(async () => {
|
|
159
|
+
await handlerDone.promise;
|
|
160
|
+
});
|
|
161
|
+
const logger = createLogger();
|
|
162
|
+
const listener = new DiscordInteractionListener(logger as never);
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
listener.handle({ id: "interaction-1" } as never, { handleInteraction } as never),
|
|
166
|
+
).resolves.toBeUndefined();
|
|
167
|
+
await flushAsyncWork();
|
|
168
|
+
expect(handleInteraction).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
170
|
+
|
|
171
|
+
handlerDone.resolve?.();
|
|
172
|
+
await flushAsyncWork();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("logs async interaction failures", async () => {
|
|
176
|
+
const handleInteraction = vi.fn(async () => {
|
|
177
|
+
throw new Error("interaction boom");
|
|
178
|
+
});
|
|
179
|
+
const logger = createLogger();
|
|
180
|
+
const listener = new DiscordInteractionListener(logger as never);
|
|
181
|
+
|
|
182
|
+
await listener.handle({ id: "interaction-1" } as never, { handleInteraction } as never);
|
|
183
|
+
await flushAsyncWork();
|
|
184
|
+
|
|
185
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
186
|
+
expect.stringContaining("discord interaction handler failed: Error: interaction boom"),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("calls onEvent callback for each interaction", async () => {
|
|
191
|
+
const handleInteraction = vi.fn(async () => {});
|
|
192
|
+
const onEvent = vi.fn();
|
|
193
|
+
const listener = new DiscordInteractionListener(undefined, onEvent);
|
|
194
|
+
|
|
195
|
+
await listener.handle({ id: "interaction-1" } as never, { handleInteraction } as never);
|
|
196
|
+
await listener.handle({ id: "interaction-2" } as never, { handleInteraction } as never);
|
|
197
|
+
|
|
198
|
+
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
2
|
+
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
|
3
|
+
import {
|
|
4
|
+
type Client,
|
|
5
|
+
InteractionCreateListener,
|
|
6
|
+
MessageCreateListener,
|
|
7
|
+
PresenceUpdateListener,
|
|
8
|
+
ThreadUpdateListener,
|
|
9
|
+
} from "../internal/discord.js";
|
|
10
|
+
import { discordEventQueueLog, runDiscordListenerWithSlowLog } from "./listeners.queue.js";
|
|
11
|
+
export { DiscordReactionListener, DiscordReactionRemoveListener } from "./listeners.reactions.js";
|
|
12
|
+
import { setPresence } from "./presence-cache.js";
|
|
13
|
+
import { isThreadArchived } from "./thread-bindings.discord-api.js";
|
|
14
|
+
import { closeDiscordThreadSessions } from "./thread-session-close.js";
|
|
15
|
+
|
|
16
|
+
type Logger = ReturnType<typeof import("openclaw/plugin-sdk/runtime-env").createSubsystemLogger>;
|
|
17
|
+
|
|
18
|
+
export type DiscordMessageEvent = Parameters<MessageCreateListener["handle"]>[0];
|
|
19
|
+
export type DiscordInteractionEvent = Parameters<InteractionCreateListener["handle"]>[0];
|
|
20
|
+
|
|
21
|
+
export type DiscordMessageHandler = (
|
|
22
|
+
data: DiscordMessageEvent,
|
|
23
|
+
client: Client,
|
|
24
|
+
options?: { abortSignal?: AbortSignal },
|
|
25
|
+
) => Promise<void>;
|
|
26
|
+
|
|
27
|
+
export function registerDiscordListener(listeners: Array<object>, listener: object) {
|
|
28
|
+
if (listeners.some((existing) => existing.constructor === listener.constructor)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
listeners.push(listener);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class DiscordMessageListener extends MessageCreateListener {
|
|
36
|
+
constructor(
|
|
37
|
+
private handler: DiscordMessageHandler,
|
|
38
|
+
private logger?: Logger,
|
|
39
|
+
private onEvent?: () => void,
|
|
40
|
+
) {
|
|
41
|
+
super();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async handle(data: DiscordMessageEvent, client: Client) {
|
|
45
|
+
this.onEvent?.();
|
|
46
|
+
// Fire-and-forget: hand off to the handler without blocking gateway dispatch.
|
|
47
|
+
// Per-session ordering is owned by the message run queue.
|
|
48
|
+
void Promise.resolve()
|
|
49
|
+
.then(() => this.handler(data, client))
|
|
50
|
+
.catch((err) => {
|
|
51
|
+
const logger = this.logger ?? discordEventQueueLog;
|
|
52
|
+
logger.error(danger(`discord handler failed: ${String(err)}`));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class DiscordInteractionListener extends InteractionCreateListener {
|
|
58
|
+
constructor(
|
|
59
|
+
private logger?: Logger,
|
|
60
|
+
private onEvent?: () => void,
|
|
61
|
+
) {
|
|
62
|
+
super();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async handle(data: DiscordInteractionEvent, client: Client) {
|
|
66
|
+
this.onEvent?.();
|
|
67
|
+
// Hand off immediately so slash/component handling can wait on session locks
|
|
68
|
+
// or compaction without blocking later gateway events.
|
|
69
|
+
void Promise.resolve()
|
|
70
|
+
.then(() => client.handleInteraction(data as Parameters<Client["handleInteraction"]>[0], {}))
|
|
71
|
+
.catch((err) => {
|
|
72
|
+
const logger = this.logger ?? discordEventQueueLog;
|
|
73
|
+
logger.error(danger(`discord interaction handler failed: ${String(err)}`));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type PresenceUpdateEvent = Parameters<PresenceUpdateListener["handle"]>[0];
|
|
79
|
+
|
|
80
|
+
export class DiscordPresenceListener extends PresenceUpdateListener {
|
|
81
|
+
private logger?: Logger;
|
|
82
|
+
private accountId?: string;
|
|
83
|
+
|
|
84
|
+
constructor(params: { logger?: Logger; accountId?: string }) {
|
|
85
|
+
super();
|
|
86
|
+
this.logger = params.logger;
|
|
87
|
+
this.accountId = params.accountId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async handle(data: PresenceUpdateEvent) {
|
|
91
|
+
try {
|
|
92
|
+
const userId =
|
|
93
|
+
"user" in data && data.user && typeof data.user === "object" && "id" in data.user
|
|
94
|
+
? data.user.id
|
|
95
|
+
: undefined;
|
|
96
|
+
if (!userId) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setPresence(this.accountId, userId, data);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const logger = this.logger ?? discordEventQueueLog;
|
|
102
|
+
logger.error(danger(`discord presence handler failed: ${String(err)}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ThreadUpdateEvent = Parameters<ThreadUpdateListener["handle"]>[0];
|
|
108
|
+
|
|
109
|
+
export class DiscordThreadUpdateListener extends ThreadUpdateListener {
|
|
110
|
+
constructor(
|
|
111
|
+
private cfg: OpenClawConfig,
|
|
112
|
+
private accountId: string,
|
|
113
|
+
private logger?: Logger,
|
|
114
|
+
) {
|
|
115
|
+
super();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async handle(data: ThreadUpdateEvent) {
|
|
119
|
+
await runDiscordListenerWithSlowLog({
|
|
120
|
+
logger: this.logger,
|
|
121
|
+
listener: this.constructor.name,
|
|
122
|
+
event: this.type,
|
|
123
|
+
run: async () => {
|
|
124
|
+
// Discord only fires THREAD_UPDATE when a field actually changes, so
|
|
125
|
+
// `thread_metadata.archived === true` in this payload means the thread
|
|
126
|
+
// just transitioned to the archived state.
|
|
127
|
+
if (!isThreadArchived(data)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const threadId = "id" in data && typeof data.id === "string" ? data.id : undefined;
|
|
131
|
+
if (!threadId) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const logger = this.logger ?? discordEventQueueLog;
|
|
135
|
+
const count = await closeDiscordThreadSessions({
|
|
136
|
+
cfg: this.cfg,
|
|
137
|
+
accountId: this.accountId,
|
|
138
|
+
threadId,
|
|
139
|
+
});
|
|
140
|
+
if (count > 0) {
|
|
141
|
+
logger.info("Discord thread archived — reset sessions", { threadId, count });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
onError: (err) => {
|
|
145
|
+
const logger = this.logger ?? discordEventQueueLog;
|
|
146
|
+
logger.error(danger(`discord thread-update handler failed: ${String(err)}`));
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
2
|
+
import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/text-runtime";
|
|
3
|
+
import type { ChannelType, Message } from "../internal/discord.js";
|
|
4
|
+
import { resolveDiscordChannelInfoSafe } from "./channel-access.js";
|
|
5
|
+
|
|
6
|
+
export type DiscordChannelInfo = {
|
|
7
|
+
type: ChannelType;
|
|
8
|
+
name?: string;
|
|
9
|
+
topic?: string;
|
|
10
|
+
parentId?: string;
|
|
11
|
+
ownerId?: string;
|
|
12
|
+
};
|
|
13
|
+
export type DiscordChannelInfoClient = {
|
|
14
|
+
fetchChannel(channelId: string): Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type DiscordMessageWithChannelId = Message & {
|
|
18
|
+
channel_id?: unknown;
|
|
19
|
+
rawData?: { channel_id?: unknown };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DISCORD_CHANNEL_INFO_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
23
|
+
const DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS = 30 * 1000;
|
|
24
|
+
const DISCORD_CHANNEL_INFO_CACHE = new Map<
|
|
25
|
+
string,
|
|
26
|
+
{ value: DiscordChannelInfo | null; expiresAt: number }
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
export function __resetDiscordChannelInfoCacheForTest() {
|
|
30
|
+
DISCORD_CHANNEL_INFO_CACHE.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeDiscordChannelId(value: unknown): string {
|
|
34
|
+
return normalizeOptionalStringifiedId(value) ?? "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveDiscordMessageChannelId(params: {
|
|
38
|
+
message: Message;
|
|
39
|
+
eventChannelId?: string | number | null;
|
|
40
|
+
}): string {
|
|
41
|
+
const message = params.message as DiscordMessageWithChannelId;
|
|
42
|
+
return (
|
|
43
|
+
normalizeDiscordChannelId(message.channelId) ||
|
|
44
|
+
normalizeDiscordChannelId(message.channel_id) ||
|
|
45
|
+
normalizeDiscordChannelId(message.rawData?.channel_id) ||
|
|
46
|
+
normalizeDiscordChannelId(params.eventChannelId)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function resolveDiscordChannelInfo(
|
|
51
|
+
client: DiscordChannelInfoClient,
|
|
52
|
+
channelId: string,
|
|
53
|
+
): Promise<DiscordChannelInfo | null> {
|
|
54
|
+
const cached = DISCORD_CHANNEL_INFO_CACHE.get(channelId);
|
|
55
|
+
if (cached) {
|
|
56
|
+
if (cached.expiresAt > Date.now()) {
|
|
57
|
+
return cached.value;
|
|
58
|
+
}
|
|
59
|
+
DISCORD_CHANNEL_INFO_CACHE.delete(channelId);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const channel = await client.fetchChannel(channelId);
|
|
63
|
+
if (!channel) {
|
|
64
|
+
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
65
|
+
value: null,
|
|
66
|
+
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
|
67
|
+
});
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const channelInfo = resolveDiscordChannelInfoSafe(channel);
|
|
71
|
+
const rawChannel = channel as { type?: ChannelType };
|
|
72
|
+
const type = (channelInfo.type as ChannelType | undefined) ?? rawChannel.type;
|
|
73
|
+
if (type === undefined) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const payload: DiscordChannelInfo = {
|
|
77
|
+
type,
|
|
78
|
+
name: channelInfo.name,
|
|
79
|
+
topic: channelInfo.topic,
|
|
80
|
+
parentId: channelInfo.parentId,
|
|
81
|
+
ownerId: channelInfo.ownerId,
|
|
82
|
+
};
|
|
83
|
+
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
84
|
+
value: payload,
|
|
85
|
+
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_CACHE_TTL_MS,
|
|
86
|
+
});
|
|
87
|
+
return payload;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
|
90
|
+
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
91
|
+
value: null,
|
|
92
|
+
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
|
93
|
+
});
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { APIAttachment, APIStickerItem } from "discord-api-types/v10";
|
|
2
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
3
|
+
import type { Message } from "../internal/discord.js";
|
|
4
|
+
|
|
5
|
+
export type DiscordSnapshotAuthor = {
|
|
6
|
+
id?: string | null;
|
|
7
|
+
username?: string | null;
|
|
8
|
+
discriminator?: string | null;
|
|
9
|
+
global_name?: string | null;
|
|
10
|
+
name?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DiscordSnapshotMessage = {
|
|
14
|
+
content?: string | null;
|
|
15
|
+
components?: unknown;
|
|
16
|
+
embeds?: Array<{ description?: string | null; title?: string | null }> | null;
|
|
17
|
+
attachments?: APIAttachment[] | null;
|
|
18
|
+
stickers?: APIStickerItem[] | null;
|
|
19
|
+
sticker_items?: APIStickerItem[] | null;
|
|
20
|
+
author?: DiscordSnapshotAuthor | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DiscordMessageSnapshot = {
|
|
24
|
+
message?: DiscordSnapshotMessage | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const FORWARD_MESSAGE_REFERENCE_TYPE = 1;
|
|
28
|
+
|
|
29
|
+
export function normalizeDiscordStickerItems(value: unknown): APIStickerItem[] {
|
|
30
|
+
if (!Array.isArray(value)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return value.filter(
|
|
34
|
+
(entry): entry is APIStickerItem =>
|
|
35
|
+
Boolean(entry) &&
|
|
36
|
+
typeof entry === "object" &&
|
|
37
|
+
typeof (entry as { id?: unknown }).id === "string" &&
|
|
38
|
+
typeof (entry as { name?: unknown }).name === "string",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveDiscordMessageStickers(message: Message): APIStickerItem[] {
|
|
43
|
+
const stickers = (message as { stickers?: unknown }).stickers;
|
|
44
|
+
const normalized = normalizeDiscordStickerItems(stickers);
|
|
45
|
+
if (normalized.length > 0) {
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
const rawData = (message as { rawData?: { sticker_items?: unknown; stickers?: unknown } })
|
|
49
|
+
.rawData;
|
|
50
|
+
return normalizeDiscordStickerItems(rawData?.sticker_items ?? rawData?.stickers);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveDiscordSnapshotStickers(snapshot: DiscordSnapshotMessage): APIStickerItem[] {
|
|
54
|
+
return normalizeDiscordStickerItems(snapshot.stickers ?? snapshot.sticker_items);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasDiscordMessageStickers(message: Message): boolean {
|
|
58
|
+
return resolveDiscordMessageStickers(message).length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapshot[] {
|
|
62
|
+
const rawData = (message as { rawData?: { message_snapshots?: unknown } }).rawData;
|
|
63
|
+
return normalizeDiscordMessageSnapshots(
|
|
64
|
+
rawData?.message_snapshots ??
|
|
65
|
+
(message as { message_snapshots?: unknown }).message_snapshots ??
|
|
66
|
+
(message as { messageSnapshots?: unknown }).messageSnapshots,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeDiscordMessageSnapshots(snapshots: unknown): DiscordMessageSnapshot[] {
|
|
71
|
+
if (!Array.isArray(snapshots)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return snapshots.filter(
|
|
75
|
+
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveDiscordReferencedForwardMessage(message: Message): Message | null {
|
|
80
|
+
const referenceType = message.messageReference?.type;
|
|
81
|
+
return Number(referenceType) === FORWARD_MESSAGE_REFERENCE_TYPE
|
|
82
|
+
? message.referencedMessage
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatDiscordSnapshotAuthor(
|
|
87
|
+
author: DiscordSnapshotAuthor | null | undefined,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
if (!author) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const globalName = normalizeOptionalString(author.global_name) ?? undefined;
|
|
93
|
+
const username = normalizeOptionalString(author.username) ?? undefined;
|
|
94
|
+
const name = normalizeOptionalString(author.name) ?? undefined;
|
|
95
|
+
const discriminator = normalizeOptionalString(author.discriminator) ?? undefined;
|
|
96
|
+
const base = globalName || username || name;
|
|
97
|
+
if (username && discriminator && discriminator !== "0") {
|
|
98
|
+
return `@${username}#${discriminator}`;
|
|
99
|
+
}
|
|
100
|
+
if (base) {
|
|
101
|
+
return `@${base}`;
|
|
102
|
+
}
|
|
103
|
+
if (author.id) {
|
|
104
|
+
return `@${author.id}`;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyImplicitReplyBatchGate } from "./message-handler.batch-gate.js";
|
|
3
|
+
|
|
4
|
+
describe("applyImplicitReplyBatchGate", () => {
|
|
5
|
+
it("leaves context unchanged when replyToMode is not batched", () => {
|
|
6
|
+
const ctx: Record<string, unknown> = {};
|
|
7
|
+
applyImplicitReplyBatchGate(ctx, "first", true);
|
|
8
|
+
expect(ctx.ReplyThreading).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("marks single-message turns as not eligible for implicit reply refs", () => {
|
|
12
|
+
const ctx: Record<string, unknown> = {};
|
|
13
|
+
applyImplicitReplyBatchGate(ctx, "batched", false);
|
|
14
|
+
expect(ctx.ReplyThreading).toEqual({ implicitCurrentMessage: "deny" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("marks batched turns as eligible for implicit reply refs", () => {
|
|
18
|
+
const ctx: Record<string, unknown> = {};
|
|
19
|
+
applyImplicitReplyBatchGate(ctx, "batched", true);
|
|
20
|
+
expect(ctx.ReplyThreading).toEqual({ implicitCurrentMessage: "allow" });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReplyToMode } from "openclaw/plugin-sdk/config-types";
|
|
2
|
+
import type { ReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
|
|
3
|
+
import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
|
|
4
|
+
|
|
5
|
+
type ReplyThreadingContext = {
|
|
6
|
+
ReplyThreading?: ReplyThreadingPolicy;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function applyImplicitReplyBatchGate(
|
|
10
|
+
ctx: object,
|
|
11
|
+
replyToMode: ReplyToMode,
|
|
12
|
+
isBatched: boolean,
|
|
13
|
+
) {
|
|
14
|
+
const replyThreading = resolveBatchedReplyThreadingPolicy(replyToMode, isBatched);
|
|
15
|
+
if (!replyThreading) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
(ctx as ReplyThreadingContext).ReplyThreading = replyThreading;
|
|
19
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createDiscordMessageHandler,
|
|
4
|
+
preflightDiscordMessageMock,
|
|
5
|
+
processDiscordMessageMock,
|
|
6
|
+
} from "./message-handler.module-test-helpers.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_DISCORD_BOT_USER_ID,
|
|
9
|
+
createDiscordHandlerParams,
|
|
10
|
+
createDiscordPreflightContext,
|
|
11
|
+
} from "./message-handler.test-helpers.js";
|
|
12
|
+
|
|
13
|
+
async function flushAsyncWork() {
|
|
14
|
+
await Promise.resolve();
|
|
15
|
+
await Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createMessageData(authorId: string, channelId = "ch-1") {
|
|
19
|
+
return {
|
|
20
|
+
author: { id: authorId, bot: authorId === DEFAULT_DISCORD_BOT_USER_ID },
|
|
21
|
+
message: {
|
|
22
|
+
id: "msg-1",
|
|
23
|
+
author: { id: authorId, bot: authorId === DEFAULT_DISCORD_BOT_USER_ID },
|
|
24
|
+
content: "hello",
|
|
25
|
+
channel_id: channelId,
|
|
26
|
+
},
|
|
27
|
+
channel_id: channelId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createPreflightContext(channelId = "ch-1") {
|
|
32
|
+
return createDiscordPreflightContext(channelId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("createDiscordMessageHandler bot-self filter", () => {
|
|
36
|
+
it("skips bot-own messages before the debounce queue", async () => {
|
|
37
|
+
preflightDiscordMessageMock.mockReset();
|
|
38
|
+
processDiscordMessageMock.mockReset();
|
|
39
|
+
|
|
40
|
+
const handler = createDiscordMessageHandler(createDiscordHandlerParams());
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
handler(createMessageData(DEFAULT_DISCORD_BOT_USER_ID) as never, {} as never),
|
|
44
|
+
).resolves.toBeUndefined();
|
|
45
|
+
|
|
46
|
+
expect(preflightDiscordMessageMock).not.toHaveBeenCalled();
|
|
47
|
+
expect(processDiscordMessageMock).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("enqueues non-bot messages for processing", async () => {
|
|
51
|
+
preflightDiscordMessageMock.mockReset();
|
|
52
|
+
processDiscordMessageMock.mockReset();
|
|
53
|
+
preflightDiscordMessageMock.mockImplementation(
|
|
54
|
+
async (params: { data: { channel_id: string } }) =>
|
|
55
|
+
createPreflightContext(params.data.channel_id),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const handler = createDiscordMessageHandler(createDiscordHandlerParams());
|
|
59
|
+
|
|
60
|
+
await expect(
|
|
61
|
+
handler(createMessageData("user-456") as never, {} as never),
|
|
62
|
+
).resolves.toBeUndefined();
|
|
63
|
+
|
|
64
|
+
await flushAsyncWork();
|
|
65
|
+
expect(preflightDiscordMessageMock).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
});
|