@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +20 -2
- package/bootstrap.json +26 -0
- package/dist/admin/admin.controller.d.ts +139 -0
- package/dist/admin/admin.controller.js +238 -0
- package/dist/admin/extensions.controller.d.ts +100 -0
- package/dist/admin/extensions.controller.js +226 -0
- package/dist/admin/setup.controller.d.ts +36 -0
- package/dist/admin/setup.controller.js +106 -0
- package/dist/admin/setup.service.d.ts +47 -0
- package/dist/admin/setup.service.js +90 -0
- package/dist/agents/agents.controller.d.ts +205 -0
- package/dist/agents/agents.controller.js +144 -0
- package/dist/agents/agents.service.d.ts +177 -0
- package/dist/agents/agents.service.js +677 -0
- package/dist/agents/definition.d.ts +198 -0
- package/dist/agents/definition.js +152 -0
- package/dist/agents/models.controller.d.ts +13 -0
- package/dist/agents/models.controller.js +31 -0
- package/dist/agents/templates.controller.d.ts +15 -0
- package/dist/agents/templates.controller.js +37 -0
- package/dist/agents/templates.d.ts +83 -0
- package/dist/agents/templates.js +173 -0
- package/dist/catalog/admin-catalog.controller.d.ts +143 -0
- package/dist/catalog/admin-catalog.controller.js +245 -0
- package/dist/catalog/catalog.service.d.ts +430 -0
- package/dist/catalog/catalog.service.js +882 -0
- package/dist/catalog/claude/agents.d.ts +27 -0
- package/dist/catalog/claude/agents.js +270 -0
- package/dist/catalog/claude/commands.d.ts +50 -0
- package/dist/catalog/claude/commands.js +179 -0
- package/dist/catalog/claude/manifest.d.ts +69 -0
- package/dist/catalog/claude/manifest.js +134 -0
- package/dist/catalog/claude/marketplace.d.ts +35 -0
- package/dist/catalog/claude/marketplace.js +194 -0
- package/dist/catalog/claude/mcp.d.ts +33 -0
- package/dist/catalog/claude/mcp.js +226 -0
- package/dist/catalog/claude/report.d.ts +86 -0
- package/dist/catalog/claude/report.js +121 -0
- package/dist/catalog/claude/translate.d.ts +56 -0
- package/dist/catalog/claude/translate.js +857 -0
- package/dist/catalog/cli.d.ts +46 -0
- package/dist/catalog/cli.js +211 -0
- package/dist/catalog/git-source.d.ts +127 -0
- package/dist/catalog/git-source.js +520 -0
- package/dist/catalog/marketplace-fetch.d.ts +99 -0
- package/dist/catalog/marketplace-fetch.js +203 -0
- package/dist/catalog/marketplaces.controller.d.ts +97 -0
- package/dist/catalog/marketplaces.controller.js +197 -0
- package/dist/catalog/marketplaces.service.d.ts +340 -0
- package/dist/catalog/marketplaces.service.js +1217 -0
- package/dist/catalog/org-catalog.controller.d.ts +128 -0
- package/dist/catalog/org-catalog.controller.js +277 -0
- package/dist/catalog/org-catalog.service.d.ts +246 -0
- package/dist/catalog/org-catalog.service.js +625 -0
- package/dist/catalog/plugin-import.d.ts +436 -0
- package/dist/catalog/plugin-import.js +1069 -0
- package/dist/catalog/plugin-install.d.ts +447 -0
- package/dist/catalog/plugin-install.js +1333 -0
- package/dist/catalog/plugin-package.d.ts +143 -0
- package/dist/catalog/plugin-package.js +382 -0
- package/dist/catalog/plugin-upgrade.d.ts +222 -0
- package/dist/catalog/plugin-upgrade.js +1219 -0
- package/dist/catalog/requests.service.d.ts +162 -0
- package/dist/catalog/requests.service.js +504 -0
- package/dist/catalog/schema-path.d.ts +7 -0
- package/dist/catalog/schema-path.js +63 -0
- package/dist/catalog/upgrade-targets.d.ts +59 -0
- package/dist/catalog/upgrade-targets.js +90 -0
- package/dist/catalog/versions.d.ts +31 -0
- package/dist/catalog/versions.js +44 -0
- package/dist/channels/actions.d.ts +37 -0
- package/dist/channels/actions.js +121 -0
- package/dist/channels/cards.d.ts +44 -0
- package/dist/channels/cards.js +156 -0
- package/dist/channels/connections.controller.d.ts +59 -0
- package/dist/channels/connections.controller.js +115 -0
- package/dist/channels/connections.service.d.ts +187 -0
- package/dist/channels/connections.service.js +534 -0
- package/dist/channels/features.d.ts +12 -0
- package/dist/channels/features.js +22 -0
- package/dist/channels/inbound.d.ts +120 -0
- package/dist/channels/inbound.js +332 -0
- package/dist/channels/outbound.d.ts +140 -0
- package/dist/channels/outbound.js +1126 -0
- package/dist/channels/phrases.d.ts +100 -0
- package/dist/channels/phrases.js +81 -0
- package/dist/channels/supervisor.d.ts +182 -0
- package/dist/channels/supervisor.js +625 -0
- package/dist/channels/threads.d.ts +29 -0
- package/dist/channels/threads.js +104 -0
- package/dist/channels/webhook-url.d.ts +16 -0
- package/dist/channels/webhook-url.js +34 -0
- package/dist/channels/webhook.controller.d.ts +19 -0
- package/dist/channels/webhook.controller.js +72 -0
- package/dist/channels/webhook.d.ts +73 -0
- package/dist/channels/webhook.js +274 -0
- package/dist/config/docs.d.ts +9 -0
- package/dist/config/docs.js +85 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +73 -0
- package/dist/config/fields.d.ts +42 -0
- package/dist/config/fields.js +214 -0
- package/dist/config/file-source.d.ts +3 -0
- package/dist/config/file-source.js +31 -0
- package/dist/config/load.d.ts +20 -0
- package/dist/config/load.js +98 -0
- package/dist/config/nacos-source.d.ts +31 -0
- package/dist/config/nacos-source.js +229 -0
- package/dist/config/schema.d.ts +237 -0
- package/dist/config/schema.js +1192 -0
- package/dist/config/sources.d.ts +18 -0
- package/dist/config/sources.js +59 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.js +7 -0
- package/dist/conversations/artifacts.d.ts +52 -0
- package/dist/conversations/artifacts.js +137 -0
- package/dist/conversations/attachments.controller.d.ts +28 -0
- package/dist/conversations/attachments.controller.js +96 -0
- package/dist/conversations/attachments.service.d.ts +180 -0
- package/dist/conversations/attachments.service.js +578 -0
- package/dist/conversations/conversations.controller.d.ts +288 -0
- package/dist/conversations/conversations.controller.js +607 -0
- package/dist/conversations/conversations.service.d.ts +321 -0
- package/dist/conversations/conversations.service.js +754 -0
- package/dist/conversations/event-log.d.ts +122 -0
- package/dist/conversations/event-log.js +259 -0
- package/dist/conversations/events.d.ts +856 -0
- package/dist/conversations/events.js +639 -0
- package/dist/conversations/files.d.ts +92 -0
- package/dist/conversations/files.js +213 -0
- package/dist/conversations/outputs-read.d.ts +34 -0
- package/dist/conversations/outputs-read.js +37 -0
- package/dist/conversations/queue.d.ts +169 -0
- package/dist/conversations/queue.js +527 -0
- package/dist/conversations/sandbox-status.d.ts +21 -0
- package/dist/conversations/sandbox-status.js +24 -0
- package/dist/conversations/selection.d.ts +46 -0
- package/dist/conversations/selection.js +75 -0
- package/dist/conversations/titles.d.ts +93 -0
- package/dist/conversations/titles.js +423 -0
- package/dist/ctx/bus.d.ts +16 -0
- package/dist/ctx/bus.js +101 -0
- package/dist/ctx/create-ctx.d.ts +109 -0
- package/dist/ctx/create-ctx.js +592 -0
- package/dist/ctx/kv.d.ts +4 -0
- package/dist/ctx/kv.js +31 -0
- package/dist/ctx/lock.d.ts +4 -0
- package/dist/ctx/lock.js +39 -0
- package/dist/ctx/namespace.d.ts +6 -0
- package/dist/ctx/namespace.js +12 -0
- package/dist/ctx/queue.d.ts +25 -0
- package/dist/ctx/queue.js +151 -0
- package/dist/ctx/secrets.d.ts +45 -0
- package/dist/ctx/secrets.js +175 -0
- package/dist/db/kernel-schema.d.ts +1122 -0
- package/dist/db/kernel-schema.js +5 -0
- package/dist/db/migrator.d.ts +28 -0
- package/dist/db/migrator.js +140 -0
- package/dist/db/pool.d.ts +21 -0
- package/dist/db/pool.js +28 -0
- package/dist/db/sql-statements.d.ts +34 -0
- package/dist/db/sql-statements.js +168 -0
- package/dist/errors.d.ts +211 -0
- package/dist/errors.js +213 -0
- package/dist/evals/budget.d.ts +61 -0
- package/dist/evals/budget.js +181 -0
- package/dist/evals/diff.d.ts +55 -0
- package/dist/evals/diff.js +133 -0
- package/dist/evals/judge.d.ts +53 -0
- package/dist/evals/judge.js +149 -0
- package/dist/evals/plan.d.ts +47 -0
- package/dist/evals/plan.js +77 -0
- package/dist/evals/reconstruct.d.ts +137 -0
- package/dist/evals/reconstruct.js +322 -0
- package/dist/evals/recorded-tools.d.ts +54 -0
- package/dist/evals/recorded-tools.js +167 -0
- package/dist/evals/run-replay.d.ts +86 -0
- package/dist/evals/run-replay.js +502 -0
- package/dist/evals/runs.controller.d.ts +100 -0
- package/dist/evals/runs.controller.js +199 -0
- package/dist/evals/runs.service.d.ts +275 -0
- package/dist/evals/runs.service.js +836 -0
- package/dist/evals/sets.controller.d.ts +61 -0
- package/dist/evals/sets.controller.js +183 -0
- package/dist/evals/sets.service.d.ts +128 -0
- package/dist/evals/sets.service.js +558 -0
- package/dist/evals/snapshot.d.ts +76 -0
- package/dist/evals/snapshot.js +207 -0
- package/dist/evals/step-replay.d.ts +113 -0
- package/dist/evals/step-replay.js +181 -0
- package/dist/evals/trajectory.controller.d.ts +41 -0
- package/dist/evals/trajectory.controller.js +88 -0
- package/dist/evals/trajectory.d.ts +144 -0
- package/dist/evals/trajectory.js +338 -0
- package/dist/evals/trajectory.service.d.ts +108 -0
- package/dist/evals/trajectory.service.js +323 -0
- package/dist/evals/worker.d.ts +101 -0
- package/dist/evals/worker.js +790 -0
- package/dist/extensions/admin-ops.d.ts +105 -0
- package/dist/extensions/admin-ops.js +353 -0
- package/dist/extensions/admin-view.d.ts +79 -0
- package/dist/extensions/admin-view.js +327 -0
- package/dist/extensions/approvals.d.ts +119 -0
- package/dist/extensions/approvals.js +205 -0
- package/dist/extensions/blobs.d.ts +45 -0
- package/dist/extensions/blobs.js +160 -0
- package/dist/extensions/bootstrap.d.ts +176 -0
- package/dist/extensions/bootstrap.js +535 -0
- package/dist/extensions/cli.d.ts +61 -0
- package/dist/extensions/cli.js +165 -0
- package/dist/extensions/config-json-schema.d.ts +9 -0
- package/dist/extensions/config-json-schema.js +73 -0
- package/dist/extensions/deployment-config.d.ts +138 -0
- package/dist/extensions/deployment-config.js +394 -0
- package/dist/extensions/enabled.d.ts +31 -0
- package/dist/extensions/enabled.js +55 -0
- package/dist/extensions/hooks.d.ts +161 -0
- package/dist/extensions/hooks.js +425 -0
- package/dist/extensions/host.d.ts +577 -0
- package/dist/extensions/host.js +2071 -0
- package/dist/extensions/inflight.d.ts +69 -0
- package/dist/extensions/inflight.js +277 -0
- package/dist/extensions/installs.d.ts +166 -0
- package/dist/extensions/installs.js +253 -0
- package/dist/extensions/instance.d.ts +167 -0
- package/dist/extensions/instance.js +329 -0
- package/dist/extensions/ledger.d.ts +30 -0
- package/dist/extensions/ledger.js +50 -0
- package/dist/extensions/lifecycle.d.ts +80 -0
- package/dist/extensions/lifecycle.js +152 -0
- package/dist/extensions/migrate-lock.d.ts +51 -0
- package/dist/extensions/migrate-lock.js +100 -0
- package/dist/extensions/org-config.controller.d.ts +24 -0
- package/dist/extensions/org-config.controller.js +59 -0
- package/dist/extensions/org-config.d.ts +169 -0
- package/dist/extensions/org-config.js +520 -0
- package/dist/extensions/package.d.ts +115 -0
- package/dist/extensions/package.js +246 -0
- package/dist/extensions/permission-set.d.ts +117 -0
- package/dist/extensions/permission-set.js +184 -0
- package/dist/extensions/permission-view.d.ts +194 -0
- package/dist/extensions/permission-view.js +284 -0
- package/dist/extensions/registry.d.ts +24 -0
- package/dist/extensions/registry.js +56 -0
- package/dist/extensions/resolve.d.ts +12 -0
- package/dist/extensions/resolve.js +71 -0
- package/dist/extensions/sources.d.ts +69 -0
- package/dist/extensions/sources.js +120 -0
- package/dist/extensions/sync.d.ts +40 -0
- package/dist/extensions/sync.js +242 -0
- package/dist/files/previewers.controller.d.ts +46 -0
- package/dist/files/previewers.controller.js +105 -0
- package/dist/files/previewers.d.ts +100 -0
- package/dist/files/previewers.js +250 -0
- package/dist/http/api.module.d.ts +114 -0
- package/dist/http/api.module.js +448 -0
- package/dist/http/app.d.ts +77 -0
- package/dist/http/app.js +220 -0
- package/dist/http/error.filter.d.ts +21 -0
- package/dist/http/error.filter.js +85 -0
- package/dist/http/gateway.module.d.ts +15 -0
- package/dist/http/gateway.module.js +13 -0
- package/dist/http/health.controller.d.ts +26 -0
- package/dist/http/health.controller.js +79 -0
- package/dist/http/nest-logger.d.ts +20 -0
- package/dist/http/nest-logger.js +28 -0
- package/dist/http/plugin-dispatch.d.ts +68 -0
- package/dist/http/plugin-dispatch.js +200 -0
- package/dist/http/tokens.d.ts +131 -0
- package/dist/http/tokens.js +131 -0
- package/dist/http/zod.pipe.d.ts +8 -0
- package/dist/http/zod.pipe.js +14 -0
- package/dist/identity/accounts.d.ts +93 -0
- package/dist/identity/accounts.js +367 -0
- package/dist/identity/api-tokens.d.ts +73 -0
- package/dist/identity/api-tokens.js +197 -0
- package/dist/identity/audit.d.ts +23 -0
- package/dist/identity/audit.js +41 -0
- package/dist/identity/auth.controller.d.ts +40 -0
- package/dist/identity/auth.controller.js +130 -0
- package/dist/identity/auth.guard.d.ts +41 -0
- package/dist/identity/auth.guard.js +162 -0
- package/dist/identity/binding.d.ts +47 -0
- package/dist/identity/binding.js +84 -0
- package/dist/identity/cookies.d.ts +16 -0
- package/dist/identity/cookies.js +32 -0
- package/dist/identity/credential-check.d.ts +17 -0
- package/dist/identity/credential-check.js +24 -0
- package/dist/identity/enterprise-admin.d.ts +14 -0
- package/dist/identity/enterprise-admin.js +27 -0
- package/dist/identity/external.d.ts +106 -0
- package/dist/identity/external.js +337 -0
- package/dist/identity/hash.d.ts +2 -0
- package/dist/identity/hash.js +3 -0
- package/dist/identity/login.d.ts +6 -0
- package/dist/identity/login.js +20 -0
- package/dist/identity/me.controller.d.ts +61 -0
- package/dist/identity/me.controller.js +159 -0
- package/dist/identity/passwords.d.ts +6 -0
- package/dist/identity/passwords.js +37 -0
- package/dist/identity/principal.d.ts +56 -0
- package/dist/identity/principal.js +38 -0
- package/dist/identity/rate-limit.d.ts +55 -0
- package/dist/identity/rate-limit.js +121 -0
- package/dist/identity/sessions.d.ts +84 -0
- package/dist/identity/sessions.js +306 -0
- package/dist/identity/settings.d.ts +121 -0
- package/dist/identity/settings.js +248 -0
- package/dist/identity/sso.controller.d.ts +59 -0
- package/dist/identity/sso.controller.js +192 -0
- package/dist/identity/sso.d.ts +83 -0
- package/dist/identity/sso.js +202 -0
- package/dist/inbox/inbox.service.d.ts +43 -0
- package/dist/inbox/inbox.service.js +57 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +67 -0
- package/dist/kernel.d.ts +259 -0
- package/dist/kernel.js +1326 -0
- package/dist/log-config.d.ts +9 -0
- package/dist/log-config.js +27 -0
- package/dist/log-context.d.ts +32 -0
- package/dist/log-context.js +40 -0
- package/dist/log-loki.d.ts +34 -0
- package/dist/log-loki.js +145 -0
- package/dist/log.d.ts +79 -0
- package/dist/log.js +303 -0
- package/dist/models/prices.d.ts +53 -0
- package/dist/models/prices.js +121 -0
- package/dist/models/purposes.controller.d.ts +19 -0
- package/dist/models/purposes.controller.js +56 -0
- package/dist/models/purposes.d.ts +140 -0
- package/dist/models/purposes.js +267 -0
- package/dist/net/proxy.d.ts +44 -0
- package/dist/net/proxy.js +162 -0
- package/dist/net/public-address.d.ts +60 -0
- package/dist/net/public-address.js +272 -0
- package/dist/net/safe-fetch.d.ts +38 -0
- package/dist/net/safe-fetch.js +140 -0
- package/dist/orgs/directory-sync.controller.d.ts +14 -0
- package/dist/orgs/directory-sync.controller.js +40 -0
- package/dist/orgs/directory-sync.d.ts +166 -0
- package/dist/orgs/directory-sync.js +558 -0
- package/dist/orgs/invitations.controller.d.ts +58 -0
- package/dist/orgs/invitations.controller.js +137 -0
- package/dist/orgs/invitations.service.d.ts +80 -0
- package/dist/orgs/invitations.service.js +217 -0
- package/dist/orgs/members.service.d.ts +63 -0
- package/dist/orgs/members.service.js +234 -0
- package/dist/orgs/offboarding.d.ts +141 -0
- package/dist/orgs/offboarding.js +398 -0
- package/dist/orgs/org-lock.d.ts +32 -0
- package/dist/orgs/org-lock.js +46 -0
- package/dist/orgs/org.guard.d.ts +35 -0
- package/dist/orgs/org.guard.js +95 -0
- package/dist/orgs/orgs.controller.d.ts +78 -0
- package/dist/orgs/orgs.controller.js +192 -0
- package/dist/orgs/orgs.service.d.ts +61 -0
- package/dist/orgs/orgs.service.js +209 -0
- package/dist/orgs/roles.d.ts +13 -0
- package/dist/orgs/roles.js +58 -0
- package/dist/orgs/settings.controller.d.ts +20 -0
- package/dist/orgs/settings.controller.js +53 -0
- package/dist/orgs/settings.service.d.ts +120 -0
- package/dist/orgs/settings.service.js +178 -0
- package/dist/packages/npm.d.ts +50 -0
- package/dist/packages/npm.js +218 -0
- package/dist/packages/provenance.d.ts +47 -0
- package/dist/packages/provenance.js +147 -0
- package/dist/packages/tgz.d.ts +76 -0
- package/dist/packages/tgz.js +225 -0
- package/dist/runtime/abort.d.ts +32 -0
- package/dist/runtime/abort.js +71 -0
- package/dist/runtime/checkpointer.d.ts +32 -0
- package/dist/runtime/checkpointer.js +243 -0
- package/dist/runtime/context/build.d.ts +254 -0
- package/dist/runtime/context/build.js +693 -0
- package/dist/runtime/context/claude-tools.d.ts +11 -0
- package/dist/runtime/context/claude-tools.js +24 -0
- package/dist/runtime/context/compaction.d.ts +26 -0
- package/dist/runtime/context/compaction.js +195 -0
- package/dist/runtime/context/pairing.d.ts +11 -0
- package/dist/runtime/context/pairing.js +82 -0
- package/dist/runtime/context/reminders.d.ts +5 -0
- package/dist/runtime/context/reminders.js +11 -0
- package/dist/runtime/context/skills.d.ts +105 -0
- package/dist/runtime/context/skills.js +283 -0
- package/dist/runtime/context/tokens.d.ts +19 -0
- package/dist/runtime/context/tokens.js +104 -0
- package/dist/runtime/conversation-lock.d.ts +40 -0
- package/dist/runtime/conversation-lock.js +97 -0
- package/dist/runtime/hook-calls.d.ts +84 -0
- package/dist/runtime/hook-calls.js +136 -0
- package/dist/runtime/langgraph-loop.d.ts +52 -0
- package/dist/runtime/langgraph-loop.js +247 -0
- package/dist/runtime/loop.d.ts +93 -0
- package/dist/runtime/loop.js +7 -0
- package/dist/runtime/maintenance.d.ts +80 -0
- package/dist/runtime/maintenance.js +148 -0
- package/dist/runtime/media-cache.d.ts +21 -0
- package/dist/runtime/media-cache.js +51 -0
- package/dist/runtime/model-call.d.ts +176 -0
- package/dist/runtime/model-call.js +1049 -0
- package/dist/runtime/providers.d.ts +86 -0
- package/dist/runtime/providers.js +224 -0
- package/dist/runtime/quota.d.ts +29 -0
- package/dist/runtime/quota.js +70 -0
- package/dist/runtime/run-finish.d.ts +45 -0
- package/dist/runtime/run-finish.js +69 -0
- package/dist/runtime/run-queue.d.ts +121 -0
- package/dist/runtime/run-queue.js +493 -0
- package/dist/runtime/run-worker.d.ts +138 -0
- package/dist/runtime/run-worker.js +806 -0
- package/dist/runtime/stream.controller.d.ts +34 -0
- package/dist/runtime/stream.controller.js +122 -0
- package/dist/runtime/stream.d.ts +63 -0
- package/dist/runtime/stream.js +262 -0
- package/dist/runtime/subagents/child.d.ts +80 -0
- package/dist/runtime/subagents/child.js +225 -0
- package/dist/runtime/subagents/definition.d.ts +103 -0
- package/dist/runtime/subagents/definition.js +392 -0
- package/dist/runtime/subagents/dispatch.d.ts +69 -0
- package/dist/runtime/subagents/dispatch.js +502 -0
- package/dist/runtime/subagents/limits.d.ts +23 -0
- package/dist/runtime/subagents/limits.js +34 -0
- package/dist/runtime/subagents/org-agents.d.ts +50 -0
- package/dist/runtime/subagents/org-agents.js +70 -0
- package/dist/runtime/subagents/preamble.d.ts +7 -0
- package/dist/runtime/subagents/preamble.js +11 -0
- package/dist/runtime/subagents/progress.d.ts +26 -0
- package/dist/runtime/subagents/progress.js +36 -0
- package/dist/runtime/subagents/roster.d.ts +55 -0
- package/dist/runtime/subagents/roster.js +111 -0
- package/dist/runtime/subagents/skill-fork.d.ts +123 -0
- package/dist/runtime/subagents/skill-fork.js +259 -0
- package/dist/runtime/usage-report.d.ts +163 -0
- package/dist/runtime/usage-report.js +551 -0
- package/dist/runtime/waits.d.ts +233 -0
- package/dist/runtime/waits.js +883 -0
- package/dist/sandbox/leases.d.ts +169 -0
- package/dist/sandbox/leases.js +515 -0
- package/dist/sandbox/lifecycle.d.ts +21 -0
- package/dist/sandbox/lifecycle.js +68 -0
- package/dist/sandbox/sandbox.service.d.ts +249 -0
- package/dist/sandbox/sandbox.service.js +908 -0
- package/dist/skills/catalog.d.ts +149 -0
- package/dist/skills/catalog.js +405 -0
- package/dist/skills/changes.controller.d.ts +56 -0
- package/dist/skills/changes.controller.js +136 -0
- package/dist/skills/changes.service.d.ts +175 -0
- package/dist/skills/changes.service.js +805 -0
- package/dist/skills/invocable.controller.d.ts +23 -0
- package/dist/skills/invocable.controller.js +50 -0
- package/dist/skills/invocation-keys.d.ts +23 -0
- package/dist/skills/invocation-keys.js +44 -0
- package/dist/skills/invoke.d.ts +135 -0
- package/dist/skills/invoke.js +286 -0
- package/dist/skills/package.d.ts +87 -0
- package/dist/skills/package.js +227 -0
- package/dist/skills/skills.controller.d.ts +40 -0
- package/dist/skills/skills.controller.js +110 -0
- package/dist/skills/skills.service.d.ts +192 -0
- package/dist/skills/skills.service.js +567 -0
- package/dist/storage/prefix.d.ts +36 -0
- package/dist/storage/prefix.js +104 -0
- package/dist/storage/purge.d.ts +27 -0
- package/dist/storage/purge.js +75 -0
- package/dist/storage/storage.service.d.ts +44 -0
- package/dist/storage/storage.service.js +145 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +13 -0
- package/dist/tools/approvals.controller.d.ts +53 -0
- package/dist/tools/approvals.controller.js +128 -0
- package/dist/tools/approvals.service.d.ts +172 -0
- package/dist/tools/approvals.service.js +568 -0
- package/dist/tools/breaker.d.ts +28 -0
- package/dist/tools/breaker.js +72 -0
- package/dist/tools/builtins/agent.d.ts +81 -0
- package/dist/tools/builtins/agent.js +223 -0
- package/dist/tools/builtins/ask-user.d.ts +28 -0
- package/dist/tools/builtins/ask-user.js +139 -0
- package/dist/tools/builtins/read-tool-output.d.ts +18 -0
- package/dist/tools/builtins/read-tool-output.js +108 -0
- package/dist/tools/builtins/sandbox/bash.d.ts +16 -0
- package/dist/tools/builtins/sandbox/bash.js +137 -0
- package/dist/tools/builtins/sandbox/common.d.ts +116 -0
- package/dist/tools/builtins/sandbox/common.js +306 -0
- package/dist/tools/builtins/sandbox/deliver.d.ts +5 -0
- package/dist/tools/builtins/sandbox/deliver.js +180 -0
- package/dist/tools/builtins/sandbox/edit.d.ts +4 -0
- package/dist/tools/builtins/sandbox/edit.js +98 -0
- package/dist/tools/builtins/sandbox/glob-match.d.ts +25 -0
- package/dist/tools/builtins/sandbox/glob-match.js +240 -0
- package/dist/tools/builtins/sandbox/glob.d.ts +4 -0
- package/dist/tools/builtins/sandbox/glob.js +104 -0
- package/dist/tools/builtins/sandbox/grep.d.ts +9 -0
- package/dist/tools/builtins/sandbox/grep.js +133 -0
- package/dist/tools/builtins/sandbox/index.d.ts +12 -0
- package/dist/tools/builtins/sandbox/index.js +27 -0
- package/dist/tools/builtins/sandbox/names.d.ts +7 -0
- package/dist/tools/builtins/sandbox/names.js +20 -0
- package/dist/tools/builtins/sandbox/read.d.ts +8 -0
- package/dist/tools/builtins/sandbox/read.js +121 -0
- package/dist/tools/builtins/sandbox/write.d.ts +4 -0
- package/dist/tools/builtins/sandbox/write.js +88 -0
- package/dist/tools/builtins/skill-load.d.ts +53 -0
- package/dist/tools/builtins/skill-load.js +446 -0
- package/dist/tools/builtins/skill-write.d.ts +59 -0
- package/dist/tools/builtins/skill-write.js +244 -0
- package/dist/tools/builtins/tool-search.d.ts +31 -0
- package/dist/tools/builtins/tool-search.js +171 -0
- package/dist/tools/connector-content.controller.d.ts +34 -0
- package/dist/tools/connector-content.controller.js +78 -0
- package/dist/tools/connector-content.d.ts +104 -0
- package/dist/tools/connector-content.js +267 -0
- package/dist/tools/connectors.controller.d.ts +210 -0
- package/dist/tools/connectors.controller.js +234 -0
- package/dist/tools/connectors.service.d.ts +303 -0
- package/dist/tools/connectors.service.js +1102 -0
- package/dist/tools/credentials.d.ts +139 -0
- package/dist/tools/credentials.js +351 -0
- package/dist/tools/drift.d.ts +141 -0
- package/dist/tools/drift.js +539 -0
- package/dist/tools/elicitation.d.ts +81 -0
- package/dist/tools/elicitation.js +266 -0
- package/dist/tools/endpoint.d.ts +18 -0
- package/dist/tools/endpoint.js +115 -0
- package/dist/tools/info.d.ts +67 -0
- package/dist/tools/info.js +113 -0
- package/dist/tools/naming.d.ts +27 -0
- package/dist/tools/naming.js +47 -0
- package/dist/tools/oauth/discovery.d.ts +65 -0
- package/dist/tools/oauth/discovery.js +307 -0
- package/dist/tools/oauth/flow.d.ts +63 -0
- package/dist/tools/oauth/flow.js +237 -0
- package/dist/tools/oauth/oauth.controller.d.ts +31 -0
- package/dist/tools/oauth/oauth.controller.js +95 -0
- package/dist/tools/oauth/tokens.d.ts +111 -0
- package/dist/tools/oauth/tokens.js +258 -0
- package/dist/tools/outputs.d.ts +61 -0
- package/dist/tools/outputs.js +156 -0
- package/dist/tools/pipeline.d.ts +322 -0
- package/dist/tools/pipeline.js +1236 -0
- package/dist/tools/policy.d.ts +60 -0
- package/dist/tools/policy.js +236 -0
- package/dist/tools/questions.controller.d.ts +24 -0
- package/dist/tools/questions.controller.js +58 -0
- package/dist/tools/questions.service.d.ts +103 -0
- package/dist/tools/questions.service.js +295 -0
- package/dist/tools/redact.d.ts +20 -0
- package/dist/tools/redact.js +60 -0
- package/dist/tools/registry.d.ts +246 -0
- package/dist/tools/registry.js +481 -0
- package/dist/tools/resources.d.ts +16 -0
- package/dist/tools/resources.js +76 -0
- package/dist/tools/specs.d.ts +14 -0
- package/dist/tools/specs.js +31 -0
- package/dist/tools/validate.d.ts +20 -0
- package/dist/tools/validate.js +183 -0
- package/dist/tools/view.d.ts +43 -0
- package/dist/tools/view.js +225 -0
- package/dist/ui/embed-token.d.ts +31 -0
- package/dist/ui/embed-token.js +132 -0
- package/dist/ui/registry.d.ts +58 -0
- package/dist/ui/registry.js +90 -0
- package/dist/ui/ui-stream.d.ts +37 -0
- package/dist/ui/ui-stream.js +162 -0
- package/dist/ui/views.controller.d.ts +81 -0
- package/dist/ui/views.controller.js +261 -0
- package/dist/ui/views.d.ts +155 -0
- package/dist/ui/views.js +306 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +6 -0
- package/package.json +69 -3
- package/src/migrations/0001_init.sql +8 -0
- package/src/migrations/0002_secrets.sql +8 -0
- package/src/migrations/0003_identity.sql +117 -0
- package/src/migrations/0004_plugin_org_config_nullable.sql +3 -0
- package/src/migrations/0005_agents.sql +27 -0
- package/src/migrations/0006_conversations.sql +37 -0
- package/src/migrations/0007_checkpoints.sql +26 -0
- package/src/migrations/0008_runs_trigger_seq.sql +3 -0
- package/src/migrations/0009_runs_active_unique.sql +4 -0
- package/src/migrations/0010_tools.sql +125 -0
- package/src/migrations/0011_approvals_spec.sql +10 -0
- package/src/migrations/0012_run_waits.sql +24 -0
- package/src/migrations/0013_app_tools_source.sql +4 -0
- package/src/migrations/0014_tool_specs.sql +8 -0
- package/src/migrations/0015_tool_drift.sql +20 -0
- package/src/migrations/0016_plugin_permissions.sql +11 -0
- package/src/migrations/0017_identity_sources.sql +6 -0
- package/src/migrations/0018_channels.sql +74 -0
- package/src/migrations/0019_identity_trust.sql +17 -0
- package/src/migrations/0020_channel_card_lookup.sql +10 -0
- package/src/migrations/0021_org_storage_purge.sql +9 -0
- package/src/migrations/0022_attachments.sql +29 -0
- package/src/migrations/0023_sandboxes.sql +37 -0
- package/src/migrations/0024_skills.sql +46 -0
- package/src/migrations/0025_skills_catalog_idx.sql +6 -0
- package/src/migrations/0026_skill_changes.sql +34 -0
- package/src/migrations/0027_model_prices.sql +18 -0
- package/src/migrations/0028_evals.sql +105 -0
- package/src/migrations/0029_model_purposes.sql +18 -0
- package/src/migrations/0030_plugin_installs.sql +73 -0
- package/src/migrations/0031_plugin_snapshot_source.sql +6 -0
- package/src/migrations/0032_plugin_deployment_configs.sql +10 -0
- package/src/migrations/0033_rename_apps_to_connectors.sql +25 -0
- package/src/migrations/0034_directory.sql +49 -0
- package/src/migrations/0035_catalogs.sql +40 -0
- package/src/migrations/0036_app_import_staging.sql +17 -0
- package/src/migrations/0037_directory_installs.sql +92 -0
- package/src/migrations/0038_channel_inbound_mode.sql +6 -0
- package/src/migrations/0039_channel_card_pending.sql +8 -0
- package/src/migrations/0040_usage_report_sweep.sql +10 -0
- package/src/migrations/0041_rename_plugins_to_extensions.sql +38 -0
- package/src/migrations/0042_rename_directory_to_catalog.sql +80 -0
- package/src/migrations/0043_rename_app_installs_to_plugin_installs.sql +20 -0
- package/src/migrations/0044_catalog_one_kind.sql +90 -0
- package/src/migrations/0045_plugin_dependencies.sql +9 -0
- package/src/migrations/0046_git_sources.sql +25 -0
- package/src/migrations/0047_marketplace_entry_heads.sql +7 -0
- package/src/migrations/0048_marketplace_entry_trees.sql +8 -0
- package/src/migrations/0049_plugin_upgrades.sql +37 -0
- package/src/migrations/0050_bootstrap.sql +53 -0
- package/src/migrations/0051_claude_compat.sql +26 -0
- package/src/migrations/0052_conversations_account_agent_idx.sql +5 -0
- package/src/migrations/0053_subagents.sql +73 -0
- package/src/migrations/0054_subagent_wait_expiry.sql +7 -0
- package/src/migrations/0055_subagent_parent_indexes.sql +10 -0
- package/src/migrations/0056_approval_reason.sql +3 -0
- package/src/migrations/0057_conversation_titles.sql +11 -0
- package/src/migrations/0058_conversation_selection.sql +4 -0
- package/src/migrations/0059_conversation_queue.sql +22 -0
- package/src/migrations/0060_file_previewers.sql +10 -0
- package/src/migrations/0061_queue_selection.sql +5 -0
- package/src/migrations/0062_queued_skill.sql +8 -0
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { isChannelSendError, } from "@capybara-hq/plugin-sdk";
|
|
3
|
+
import { DelayedError, Queue, UnrecoverableError, Worker } from "bullmq";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
import { conversationTopic, } from "../conversations/event-log.js";
|
|
6
|
+
import { ApiError } from "../errors.js";
|
|
7
|
+
import { withJobContext } from "../log-context.js";
|
|
8
|
+
import { readOrgSettings } from "../orgs/settings.service.js";
|
|
9
|
+
import { ConversationLock } from "../runtime/conversation-lock.js";
|
|
10
|
+
import { bullConnection } from "../runtime/run-queue.js";
|
|
11
|
+
import { approvalCard, cardMessage, connectUrl, conversationUrl, questionCard, webBase, } from "./cards.js";
|
|
12
|
+
import { phrase } from "./phrases.js";
|
|
13
|
+
/** 渠道队列(出站投递与入站任务,不含 `:`),只在 worker 角色上消费。 */
|
|
14
|
+
export const CHANNEL_QUEUE_NAME = "cap-kernel-channels";
|
|
15
|
+
/** 投递任务名。 */
|
|
16
|
+
export const DELIVER_JOB = "deliver";
|
|
17
|
+
/** 投递任务 id(不含 `:`):同一会话、同一日志位置只排一个任务。 */
|
|
18
|
+
export const deliverJobId = (conversationId, seq) => `deliver-${conversationId}-${seq}`;
|
|
19
|
+
/**
|
|
20
|
+
* 结束后复核排的任务 id:每次唯一(带随机后缀,不含 `:`)。不能复用 `deliver-<会话>-<seq>`——同 id 的任务
|
|
21
|
+
* 可能正因锁被占而即将直接结束,BullMQ 会把新任务去重进它,于是没人投递。
|
|
22
|
+
*/
|
|
23
|
+
export const followUpJobId = (conversationId, seq) => `deliver-${conversationId}-${seq}-f${randomUUID().slice(0, 8)}`;
|
|
24
|
+
/**
|
|
25
|
+
* 发给插件的幂等键(Ruling A15):把逻辑键(如 `<会话>-<seq>`)做 sha256,取 base64url 的前 32 个字符——
|
|
26
|
+
* 稳定、只含 `[A-Za-z0-9_-]`,满足常见平台的长度限制。
|
|
27
|
+
*/
|
|
28
|
+
export function idempotencyKey(raw) {
|
|
29
|
+
return createHash("sha256").update(raw).digest("base64url").slice(0, 32);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 投递锁被占时怎么办:读不到持有者(刚被释放)→ 再取一次;是自己上一次(已崩溃、被 stalled 重新投递)的尝试 →
|
|
33
|
+
* 推迟重试等它过期;别的任务 → 直接结束(它会读到新事件,结束时还会复核)。
|
|
34
|
+
*/
|
|
35
|
+
export function busyLockAction(holder, owner) {
|
|
36
|
+
if (holder === null)
|
|
37
|
+
return "retry";
|
|
38
|
+
return holder.startsWith(owner) ? "requeue" : "drop";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 收尾草稿时写入的文本:`failed` / `stopped` 为「已有文本 + 空行 + 说明」(不知道已有文本时只有说明);
|
|
42
|
+
* `none` 返回 undefined(保留草稿原样,只清掉草稿 id)。
|
|
43
|
+
*/
|
|
44
|
+
export function closingText(prior, how, locale) {
|
|
45
|
+
if (how === "none")
|
|
46
|
+
return undefined;
|
|
47
|
+
const marker = phrase(how === "failed" ? "runFailed" : "runStopped", locale);
|
|
48
|
+
return prior?.trim() ? `${prior}\n\n${marker}` : marker;
|
|
49
|
+
}
|
|
50
|
+
/** 按会话互斥的投递锁(Redis 键前缀;值为 `<job id>.<随机>`,TTL = `runtime.jobLockMs`)。 */
|
|
51
|
+
export const DELIVER_LOCK_PREFIX = "capk:channel-deliver:";
|
|
52
|
+
/** 草稿更新的最小间隔(毫秒);渠道声明了更大的 `features.minEditIntervalMs` 时取后者。 */
|
|
53
|
+
export const DRAFT_INTERVAL_MS = 1000;
|
|
54
|
+
/** 没有新消息时多久复核一次(是否该结束、是否有待刷新的草稿)。 */
|
|
55
|
+
const IDLE_POLL_MS = 500;
|
|
56
|
+
/** 投递锁丢失 / 被自己上一次(已崩溃的)尝试占着时,任务推迟多久再试。 */
|
|
57
|
+
const LOCK_BUSY_DELAY_MS = 1000;
|
|
58
|
+
/** 本副本没有加载连接的插件时,任务推迟多久再试(滚动升级期间由有插件的副本接手)。 */
|
|
59
|
+
const PLUGIN_PENDING_DELAY_MS = 5000;
|
|
60
|
+
/** 插件一直没有在任何副本上加载:任务排进队列这么久之后放弃(之后的新事件会再排任务)。 */
|
|
61
|
+
const PLUGIN_PENDING_MAX_MS = 3_600_000;
|
|
62
|
+
/** 可重试失败(限流、5xx、网络、超时)的总尝试次数。 */
|
|
63
|
+
export const DELIVER_ATTEMPTS = 8;
|
|
64
|
+
/** 同一事件不可重试地失败这么多次后跳过它(Ruling A14)。 */
|
|
65
|
+
export const POISON_FAILURES = 3;
|
|
66
|
+
/** 退避:未给出建议等待时长时 `min(上限, 基数 · 2^(n-1))`。 */
|
|
67
|
+
const BACKOFF_BASE_MS = 1000;
|
|
68
|
+
const BACKOFF_MAX_MS = 60_000;
|
|
69
|
+
/** 补读事件的分页大小。 */
|
|
70
|
+
const PAGE = 200;
|
|
71
|
+
/** `last_error` 的最大长度(同渠道选主)。 */
|
|
72
|
+
const MAX_ERROR_CHARS = 1000;
|
|
73
|
+
/** 入站任务名。 */
|
|
74
|
+
export const INBOUND_JOB = "inbound";
|
|
75
|
+
/**
|
|
76
|
+
* BullMQ 优先级(数字越小越先):入站任务先于出站投递任务——投递任务可能占着并发槽等输出,
|
|
77
|
+
* 用户发来的消息不能排在它们后面。
|
|
78
|
+
*/
|
|
79
|
+
export const INBOUND_PRIORITY = 1;
|
|
80
|
+
export const DELIVER_PRIORITY = 10;
|
|
81
|
+
/** 入站任务的总尝试次数(处理失败由 BullMQ 指数退避重试:1s、2s、4s、8s)。 */
|
|
82
|
+
export const INBOUND_ATTEMPTS = 5;
|
|
83
|
+
const INBOUND_BACKOFF_MS = 1000;
|
|
84
|
+
/**
|
|
85
|
+
* 入站消息的任务 id(不含 `:`):`inbound-<连接>-<sha256(消息 id) 的 base64url 前 22 位>`。同一条消息只排一个任务
|
|
86
|
+
* (已完成的任务保留一段时间,期间重复入队被 BullMQ 忽略)。
|
|
87
|
+
*/
|
|
88
|
+
export const inboundJobId = (connectionId, messageId) => `inbound-${connectionId}-${createHash("sha256").update(messageId).digest("base64url").slice(0, 22)}`;
|
|
89
|
+
/** 可重试的投递失败:带已打码的说明与平台建议的等待时长,供退避策略与最后一次失败时记录。 */
|
|
90
|
+
class RetryableDeliveryError extends Error {
|
|
91
|
+
connectionId;
|
|
92
|
+
retryAfterMs;
|
|
93
|
+
name = "RetryableDeliveryError";
|
|
94
|
+
constructor(connectionId, message, retryAfterMs) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.connectionId = connectionId;
|
|
97
|
+
this.retryAfterMs = retryAfterMs;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** 投递进度被别人推进了(投递锁丢失后另一实例接手):本实例立刻停止并交还任务。 */
|
|
101
|
+
class DeliveryLostError extends Error {
|
|
102
|
+
name = "DeliveryLostError";
|
|
103
|
+
}
|
|
104
|
+
const errMsg = (e) => (e instanceof Error ? e.message : String(e));
|
|
105
|
+
/** 编辑草稿被平台不可重试地拒绝(凭据失效、目标已不存在除外:那两种改发新消息同样会失败)。 */
|
|
106
|
+
const editRejected = (e) => isChannelSendError(e) && !e.retryable && e.reason !== "auth" && e.reason !== "target_gone";
|
|
107
|
+
function isBusMessage(m) {
|
|
108
|
+
if (typeof m !== "object" || m === null)
|
|
109
|
+
return false;
|
|
110
|
+
const k = m.kind;
|
|
111
|
+
if (k === "events")
|
|
112
|
+
return typeof m.toSeq === "number";
|
|
113
|
+
return k === "delta" && typeof m.runId === "string";
|
|
114
|
+
}
|
|
115
|
+
/** BullMQ 退避:平台给了等待时长就用它,否则指数退避。 */
|
|
116
|
+
export function deliverBackoff(attemptsMade, err) {
|
|
117
|
+
if (err instanceof RetryableDeliveryError && err.retryAfterMs !== undefined) {
|
|
118
|
+
return Math.min(BACKOFF_MAX_MS, Math.max(0, err.retryAfterMs));
|
|
119
|
+
}
|
|
120
|
+
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attemptsMade - 1));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 渠道出站投递:把会话日志里 `channel_deliveries.last_seq` 之后的内容按序投到 IM。
|
|
124
|
+
* - 生产方(任意角色):`enqueue(conversationId)` 投递 `deliver-<会话>-<当前 seq>` 任务(会话没有投递目标时什么都不做)。
|
|
125
|
+
* 内核在会话每次追加事件后都调用它(事件日志的 notify 钩子),运行开始时也调用。
|
|
126
|
+
* - Worker(只在 worker 角色):每个任务先取按会话互斥的投递锁,订阅 `conv.<id>` 后补读日志;运行进入 waiting /
|
|
127
|
+
* 终态且日志读完,或追上后 `channels.deliveryIdleMs` 内没有新的总线消息,任务就结束(不长期占并发槽)。
|
|
128
|
+
* 锁被别的任务持有 → 本任务直接结束:持有者会读到新事件,且它结束时(释放锁之后)复核日志是否有新内容,有就再排一个任务;
|
|
129
|
+
* 锁被自己上一次(已崩溃的)尝试占着 → 推迟重试,等锁过期。
|
|
130
|
+
* 崩溃后 BullMQ stalled 机制把任务交给其他副本,从 `last_seq` 续投;每投递一个事件就在同一事务里推进 `last_seq`
|
|
131
|
+
* 并记下 IM 消息 id(草稿 id、卡片消息 id),已投递的不会重发。
|
|
132
|
+
* 审批 / 提问卡片在发送之前先记一行 `pending`(T5a),卡片一出现就被点击也能通过 Ruling A13 的核对;
|
|
133
|
+
* 推进 `last_seq` 的事务再补上消息 id、改为 `sent`。崩溃后续投的副本用同一个幂等键重发。
|
|
134
|
+
* 同一队列还承载入站任务(`inbound`,Ruling A21):由 `enqueueInbound` 排入,worker 调用 `deps.inbound` 处理,
|
|
135
|
+
* 失败按 BullMQ 指数退避重试。
|
|
136
|
+
* 状态全部在 PG / Redis;进程内只有本任务自己的订阅、草稿节流时刻与等待定时器(本进程持有的资源)。
|
|
137
|
+
*/
|
|
138
|
+
export class ChannelOutbound {
|
|
139
|
+
d;
|
|
140
|
+
queue;
|
|
141
|
+
queueConn;
|
|
142
|
+
worker;
|
|
143
|
+
workerConn;
|
|
144
|
+
closed = false;
|
|
145
|
+
/** 停机:进行中的任务在下一个检查点交还队列(由其他副本续投)。 */
|
|
146
|
+
stopping = new AbortController();
|
|
147
|
+
lock;
|
|
148
|
+
constructor(d) {
|
|
149
|
+
this.d = d;
|
|
150
|
+
this.lock = new ConversationLock(d.redis, d.log, DELIVER_LOCK_PREFIX);
|
|
151
|
+
}
|
|
152
|
+
/** 会话有 IM 投递目标时投递一个出站任务(同一日志位置重复调用只排一个);已关闭时什么都不做。 */
|
|
153
|
+
async enqueue(conversationId, opts = {}) {
|
|
154
|
+
if (this.closed)
|
|
155
|
+
return;
|
|
156
|
+
const row = await this.d.db
|
|
157
|
+
.selectFrom("channel_deliveries")
|
|
158
|
+
.innerJoin("conversations", "conversations.id", "channel_deliveries.conversation_id")
|
|
159
|
+
.select("conversations.last_seq")
|
|
160
|
+
.where("channel_deliveries.conversation_id", "=", conversationId)
|
|
161
|
+
.executeTakeFirst();
|
|
162
|
+
if (!row || this.closed)
|
|
163
|
+
return;
|
|
164
|
+
await this.getQueue().add(DELIVER_JOB, { conversationId }, {
|
|
165
|
+
jobId: opts.followUp
|
|
166
|
+
? followUpJobId(conversationId, Number(row.last_seq))
|
|
167
|
+
: deliverJobId(conversationId, Number(row.last_seq)),
|
|
168
|
+
attempts: DELIVER_ATTEMPTS,
|
|
169
|
+
backoff: { type: "deliver" },
|
|
170
|
+
priority: DELIVER_PRIORITY,
|
|
171
|
+
removeOnComplete: true,
|
|
172
|
+
removeOnFail: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 排一个入站任务(Ruling A21,同一队列 `cap-kernel-channels`):处理失败按 {@link INBOUND_ATTEMPTS} 次指数退避重试。
|
|
177
|
+
* 已关闭时抛错(调用方据此让插件不确认、由平台重投)。
|
|
178
|
+
*/
|
|
179
|
+
async enqueueInbound(connectionId, event, jobId) {
|
|
180
|
+
await this.getQueue().add(INBOUND_JOB, { connectionId, event }, {
|
|
181
|
+
jobId,
|
|
182
|
+
attempts: INBOUND_ATTEMPTS,
|
|
183
|
+
priority: INBOUND_PRIORITY,
|
|
184
|
+
backoff: { type: "exponential", delay: INBOUND_BACKOFF_MS },
|
|
185
|
+
removeOnComplete: { age: 3600, count: 1000 },
|
|
186
|
+
removeOnFail: { age: 86_400 },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 连接重新可投递(轮换了凭据 / 重新启用):给该连接投递进度落后于会话日志的会话各排一个出站任务,
|
|
191
|
+
* 补投停用或凭据失效期间落下的内容(不必等下一条消息)。
|
|
192
|
+
*/
|
|
193
|
+
async resumeConnection(connectionId) {
|
|
194
|
+
const rows = await this.d.db
|
|
195
|
+
.selectFrom("channel_deliveries")
|
|
196
|
+
.innerJoin("conversations", "conversations.id", "channel_deliveries.conversation_id")
|
|
197
|
+
.select("channel_deliveries.conversation_id")
|
|
198
|
+
.where("channel_deliveries.connection_id", "=", connectionId)
|
|
199
|
+
.whereRef("channel_deliveries.last_seq", "<", "conversations.last_seq")
|
|
200
|
+
.execute();
|
|
201
|
+
for (const r of rows)
|
|
202
|
+
await this.enqueue(r.conversation_id, { followUp: true });
|
|
203
|
+
}
|
|
204
|
+
/** 启动 Worker(只在含 `worker` 角色的进程)。任务锁与 stalled 检测沿用运行队列的配置。 */
|
|
205
|
+
startWorker() {
|
|
206
|
+
if (this.worker || this.closed)
|
|
207
|
+
return;
|
|
208
|
+
const { jobLockMs, stalledIntervalMs } = this.d.config().runtime;
|
|
209
|
+
this.workerConn = bullConnection(this.d.redisUrl, this.d.log);
|
|
210
|
+
this.worker = new Worker(CHANNEL_QUEUE_NAME, (job, token) => withJobContext(CHANNEL_QUEUE_NAME, job, () => this.process(job, token)), {
|
|
211
|
+
connection: this.workerConn,
|
|
212
|
+
concurrency: this.d.config().channels.deliveryConcurrency,
|
|
213
|
+
lockDuration: jobLockMs,
|
|
214
|
+
stalledInterval: stalledIntervalMs,
|
|
215
|
+
maxStalledCount: 3,
|
|
216
|
+
settings: { backoffStrategy: (n, _type, err) => deliverBackoff(n, err) },
|
|
217
|
+
});
|
|
218
|
+
this.worker.on("error", (err) => this.d.log.error("channel outbound worker error", { err }));
|
|
219
|
+
this.worker.on("failed", (job, err) => {
|
|
220
|
+
if (err instanceof DelayedError || err.name === "DelayedError")
|
|
221
|
+
return;
|
|
222
|
+
this.d.log.warn("channel delivery attempt failed", { jobId: job?.id, error: err.message });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async close() {
|
|
226
|
+
this.closed = true;
|
|
227
|
+
this.stopping.abort();
|
|
228
|
+
const w = this.worker;
|
|
229
|
+
const q = this.queue;
|
|
230
|
+
this.worker = undefined;
|
|
231
|
+
this.queue = undefined;
|
|
232
|
+
await w?.close().catch(() => { });
|
|
233
|
+
await q?.close().catch(() => { });
|
|
234
|
+
for (const c of [this.workerConn, this.queueConn]) {
|
|
235
|
+
if (c)
|
|
236
|
+
await c.quit().catch(() => c.disconnect());
|
|
237
|
+
}
|
|
238
|
+
this.workerConn = undefined;
|
|
239
|
+
this.queueConn = undefined;
|
|
240
|
+
}
|
|
241
|
+
getQueue() {
|
|
242
|
+
if (this.closed)
|
|
243
|
+
throw new Error("channel outbound queue is closed");
|
|
244
|
+
if (!this.queue) {
|
|
245
|
+
this.queueConn = bullConnection(this.d.redisUrl, this.d.log);
|
|
246
|
+
this.queue = new Queue(CHANNEL_QUEUE_NAME, { connection: this.queueConn });
|
|
247
|
+
this.queue.on("error", (err) => this.d.log.error("channel outbound queue error", { err }));
|
|
248
|
+
}
|
|
249
|
+
return this.queue;
|
|
250
|
+
}
|
|
251
|
+
async requeue(job, token, delayMs) {
|
|
252
|
+
await job.moveToDelayed(Date.now() + delayMs, token);
|
|
253
|
+
throw new DelayedError();
|
|
254
|
+
}
|
|
255
|
+
async process(job, token) {
|
|
256
|
+
if (this.stopping.signal.aborted)
|
|
257
|
+
return this.requeue(job, token, 0);
|
|
258
|
+
if (job.name === INBOUND_JOB)
|
|
259
|
+
return this.processInbound(job, token);
|
|
260
|
+
return this.deliver(job, token);
|
|
261
|
+
}
|
|
262
|
+
/** 本副本没有加载连接的插件:推迟重试(不消耗重试次数),排队超过上限才放弃。 */
|
|
263
|
+
async pluginPending(job, token, what) {
|
|
264
|
+
if (Date.now() - job.timestamp > PLUGIN_PENDING_MAX_MS) {
|
|
265
|
+
this.d.log.warn(`channel ${what} dropped: extension not loaded on any replica in time`, {
|
|
266
|
+
jobId: job.id,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
return this.requeue(job, token, PLUGIN_PENDING_DELAY_MS);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 入站任务:失败抛出由 BullMQ 重试;插件在本副本未加载 → 推迟重试(同出站);回复时插件报凭据失效 /
|
|
274
|
+
* 目标已不存在 → 不重试(`UnrecoverableError`)。最后一次也失败时记错误日志(消息丢失,平台已确认过)。
|
|
275
|
+
*/
|
|
276
|
+
async processInbound(job, token) {
|
|
277
|
+
const { connectionId, event } = job.data;
|
|
278
|
+
if (!this.d.inbound)
|
|
279
|
+
throw new Error("channel inbound handler is not configured");
|
|
280
|
+
try {
|
|
281
|
+
await this.d.inbound(connectionId, event);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
if (err instanceof ApiError && err.code === "extension.not_found") {
|
|
285
|
+
return this.pluginPending(job, token, "inbound event");
|
|
286
|
+
}
|
|
287
|
+
if (isChannelSendError(err) &&
|
|
288
|
+
!err.retryable &&
|
|
289
|
+
(err.reason === "auth" || err.reason === "target_gone")) {
|
|
290
|
+
this.d.log.warn("channel inbound reply failed permanently", {
|
|
291
|
+
connectionId,
|
|
292
|
+
type: event.type,
|
|
293
|
+
reason: err.reason,
|
|
294
|
+
});
|
|
295
|
+
throw new UnrecoverableError(`inbound reply failed: ${err.reason}`);
|
|
296
|
+
}
|
|
297
|
+
if (job.attemptsMade + 1 >= (job.opts.attempts ?? 1)) {
|
|
298
|
+
this.d.log.error("channel inbound event dropped after retries", {
|
|
299
|
+
connectionId,
|
|
300
|
+
type: event.type,
|
|
301
|
+
err,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async deliver(job, token) {
|
|
308
|
+
const { conversationId } = job.data;
|
|
309
|
+
const owner = `${job.id ?? "job"}.`;
|
|
310
|
+
const acquire = () => this.lock.acquire(conversationId, `${owner}${randomUUID()}`, this.d.config().runtime.jobLockMs);
|
|
311
|
+
let lease = await acquire();
|
|
312
|
+
if (!lease) {
|
|
313
|
+
const action = busyLockAction(await this.lock.holder(conversationId), owner);
|
|
314
|
+
if (action === "requeue")
|
|
315
|
+
return this.requeue(job, token, LOCK_BUSY_DELAY_MS);
|
|
316
|
+
if (action === "drop")
|
|
317
|
+
return;
|
|
318
|
+
lease = await acquire();
|
|
319
|
+
// 再次被占:取到锁的那个任务会投递
|
|
320
|
+
if (!lease)
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// 本次尝试开始时的日志位置:持锁期间提交的事件,其任务见锁被占直接结束,靠本任务结束时复核补上
|
|
324
|
+
const startSeq = await this.lastSeq(conversationId);
|
|
325
|
+
let outcome;
|
|
326
|
+
try {
|
|
327
|
+
outcome = await new Delivery(this.d, conversationId, lease, this.stopping.signal).run();
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
if (err instanceof RetryableDeliveryError) {
|
|
331
|
+
const last = job.attemptsMade + 1 >= (job.opts.attempts ?? 1);
|
|
332
|
+
if (last)
|
|
333
|
+
await this.recordError(err.connectionId, err.message);
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
await lease
|
|
339
|
+
.release()
|
|
340
|
+
.catch((err) => this.d.log.warn("cannot release channel delivery lock", { conversationId, err }));
|
|
341
|
+
}
|
|
342
|
+
if (outcome.error)
|
|
343
|
+
await this.recordError(outcome.error.connectionId, outcome.error.message);
|
|
344
|
+
switch (outcome.kind) {
|
|
345
|
+
case "shutdown":
|
|
346
|
+
return this.requeue(job, token, 0);
|
|
347
|
+
case "lost":
|
|
348
|
+
// 锁丢失 / 进度被别人推进:交还任务稍后再试(不当作完成,以免最后一段输出无人投递)
|
|
349
|
+
return this.requeue(job, token, LOCK_BUSY_DELAY_MS);
|
|
350
|
+
case "done":
|
|
351
|
+
// 释放锁之后再复核:期间到达、因锁被占而直接结束的任务的事件由这里补上
|
|
352
|
+
if (outcome.seen !== undefined)
|
|
353
|
+
await this.followUp(conversationId, outcome.seen);
|
|
354
|
+
return;
|
|
355
|
+
case "stopped":
|
|
356
|
+
// 不可重试失败:只在本次尝试开始之后又有新事件时复核(那些事件的任务因锁被占已直接结束,
|
|
357
|
+
// 否则「后续事件触发续投」会丢);没有新事件就停下,不立刻重试同一个事件
|
|
358
|
+
if (startSeq !== undefined)
|
|
359
|
+
await this.followUp(conversationId, startSeq);
|
|
360
|
+
return;
|
|
361
|
+
case "changed":
|
|
362
|
+
// 连接改过(轮换凭据等):释放锁之后排新任务,用新的出站会话从 last_seq 接着投
|
|
363
|
+
await this.enqueue(conversationId, { followUp: true });
|
|
364
|
+
return;
|
|
365
|
+
case "pending":
|
|
366
|
+
return this.pluginPending(job, token, "delivery");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** 会话日志当前的最后位置(会话不存在时 undefined)。 */
|
|
370
|
+
async lastSeq(conversationId) {
|
|
371
|
+
const conv = await this.d.db
|
|
372
|
+
.selectFrom("conversations")
|
|
373
|
+
.select("last_seq")
|
|
374
|
+
.where("id", "=", conversationId)
|
|
375
|
+
.executeTakeFirst();
|
|
376
|
+
return conv ? Number(conv.last_seq) : undefined;
|
|
377
|
+
}
|
|
378
|
+
/** 日志比 `seen` 更新时再排一个任务。 */
|
|
379
|
+
async followUp(conversationId, seen) {
|
|
380
|
+
const last = await this.lastSeq(conversationId);
|
|
381
|
+
if (last !== undefined && last > seen)
|
|
382
|
+
await this.enqueue(conversationId, { followUp: true });
|
|
383
|
+
}
|
|
384
|
+
/** 不可重试(或重试用尽)的发送失败:记连接 `last_error`(已打码)。 */
|
|
385
|
+
async recordError(connectionId, message) {
|
|
386
|
+
const text = `delivery failed: ${message}`.slice(0, MAX_ERROR_CHARS);
|
|
387
|
+
this.d.log.warn("channel delivery failed", { connectionId, error: text });
|
|
388
|
+
await this.d.db
|
|
389
|
+
.updateTable("channel_connections")
|
|
390
|
+
.set({ last_error: text })
|
|
391
|
+
.where("id", "=", connectionId)
|
|
392
|
+
.execute()
|
|
393
|
+
.catch((err) => this.d.log.warn("cannot record channel delivery error", { err }));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** 一次投递任务(本进程持有):订阅、补读、草稿节流。 */
|
|
397
|
+
class Delivery {
|
|
398
|
+
d;
|
|
399
|
+
conversationId;
|
|
400
|
+
lease;
|
|
401
|
+
stopping;
|
|
402
|
+
inbox = [];
|
|
403
|
+
wake;
|
|
404
|
+
session;
|
|
405
|
+
target;
|
|
406
|
+
locale = "en";
|
|
407
|
+
base;
|
|
408
|
+
/** 已处理到的事件 seq(内存)与已写入 PG 的 seq。 */
|
|
409
|
+
seen = 0;
|
|
410
|
+
persisted = 0;
|
|
411
|
+
/** 正在投递的事件(不可重试失败按它计数)。 */
|
|
412
|
+
handling;
|
|
413
|
+
/** 当前草稿消息 id(PG 里的 `draft_message_id`)与它所属的步骤(从 PG 读出的草稿不知道步骤)。 */
|
|
414
|
+
draftId = null;
|
|
415
|
+
draftKey;
|
|
416
|
+
/** 草稿当前显示的文本(从 PG 读出的草稿不知道)。 */
|
|
417
|
+
draftText;
|
|
418
|
+
lastDraftAt = 0;
|
|
419
|
+
streaming;
|
|
420
|
+
dirty = false;
|
|
421
|
+
/** 任务开始时连接的 `updated_at`(毫秒)与上次复核连接的时刻。 */
|
|
422
|
+
version = 0;
|
|
423
|
+
checkedAt = 0;
|
|
424
|
+
/** 平台不可重试地拒绝了对草稿的编辑:不再编辑它,定稿改发新消息(Ruling A22)。 */
|
|
425
|
+
draftBroken = false;
|
|
426
|
+
/** 已定稿的步骤:之后到达的增量丢弃。 */
|
|
427
|
+
finalized = new Set();
|
|
428
|
+
constructor(d, conversationId, lease, stopping) {
|
|
429
|
+
this.d = d;
|
|
430
|
+
this.conversationId = conversationId;
|
|
431
|
+
this.lease = lease;
|
|
432
|
+
this.stopping = stopping;
|
|
433
|
+
}
|
|
434
|
+
get s() {
|
|
435
|
+
return this.session;
|
|
436
|
+
}
|
|
437
|
+
/** 普通消息的幂等键(Ruling A15):`<会话>-<seq>[后缀]` 的稳定哈希。 */
|
|
438
|
+
key(seq, suffix = "") {
|
|
439
|
+
return idempotencyKey(`${this.conversationId}-${seq}${suffix}`);
|
|
440
|
+
}
|
|
441
|
+
async run() {
|
|
442
|
+
const row = await this.d.db
|
|
443
|
+
.selectFrom("channel_deliveries")
|
|
444
|
+
.select(["connection_id", "target", "last_seq", "draft_message_id"])
|
|
445
|
+
.where("conversation_id", "=", this.conversationId)
|
|
446
|
+
.executeTakeFirst();
|
|
447
|
+
if (!row)
|
|
448
|
+
return { kind: "done" };
|
|
449
|
+
// 先订阅再补读:两者之间提交的事件不会漏掉
|
|
450
|
+
const unsubscribe = await this.d.bus.subscribe(conversationTopic(this.conversationId), (m) => {
|
|
451
|
+
if (!isBusMessage(m))
|
|
452
|
+
return;
|
|
453
|
+
this.inbox.push(m);
|
|
454
|
+
this.wake?.();
|
|
455
|
+
});
|
|
456
|
+
try {
|
|
457
|
+
try {
|
|
458
|
+
this.session = await this.d.connections.outbound(row.connection_id);
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
// 插件在本副本未加载(滚动升级期间新插件只在部分副本上):推迟重试,留给有插件的副本
|
|
462
|
+
if (err instanceof ApiError && err.code === "extension.not_found")
|
|
463
|
+
return { kind: "pending" };
|
|
464
|
+
// 连接已删除 / 插件在该组织停用 / 配置不再合法:没有可投递的地方
|
|
465
|
+
if (err instanceof ApiError) {
|
|
466
|
+
this.d.log.warn("channel delivery skipped", {
|
|
467
|
+
conversationId: this.conversationId,
|
|
468
|
+
code: err.code,
|
|
469
|
+
});
|
|
470
|
+
return { kind: "done" };
|
|
471
|
+
}
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
if (this.s.row.status !== "active")
|
|
475
|
+
return { kind: "done" };
|
|
476
|
+
const version = await this.connectionVersion();
|
|
477
|
+
if (version === undefined)
|
|
478
|
+
return { kind: "done" };
|
|
479
|
+
this.version = version;
|
|
480
|
+
this.target = row.target;
|
|
481
|
+
this.seen = Number(row.last_seq);
|
|
482
|
+
this.persisted = this.seen;
|
|
483
|
+
this.draftId = row.draft_message_id;
|
|
484
|
+
this.locale = (await readOrgSettings(this.d.db, this.s.row.orgId)).locale;
|
|
485
|
+
this.base = webBase(this.d.config().http.publicUrl);
|
|
486
|
+
return await this.loop();
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
if (err instanceof DeliveryLostError)
|
|
490
|
+
return { kind: "lost" };
|
|
491
|
+
if (isChannelSendError(err) && !err.retryable) {
|
|
492
|
+
const message = this.redact(errMsg(err));
|
|
493
|
+
// Ruling A22:机器人被移出群 / 会话不存在 → 停止本轮,不计毒事件(不是连接的问题,不记 last_error)
|
|
494
|
+
if (err.reason === "target_gone") {
|
|
495
|
+
this.d.log.warn("channel delivery target is gone", {
|
|
496
|
+
conversationId: this.conversationId,
|
|
497
|
+
error: message,
|
|
498
|
+
});
|
|
499
|
+
return { kind: "stopped" };
|
|
500
|
+
}
|
|
501
|
+
const error = { connectionId: row.connection_id, message };
|
|
502
|
+
// 凭据失效:记 last_error、停止本轮,不计毒事件(换好凭据后之后的事件照常续投)
|
|
503
|
+
if (err.reason === "auth")
|
|
504
|
+
return { kind: "stopped", error };
|
|
505
|
+
if (this.handling !== undefined && (await this.countFailure(this.handling))) {
|
|
506
|
+
return { kind: "done", seen: this.seen, error };
|
|
507
|
+
}
|
|
508
|
+
return { kind: "stopped", error };
|
|
509
|
+
}
|
|
510
|
+
if (this.session === undefined)
|
|
511
|
+
throw err;
|
|
512
|
+
throw new RetryableDeliveryError(row.connection_id, this.redact(errMsg(err)), isChannelSendError(err) ? err.retryAfterMs : undefined);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
await unsubscribe().catch((err) => this.d.log.warn("channel delivery unsubscribe failed", { err }));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
redact(s) {
|
|
519
|
+
return this.session ? this.session.redact(s) : s;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 记一次不可重试的失败(按事件 seq 计数,Ruling A14)。同一事件失败满 {@link POISON_FAILURES} 次:
|
|
523
|
+
* 跳过它(推进 `last_seq` 越过它、清掉草稿),尽力发一句「请到网页上查看」,返回 true。
|
|
524
|
+
*/
|
|
525
|
+
async countFailure(seq) {
|
|
526
|
+
const r = await this.d.db
|
|
527
|
+
.updateTable("channel_deliveries")
|
|
528
|
+
.set({
|
|
529
|
+
fail_count: sql `CASE WHEN fail_seq = ${seq} THEN fail_count + 1 ELSE 1 END`,
|
|
530
|
+
fail_seq: seq,
|
|
531
|
+
})
|
|
532
|
+
.where("conversation_id", "=", this.conversationId)
|
|
533
|
+
.where("last_seq", "=", String(this.persisted))
|
|
534
|
+
.returning("fail_count")
|
|
535
|
+
.executeTakeFirst();
|
|
536
|
+
if (!r || r.fail_count < POISON_FAILURES)
|
|
537
|
+
return false;
|
|
538
|
+
this.d.log.warn("channel delivery skipped an event that keeps failing", {
|
|
539
|
+
conversationId: this.conversationId,
|
|
540
|
+
seq,
|
|
541
|
+
});
|
|
542
|
+
// 尽力收尾挂着的草稿(同运行失败),再提示去网页查看;两者失败都不影响跳过
|
|
543
|
+
try {
|
|
544
|
+
await this.markDraft(seq, "failed");
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
this.d.log.warn("cannot close channel draft before skipping", { err: errMsg(err) });
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
await this.send({ text: phrase("handleOnWeb", this.locale) }, this.key(seq, "-skipped"));
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
this.d.log.warn("cannot send channel skip notice", { err: errMsg(err) });
|
|
554
|
+
}
|
|
555
|
+
// A card row recorded for the skipped event deliberately stays `pending`: the send may have
|
|
556
|
+
// reached the platform even though it failed for us, so a click on it must still be accepted.
|
|
557
|
+
// That is safe: Ruling A13 (same connection and chat), the plugin's signature checks and the
|
|
558
|
+
// approver / asker authorization in decide / answer still apply.
|
|
559
|
+
await this.advance(seq, { draft: null });
|
|
560
|
+
this.seen = seq;
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
/** 连接的 `updated_at`(毫秒);连接已停用 / 删除时为 undefined。 */
|
|
564
|
+
async connectionVersion() {
|
|
565
|
+
const r = await this.d.db
|
|
566
|
+
.selectFrom("channel_connections")
|
|
567
|
+
.select(["status", "updated_at"])
|
|
568
|
+
.where("id", "=", this.s.row.id)
|
|
569
|
+
.where("deleted_at", "is", null)
|
|
570
|
+
.executeTakeFirst();
|
|
571
|
+
return r?.status === "active" ? r.updated_at.getTime() : undefined;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 至多每 {@link IDLE_POLL_MS} 复核一次连接:停用 / 删除 → `done`(不复核、不再投递);改过(轮换凭据等,
|
|
575
|
+
* `updated_at` 变了)→ `changed`(释放锁后排新任务,用新的出站会话接着投);没变返回 undefined。
|
|
576
|
+
*/
|
|
577
|
+
async recheckConnection() {
|
|
578
|
+
if (Date.now() - this.checkedAt < IDLE_POLL_MS)
|
|
579
|
+
return undefined;
|
|
580
|
+
this.checkedAt = Date.now();
|
|
581
|
+
const v = await this.connectionVersion();
|
|
582
|
+
if (v === undefined)
|
|
583
|
+
return { kind: "done" };
|
|
584
|
+
if (v !== this.version)
|
|
585
|
+
return { kind: "changed" };
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
async loop() {
|
|
589
|
+
const idleMs = this.d.config().channels.deliveryIdleMs;
|
|
590
|
+
let lastMessageAt = Date.now();
|
|
591
|
+
let needCatchUp = true;
|
|
592
|
+
this.checkedAt = Date.now();
|
|
593
|
+
for (;;) {
|
|
594
|
+
if (this.lease.lost)
|
|
595
|
+
return { kind: "lost" };
|
|
596
|
+
if (this.stopping.aborted)
|
|
597
|
+
return { kind: "shutdown" };
|
|
598
|
+
const changed = await this.recheckConnection();
|
|
599
|
+
if (changed)
|
|
600
|
+
return changed;
|
|
601
|
+
if (needCatchUp) {
|
|
602
|
+
await this.catchUp();
|
|
603
|
+
needCatchUp = false;
|
|
604
|
+
}
|
|
605
|
+
while (this.inbox.length > 0) {
|
|
606
|
+
if (this.lease.lost)
|
|
607
|
+
return { kind: "lost" };
|
|
608
|
+
const m = this.inbox.shift();
|
|
609
|
+
if (m.kind === "events") {
|
|
610
|
+
if (m.toSeq > this.seen)
|
|
611
|
+
await this.catchUp();
|
|
612
|
+
}
|
|
613
|
+
else if (m.kind === "delta") {
|
|
614
|
+
this.onDelta(m);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
await this.flushDraft();
|
|
618
|
+
if (await this.waitWake(IDLE_POLL_MS)) {
|
|
619
|
+
lastMessageAt = Date.now();
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
needCatchUp = true;
|
|
623
|
+
const quiet = Date.now() - lastMessageAt >= idleMs;
|
|
624
|
+
if (quiet || (await this.idle())) {
|
|
625
|
+
await this.catchUp();
|
|
626
|
+
needCatchUp = false;
|
|
627
|
+
if (this.inbox.length > 0)
|
|
628
|
+
continue;
|
|
629
|
+
// 运行进入 waiting / 终态且读完;或已追上、长时间没有新输出(之后的事件会排新任务)
|
|
630
|
+
if (quiet || (await this.idle()))
|
|
631
|
+
return { kind: "done", seen: this.seen };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/** 运行已进入 waiting / 终态(没有 queued / running 的运行),且日志已读完。 */
|
|
636
|
+
async idle() {
|
|
637
|
+
const active = await this.d.db
|
|
638
|
+
.selectFrom("runs")
|
|
639
|
+
.select("id")
|
|
640
|
+
.where("conversation_id", "=", this.conversationId)
|
|
641
|
+
.where("status", "in", ["queued", "running"])
|
|
642
|
+
.executeTakeFirst();
|
|
643
|
+
if (active)
|
|
644
|
+
return false;
|
|
645
|
+
const conv = await this.d.db
|
|
646
|
+
.selectFrom("conversations")
|
|
647
|
+
.select("last_seq")
|
|
648
|
+
.where("id", "=", this.conversationId)
|
|
649
|
+
.executeTakeFirst();
|
|
650
|
+
return !conv || Number(conv.last_seq) <= this.seen;
|
|
651
|
+
}
|
|
652
|
+
waitWake(ms) {
|
|
653
|
+
if (this.inbox.length > 0)
|
|
654
|
+
return Promise.resolve(true);
|
|
655
|
+
return new Promise((resolve) => {
|
|
656
|
+
const done = (v) => {
|
|
657
|
+
clearTimeout(timer);
|
|
658
|
+
this.stopping.removeEventListener("abort", onAbort);
|
|
659
|
+
this.wake = undefined;
|
|
660
|
+
resolve(v);
|
|
661
|
+
};
|
|
662
|
+
const onAbort = () => done(false);
|
|
663
|
+
const timer = setTimeout(() => done(false), ms);
|
|
664
|
+
this.wake = () => done(true);
|
|
665
|
+
this.stopping.addEventListener("abort", onAbort, { once: true });
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/** 读 `seen` 之后的全部事件并逐个投递;只是跳过的事件最后统一推进一次进度。 */
|
|
669
|
+
async catchUp() {
|
|
670
|
+
for (;;) {
|
|
671
|
+
const evs = await this.d.eventLog.readAfter(this.conversationId, this.seen, PAGE);
|
|
672
|
+
for (const e of evs) {
|
|
673
|
+
if (e.seq <= this.seen)
|
|
674
|
+
continue;
|
|
675
|
+
if (this.lease.lost)
|
|
676
|
+
throw new DeliveryLostError();
|
|
677
|
+
this.handling = e.seq;
|
|
678
|
+
await this.handle(e);
|
|
679
|
+
this.handling = undefined;
|
|
680
|
+
this.seen = e.seq;
|
|
681
|
+
}
|
|
682
|
+
if (evs.length < PAGE)
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
if (this.seen > this.persisted)
|
|
686
|
+
await this.advance(this.seen);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* 推进 `last_seq`(同一事务记下草稿 / 卡片消息 id,清零失败计数)。以「仍是我上次写入的值」为条件:
|
|
690
|
+
* 被别人推进过(投递锁丢失后另一实例接手)就停止,不再重复发送。
|
|
691
|
+
*/
|
|
692
|
+
async advance(seq, opts = {}) {
|
|
693
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
694
|
+
const r = await t
|
|
695
|
+
.updateTable("channel_deliveries")
|
|
696
|
+
.set({
|
|
697
|
+
last_seq: String(seq),
|
|
698
|
+
fail_seq: null,
|
|
699
|
+
fail_count: 0,
|
|
700
|
+
...(opts.draft !== undefined && { draft_message_id: opts.draft }),
|
|
701
|
+
updated_at: new Date(),
|
|
702
|
+
})
|
|
703
|
+
.where("conversation_id", "=", this.conversationId)
|
|
704
|
+
.where("last_seq", "=", String(this.persisted))
|
|
705
|
+
.executeTakeFirst();
|
|
706
|
+
if (r.numUpdatedRows === 0n)
|
|
707
|
+
throw new DeliveryLostError();
|
|
708
|
+
if (opts.card) {
|
|
709
|
+
// The row was recorded as `pending` before the send (recordCard); fill in the message id
|
|
710
|
+
const messageId = opts.card.messageId;
|
|
711
|
+
await t
|
|
712
|
+
.insertInto("channel_card_messages")
|
|
713
|
+
.values({
|
|
714
|
+
conversation_id: this.conversationId,
|
|
715
|
+
connection_id: this.s.row.id,
|
|
716
|
+
card_kind: opts.card.kind,
|
|
717
|
+
card_id: opts.card.id,
|
|
718
|
+
message_id: messageId,
|
|
719
|
+
status: "sent",
|
|
720
|
+
target: JSON.stringify(this.target),
|
|
721
|
+
})
|
|
722
|
+
.onConflict((oc) => oc.columns(["conversation_id", "connection_id", "card_kind", "card_id"]).doUpdateSet({
|
|
723
|
+
message_id: messageId,
|
|
724
|
+
status: "sent",
|
|
725
|
+
target: JSON.stringify(this.target),
|
|
726
|
+
}))
|
|
727
|
+
.execute();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
this.persisted = seq;
|
|
731
|
+
if (opts.draft !== undefined) {
|
|
732
|
+
this.draftId = opts.draft;
|
|
733
|
+
if (opts.draft === null) {
|
|
734
|
+
this.draftKey = undefined;
|
|
735
|
+
this.draftText = undefined;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Record a card as `pending` before it is sent (T5a), in its own statement, so a click that arrives
|
|
741
|
+
* as soon as the card shows up on the platform finds it (Ruling A13: the card must be one this
|
|
742
|
+
* connection sent in this chat). The send's message id is filled in by {@link advance}. A row left
|
|
743
|
+
* `pending` by a failed send or a dead replica is kept: the event is handled again from `last_seq`
|
|
744
|
+
* and the card resent with the same idempotency key. An existing `sent` row is never touched.
|
|
745
|
+
*/
|
|
746
|
+
async recordCard(kind, id) {
|
|
747
|
+
// Best-effort guard only (the lease can be lost right after it): correctness rests on
|
|
748
|
+
// ON CONFLICT DO NOTHING below and on advance() being conditional on last_seq
|
|
749
|
+
if (this.lease.lost)
|
|
750
|
+
throw new DeliveryLostError();
|
|
751
|
+
await this.d.db
|
|
752
|
+
.insertInto("channel_card_messages")
|
|
753
|
+
.values({
|
|
754
|
+
conversation_id: this.conversationId,
|
|
755
|
+
connection_id: this.s.row.id,
|
|
756
|
+
card_kind: kind,
|
|
757
|
+
card_id: id,
|
|
758
|
+
message_id: null,
|
|
759
|
+
status: "pending",
|
|
760
|
+
target: JSON.stringify(this.target),
|
|
761
|
+
})
|
|
762
|
+
.onConflict((oc) => oc.columns(["conversation_id", "connection_id", "card_kind", "card_id"]).doNothing())
|
|
763
|
+
.execute();
|
|
764
|
+
}
|
|
765
|
+
send(message, idempotencyKey) {
|
|
766
|
+
return this.s.send(this.target, message, idempotencyKey);
|
|
767
|
+
}
|
|
768
|
+
async handle(e) {
|
|
769
|
+
switch (e.type) {
|
|
770
|
+
case "assistant_message":
|
|
771
|
+
return this.finalize(e.runId, e.payload.step, e.payload.content, e.seq);
|
|
772
|
+
case "approval_requested":
|
|
773
|
+
return this.approvalCardAt(e.seq, e.payload.approvalId);
|
|
774
|
+
case "question_asked":
|
|
775
|
+
return this.questionCardAt(e.seq, e.payload.questionId, e.payload.form);
|
|
776
|
+
case "subagent_waiting": {
|
|
777
|
+
// a sub-agent's approval or question (sub-agents spec §9): the same card as the parent's
|
|
778
|
+
// own, by id, so a conversation started from IM gets its sub-agents' requests in the thread.
|
|
779
|
+
// A connect card stays on the web (the child got the connect text as its result).
|
|
780
|
+
const p = e.payload;
|
|
781
|
+
// the child's earlier requests may be decided by now: bring their cards up to date first
|
|
782
|
+
// (the decisions are in the child's log, see subagent_finished below)
|
|
783
|
+
if (p.kind !== "connect")
|
|
784
|
+
await this.refreshChildCards(p.childRunId);
|
|
785
|
+
if (p.kind === "approval" && p.approvalId !== undefined) {
|
|
786
|
+
return this.approvalCardAt(e.seq, p.approvalId);
|
|
787
|
+
}
|
|
788
|
+
if (p.kind === "input" && p.questionId !== undefined) {
|
|
789
|
+
const q = await this.d.db
|
|
790
|
+
.selectFrom("questions")
|
|
791
|
+
.select("spec")
|
|
792
|
+
.where("id", "=", p.questionId)
|
|
793
|
+
.executeTakeFirst();
|
|
794
|
+
const form = q?.spec?.form;
|
|
795
|
+
if (!form)
|
|
796
|
+
return;
|
|
797
|
+
return this.questionCardAt(e.seq, p.questionId, form, p.subagentId);
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
case "subagent_finished":
|
|
802
|
+
// the child's decisions were logged in the child's log, which this delivery does not read:
|
|
803
|
+
// its cards in this thread are brought up to date once it is done
|
|
804
|
+
if (await this.refreshChildCards(e.payload.childRunId))
|
|
805
|
+
return this.advance(e.seq);
|
|
806
|
+
return;
|
|
807
|
+
case "connection_required": {
|
|
808
|
+
const text = phrase("connectTitle", this.locale);
|
|
809
|
+
await this.send(this.base === undefined
|
|
810
|
+
? { text: phrase("handleOnWeb", this.locale) }
|
|
811
|
+
: cardMessage(text, {
|
|
812
|
+
kind: "connect",
|
|
813
|
+
appName: e.payload.appName,
|
|
814
|
+
url: connectUrl(this.base, e.payload.appId),
|
|
815
|
+
}), this.key(e.seq));
|
|
816
|
+
return this.advance(e.seq);
|
|
817
|
+
}
|
|
818
|
+
case "approval_decided": {
|
|
819
|
+
const at = await this.cardMessageOf("approval", e.payload.approvalId);
|
|
820
|
+
if (!at || this.base === undefined)
|
|
821
|
+
return;
|
|
822
|
+
const card = await approvalCard(this.d, e.payload.approvalId, this.base, this.locale, this.s.redact);
|
|
823
|
+
if (!card)
|
|
824
|
+
return;
|
|
825
|
+
await this.replaceCard(at, cardMessage(card.title, card), idempotencyKey(`${this.conversationId}-approval-${card.id}-${card.state}`));
|
|
826
|
+
return this.advance(e.seq);
|
|
827
|
+
}
|
|
828
|
+
case "question_answered": {
|
|
829
|
+
const at = await this.cardMessageOf("question", e.payload.questionId);
|
|
830
|
+
if (!at || this.base === undefined || e.runId === null)
|
|
831
|
+
return;
|
|
832
|
+
const q = (await this.d.questions.forCall(e.runId, e.payload.callId)).find((x) => x.id === e.payload.questionId);
|
|
833
|
+
if (!q)
|
|
834
|
+
return;
|
|
835
|
+
const card = questionCard(q.id, q.form, e.payload.outcome, conversationUrl(this.base, this.conversationId), e.payload.answer);
|
|
836
|
+
await this.replaceCard(at, cardMessage(phrase("questionTitle", this.locale), card), idempotencyKey(`${this.conversationId}-question-${card.id}-${card.state}`));
|
|
837
|
+
return this.advance(e.seq);
|
|
838
|
+
}
|
|
839
|
+
case "run_failed":
|
|
840
|
+
// 有草稿:收尾为「已有文本 + 失败说明」;没有草稿:单发一句
|
|
841
|
+
if (await this.closeDraft(e.seq, "failed"))
|
|
842
|
+
return;
|
|
843
|
+
await this.send({ text: phrase("runFailed", this.locale) }, this.key(e.seq));
|
|
844
|
+
return this.advance(e.seq);
|
|
845
|
+
case "run_aborted":
|
|
846
|
+
// 中止时还挂着的草稿:加「已停止」并清掉,避免下一次回复去编辑它
|
|
847
|
+
await this.closeDraft(e.seq, "stopped");
|
|
848
|
+
return;
|
|
849
|
+
case "run_finished":
|
|
850
|
+
// 正常结束却还挂着草稿(没有等到定稿):草稿原样保留,只清掉 id
|
|
851
|
+
await this.closeDraft(e.seq, "none");
|
|
852
|
+
return;
|
|
853
|
+
default:
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** Sends the approval card of `approvalId` for the event at `seq` (or the web hint). */
|
|
858
|
+
async approvalCardAt(seq, approvalId) {
|
|
859
|
+
if (this.base === undefined) {
|
|
860
|
+
await this.send({ text: phrase("handleOnWeb", this.locale) }, this.key(seq));
|
|
861
|
+
return this.advance(seq);
|
|
862
|
+
}
|
|
863
|
+
const card = await approvalCard(this.d, approvalId, this.base, this.locale, this.s.redact);
|
|
864
|
+
if (!card)
|
|
865
|
+
return;
|
|
866
|
+
await this.recordCard("approval", card.id);
|
|
867
|
+
const { messageId } = await this.send(cardMessage(card.title, card), this.key(seq));
|
|
868
|
+
return this.advance(seq, { card: { kind: "approval", id: card.id, messageId } });
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Sends the (pending) question card of `questionId` for the event at `seq` (or the web hint). A
|
|
872
|
+
* sub-agent's question (`child` = its conversation) links to the parent with the child open, as
|
|
873
|
+
* its approvals' `conversationPath` does.
|
|
874
|
+
*/
|
|
875
|
+
async questionCardAt(seq, questionId, form, child) {
|
|
876
|
+
if (this.base === undefined) {
|
|
877
|
+
await this.send({ text: phrase("handleOnWeb", this.locale) }, this.key(seq));
|
|
878
|
+
return this.advance(seq);
|
|
879
|
+
}
|
|
880
|
+
const card = questionCard(questionId, form, "pending", this.questionUrl(this.base, child));
|
|
881
|
+
await this.recordCard("question", card.id);
|
|
882
|
+
const { messageId } = await this.send(cardMessage(phrase("questionTitle", this.locale), card), this.key(seq));
|
|
883
|
+
return this.advance(seq, { card: { kind: "question", id: card.id, messageId } });
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Updates the cards this connection sent in this conversation for a finished sub-agent's
|
|
887
|
+
* approvals and questions to their final state, with the idempotency keys the parent's own
|
|
888
|
+
* `approval_decided` / `question_answered` updates use. Returns whether any card was updated.
|
|
889
|
+
*/
|
|
890
|
+
async refreshChildCards(childRunId) {
|
|
891
|
+
if (this.base === undefined)
|
|
892
|
+
return false;
|
|
893
|
+
const [approvals, questions] = await Promise.all([
|
|
894
|
+
this.d.db
|
|
895
|
+
.selectFrom("channel_card_messages as m")
|
|
896
|
+
.innerJoin("approvals as a", "a.id", "m.card_id")
|
|
897
|
+
.select("a.id")
|
|
898
|
+
.where("m.conversation_id", "=", this.conversationId)
|
|
899
|
+
.where("m.connection_id", "=", this.s.row.id)
|
|
900
|
+
.where("m.card_kind", "=", "approval")
|
|
901
|
+
.where("a.run_id", "=", childRunId)
|
|
902
|
+
.where("a.status", "!=", "pending")
|
|
903
|
+
.execute(),
|
|
904
|
+
this.d.db
|
|
905
|
+
.selectFrom("channel_card_messages as m")
|
|
906
|
+
.innerJoin("questions as q", "q.id", "m.card_id")
|
|
907
|
+
.select(["q.id", "q.call_id", "q.conversation_id"])
|
|
908
|
+
.where("m.conversation_id", "=", this.conversationId)
|
|
909
|
+
.where("m.connection_id", "=", this.s.row.id)
|
|
910
|
+
.where("m.card_kind", "=", "question")
|
|
911
|
+
.where("q.run_id", "=", childRunId)
|
|
912
|
+
.where("q.status", "!=", "pending")
|
|
913
|
+
.execute(),
|
|
914
|
+
]);
|
|
915
|
+
let updated = false;
|
|
916
|
+
for (const a of approvals) {
|
|
917
|
+
const at = await this.cardMessageOf("approval", a.id);
|
|
918
|
+
if (!at)
|
|
919
|
+
continue;
|
|
920
|
+
const card = await approvalCard(this.d, a.id, this.base, this.locale, this.s.redact);
|
|
921
|
+
if (!card)
|
|
922
|
+
continue;
|
|
923
|
+
await this.replaceCard(at, cardMessage(card.title, card), idempotencyKey(`${this.conversationId}-approval-${card.id}-${card.state}`));
|
|
924
|
+
updated = true;
|
|
925
|
+
}
|
|
926
|
+
for (const row of questions) {
|
|
927
|
+
const at = await this.cardMessageOf("question", row.id);
|
|
928
|
+
if (!at)
|
|
929
|
+
continue;
|
|
930
|
+
const q = (await this.d.questions.forCall(childRunId, row.call_id)).find((x) => x.id === row.id);
|
|
931
|
+
if (!q || q.status === "pending")
|
|
932
|
+
continue;
|
|
933
|
+
const card = questionCard(q.id, q.form, q.status, this.questionUrl(this.base, row.conversation_id), q.answer);
|
|
934
|
+
await this.replaceCard(at, cardMessage(phrase("questionTitle", this.locale), card), idempotencyKey(`${this.conversationId}-question-${card.id}-${card.state}`));
|
|
935
|
+
updated = true;
|
|
936
|
+
}
|
|
937
|
+
return updated;
|
|
938
|
+
}
|
|
939
|
+
/** The web link of a question card: this conversation, with the sub-agent `child` open. */
|
|
940
|
+
questionUrl(base, child) {
|
|
941
|
+
const url = conversationUrl(base, this.conversationId);
|
|
942
|
+
return child === undefined ? url : `${url}?subagent=${encodeURIComponent(child)}`;
|
|
943
|
+
}
|
|
944
|
+
/** 按 {@link closingText} 改写挂着的草稿消息(`none` 不改);不推进进度。没有草稿返回 false。 */
|
|
945
|
+
async markDraft(seq, how) {
|
|
946
|
+
if (this.draftId === null)
|
|
947
|
+
return false;
|
|
948
|
+
this.streaming = undefined;
|
|
949
|
+
this.dirty = false;
|
|
950
|
+
const text = closingText(this.draftText, how, this.locale);
|
|
951
|
+
if (text !== undefined && this.s.features.edit) {
|
|
952
|
+
if (this.draftBroken) {
|
|
953
|
+
// 草稿已不能编辑:说明作为新消息发出,不静默丢掉
|
|
954
|
+
const marker = phrase(how === "failed" ? "runFailed" : "runStopped", this.locale);
|
|
955
|
+
await this.send({ text: marker }, this.key(seq, `-close-${how}`));
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
await this.s.update(this.target, this.draftId, { text }, this.key(seq, `-close-${how}`));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
/** 收尾挂着的草稿(见 {@link markDraft})并清掉草稿 id。没有草稿返回 false。 */
|
|
964
|
+
async closeDraft(seq, how) {
|
|
965
|
+
if (!(await this.markDraft(seq, how)))
|
|
966
|
+
return false;
|
|
967
|
+
await this.advance(seq, { draft: null });
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* 已发出的卡片消息;不是本连接发的(或在投递目标建立之前)返回 undefined。
|
|
972
|
+
* A row without `message_id` has no message to update. Events are delivered in order, so the
|
|
973
|
+
* card's own event set `message_id` before any later update is handled; a row without one here
|
|
974
|
+
* means the card was never delivered (its event was skipped after repeated failures), so the
|
|
975
|
+
* update is skipped rather than applied to nothing. Only `message_id` decides: `status` is
|
|
976
|
+
* informational, because a replica still on the pre-0039 code fills `message_id` without setting
|
|
977
|
+
* `status`, leaving a delivered card `pending` during a rolling upgrade.
|
|
978
|
+
*/
|
|
979
|
+
async cardMessageOf(kind, id) {
|
|
980
|
+
const r = await this.d.db
|
|
981
|
+
.selectFrom("channel_card_messages")
|
|
982
|
+
.select(["message_id", "target"])
|
|
983
|
+
.where("conversation_id", "=", this.conversationId)
|
|
984
|
+
.where("connection_id", "=", this.s.row.id)
|
|
985
|
+
.where("card_kind", "=", kind)
|
|
986
|
+
.where("card_id", "=", id)
|
|
987
|
+
.executeTakeFirst();
|
|
988
|
+
if (!r)
|
|
989
|
+
return undefined;
|
|
990
|
+
if (r.message_id === null) {
|
|
991
|
+
this.d.log.warn("channel card update skipped: the card was never delivered", {
|
|
992
|
+
conversationId: this.conversationId,
|
|
993
|
+
kind,
|
|
994
|
+
id,
|
|
995
|
+
});
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
return { messageId: r.message_id, target: r.target };
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* 更新卡片状态。不能编辑已发消息的渠道(`features.edit` 为 false)另发一条带新状态的卡片——
|
|
1002
|
+
* 旧卡片上的按钮仍然在,之后再点由 `decide` / `answer` 回「已处理」,不会重复生效。
|
|
1003
|
+
*/
|
|
1004
|
+
async replaceCard(at, message, idempotencyKey) {
|
|
1005
|
+
if (this.s.features.edit) {
|
|
1006
|
+
await this.s.update(at.target, at.messageId, message, idempotencyKey);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
await this.s.send(at.target, message, idempotencyKey);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/** `assistant_message`:以完整文本定稿(更新草稿或新发);不发送 thinking。 */
|
|
1013
|
+
async finalize(runId, step, content, seq) {
|
|
1014
|
+
const key = `${runId}:${step}`;
|
|
1015
|
+
this.finalized.add(key);
|
|
1016
|
+
if (this.streaming?.key === key) {
|
|
1017
|
+
this.streaming = undefined;
|
|
1018
|
+
this.dirty = false;
|
|
1019
|
+
}
|
|
1020
|
+
const text = content
|
|
1021
|
+
.filter((b) => b.type === "text")
|
|
1022
|
+
.map((b) => b.text ?? "")
|
|
1023
|
+
.join("");
|
|
1024
|
+
const useDraft = this.draftId !== null &&
|
|
1025
|
+
this.s.features.edit &&
|
|
1026
|
+
(this.draftKey === undefined || this.draftKey === key);
|
|
1027
|
+
if (text.trim() === "") {
|
|
1028
|
+
// 草稿已发出、定稿却没有文本(如重试后这一步只调用工具):草稿原样保留、不加说明,只清掉 id
|
|
1029
|
+
if (useDraft)
|
|
1030
|
+
await this.closeDraft(seq, "none");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (useDraft && !this.draftBroken) {
|
|
1034
|
+
try {
|
|
1035
|
+
await this.s.update(this.target, this.draftId, { text }, this.key(seq));
|
|
1036
|
+
}
|
|
1037
|
+
catch (err) {
|
|
1038
|
+
if (!editRejected(err))
|
|
1039
|
+
throw err;
|
|
1040
|
+
// 编辑草稿被拒(消息太旧不能再改、内容被拒等):把完整文本作为新消息发出,不计毒事件
|
|
1041
|
+
this.d.log.warn("channel draft edit rejected; sending the final text as a new message", {
|
|
1042
|
+
conversationId: this.conversationId,
|
|
1043
|
+
error: this.redact(errMsg(err)),
|
|
1044
|
+
});
|
|
1045
|
+
await this.send({ text }, this.key(seq));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
await this.send({ text }, this.key(seq));
|
|
1050
|
+
}
|
|
1051
|
+
await this.advance(seq, { draft: null });
|
|
1052
|
+
}
|
|
1053
|
+
/** 流式增量:只累积本步从头收到的文本(`offset` 对得上),增量本身不进日志、不推进进度。 */
|
|
1054
|
+
onDelta(m) {
|
|
1055
|
+
if (!this.s.features.edit)
|
|
1056
|
+
return;
|
|
1057
|
+
const key = `${m.runId}:${m.step}`;
|
|
1058
|
+
if (this.finalized.has(key))
|
|
1059
|
+
return;
|
|
1060
|
+
if (m.delta === "step_reset") {
|
|
1061
|
+
// 该步重试:此前的文本作废,下一次尝试从 offset 0 重新开始
|
|
1062
|
+
this.streaming = { key, text: "", complete: false };
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (m.delta !== "text" || m.text === undefined)
|
|
1066
|
+
return;
|
|
1067
|
+
const cur = this.streaming?.key === key ? this.streaming : undefined;
|
|
1068
|
+
if (m.offset === 0) {
|
|
1069
|
+
this.streaming = { key, text: m.text, complete: true };
|
|
1070
|
+
}
|
|
1071
|
+
else if (cur?.complete && m.offset === cur.text.length) {
|
|
1072
|
+
cur.text += m.text;
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
this.streaming = { key, text: cur?.text ?? "", complete: false };
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.dirty = true;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* 首次有文本时发草稿(先把消息 id 写进 PG 再继续),之后至多每 `max(1s, features.minEditIntervalMs)` 更新一次。
|
|
1082
|
+
*/
|
|
1083
|
+
async flushDraft() {
|
|
1084
|
+
const cur = this.streaming;
|
|
1085
|
+
if (!cur?.complete || !this.dirty || cur.text.trim() === "")
|
|
1086
|
+
return;
|
|
1087
|
+
if (this.draftId !== null && this.draftKey !== undefined && this.draftKey !== cur.key)
|
|
1088
|
+
return;
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
if (this.draftId === null) {
|
|
1091
|
+
const { messageId } = await this.send({ text: cur.text }, idempotencyKey(`${this.conversationId}-draft-${cur.key}`));
|
|
1092
|
+
const r = await this.d.db
|
|
1093
|
+
.updateTable("channel_deliveries")
|
|
1094
|
+
.set({ draft_message_id: messageId, updated_at: new Date() })
|
|
1095
|
+
.where("conversation_id", "=", this.conversationId)
|
|
1096
|
+
.where("last_seq", "=", String(this.persisted))
|
|
1097
|
+
.executeTakeFirst();
|
|
1098
|
+
if (r.numUpdatedRows === 0n)
|
|
1099
|
+
throw new DeliveryLostError();
|
|
1100
|
+
this.draftId = messageId;
|
|
1101
|
+
this.draftKey = cur.key;
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
if (this.draftBroken)
|
|
1105
|
+
return;
|
|
1106
|
+
const interval = Math.max(DRAFT_INTERVAL_MS, this.s.features.minEditIntervalMs ?? 0);
|
|
1107
|
+
if (now - this.lastDraftAt < interval)
|
|
1108
|
+
return;
|
|
1109
|
+
try {
|
|
1110
|
+
await this.s.update(this.target, this.draftId, { text: cur.text });
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
if (!editRejected(err))
|
|
1114
|
+
throw err;
|
|
1115
|
+
// 中途编辑被拒:不再编辑草稿,定稿时改发新消息
|
|
1116
|
+
this.draftBroken = true;
|
|
1117
|
+
this.dirty = false;
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
this.draftKey = cur.key;
|
|
1121
|
+
}
|
|
1122
|
+
this.draftText = cur.text;
|
|
1123
|
+
this.lastDraftAt = now;
|
|
1124
|
+
this.dirty = false;
|
|
1125
|
+
}
|
|
1126
|
+
}
|