@fastagent-sh/fastagent 0.21.1 → 0.22.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/README.md +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +233 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +166 -229
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
|
@@ -139,7 +139,8 @@ export function createFeishuApi(opts) {
|
|
|
139
139
|
return cached.token;
|
|
140
140
|
};
|
|
141
141
|
/** The pipeline: one JSON API call, carrying every transport invariant (module header). */
|
|
142
|
-
const call = async (label, method, path, body) => {
|
|
142
|
+
const call = async (label, method, path, body, opts = {}) => {
|
|
143
|
+
const retries = opts.retries ?? RETRIES;
|
|
143
144
|
let refreshedAuth = false;
|
|
144
145
|
for (let attempt = 0;;) {
|
|
145
146
|
const token = await tenantToken();
|
|
@@ -178,7 +179,7 @@ export function createFeishuApi(opts) {
|
|
|
178
179
|
cached = undefined;
|
|
179
180
|
continue;
|
|
180
181
|
}
|
|
181
|
-
if ((res.status === 429 || code === RATE_LIMIT_CODE) && attempt <
|
|
182
|
+
if ((res.status === 429 || code === RATE_LIMIT_CODE) && attempt < retries) {
|
|
182
183
|
attempt++;
|
|
183
184
|
await wait(attempt * 1000);
|
|
184
185
|
continue;
|
|
@@ -327,8 +328,8 @@ export function createFeishuApi(opts) {
|
|
|
327
328
|
throw new FeishuApiError(kind, "createCard", 200, 0, "response carried no card_id");
|
|
328
329
|
return id;
|
|
329
330
|
},
|
|
330
|
-
async updateCardElement(cardId, elementId, content, sequence) {
|
|
331
|
-
await call("updateCardElement", "PUT", `/open-apis/cardkit/v1/cards/${encodeURIComponent(cardId)}/elements/${encodeURIComponent(elementId)}/content`, { content, sequence });
|
|
331
|
+
async updateCardElement(cardId, elementId, content, sequence, opts) {
|
|
332
|
+
await call("updateCardElement", "PUT", `/open-apis/cardkit/v1/cards/${encodeURIComponent(cardId)}/elements/${encodeURIComponent(elementId)}/content`, { content, sequence }, opts);
|
|
332
333
|
},
|
|
333
334
|
async updateCard(cardId, cardJson, sequence) {
|
|
334
335
|
await call("updateCard", "PUT", `/open-apis/cardkit/v1/cards/${encodeURIComponent(cardId)}`, {
|
|
@@ -10,24 +10,18 @@ interface FeishuChannelBaseOptions {
|
|
|
10
10
|
appId: string;
|
|
11
11
|
/** App Secret (same page) — drives both ingress authentication and outbound API calls. */
|
|
12
12
|
appSecret: string;
|
|
13
|
-
/** Policy: whether/where to answer an event (return null to ignore).
|
|
13
|
+
/** Policy: whether/where to answer an event (return null to ignore). */
|
|
14
14
|
route?: (event: FeishuMessageEvent) => FeishuRoute | null;
|
|
15
|
-
/** Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator
|
|
16
|
-
* log). Return a string to send it, or undefined/"" to stay silent. Default: a neutral message keyed
|
|
17
|
-
* on `retryable`. A developer's own bot can surface the raw details, e.g. `(f) => `⚠️ ${f.details}``. */
|
|
15
|
+
/** Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator log). */
|
|
18
16
|
onError?: (failed: FeishuFailure) => string | undefined;
|
|
19
|
-
/** API origin override (tests / self-hosted gateways).
|
|
20
|
-
* `https://open.feishu.cn`; Lark factories default to `https://open.larksuite.com`. Named to match
|
|
21
|
-
* the other channels (telegram/slack). */
|
|
17
|
+
/** API origin override (tests / self-hosted gateways). */
|
|
22
18
|
apiBaseUrl?: string;
|
|
23
|
-
/** How long (ms) a turn waits before its reply-quoted "⏳ Queued" card mounts.
|
|
24
|
-
* (immediate); the same card is later taken over by the live preview/final answer. */
|
|
19
|
+
/** How long (ms) a turn waits before its reply-quoted "⏳ Queued" card mounts. */
|
|
25
20
|
queueNoticeDelayMs?: number;
|
|
26
21
|
}
|
|
27
22
|
export interface FeishuChannelOptions extends FeishuChannelBaseOptions {
|
|
28
23
|
/** Verification Token for Request-URL authentication. */
|
|
29
24
|
verificationToken: string;
|
|
30
|
-
/** Optional webhook Encrypt Key. When set, plaintext events are rejected. */
|
|
31
25
|
encryptKey?: string;
|
|
32
26
|
}
|
|
33
27
|
export type FeishuWebSocketChannelOptions = FeishuChannelBaseOptions & {
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical Feishu bot-channel engine: verified webhook or official-SDK WebSocket → dedup → route →
|
|
3
|
-
*
|
|
4
|
-
* international binds this engine through an explicit compatibility profile because its control plane
|
|
5
|
-
* trails Feishu; protocol reuse does not make Lark the design center.
|
|
6
|
-
*
|
|
7
|
-
* The channel kind remains the unit of route, env namespace, state home, logs, and onboarding, so one
|
|
8
|
-
* workspace may run both without sharing state. Webhook returns the existing route factory; WebSocket
|
|
9
|
-
* returns an explicit long-connection module. Both feed the same acceptance/turn engine. See docs/feishu.md.
|
|
2
|
+
* Canonical Feishu bot-channel engine: verified webhook or official-SDK WebSocket → dedup → route → persist → enqueue
|
|
3
|
+
* → stream a live card.
|
|
10
4
|
*/
|
|
11
5
|
import { isAbsolute, join } from "node:path";
|
|
12
6
|
import { log } from "../../log.js";
|
|
@@ -15,22 +9,24 @@ import { text } from "../respond.js";
|
|
|
15
9
|
import { secretEquals } from "../secret.js";
|
|
16
10
|
import { createSeenRing } from "../kit/seen.js";
|
|
17
11
|
import { createTaskTracker } from "../kit/tasks.js";
|
|
12
|
+
import { attachmentsDir } from "../kit/attachment-path.js";
|
|
18
13
|
import { ensureStateHome, loadStateFile, saveStateFile } from "../kit/state.js";
|
|
19
14
|
import { signatureIsFresh } from "../kit/signature.js";
|
|
20
15
|
import { dispatchStop, isStopText } from "../kit/stop-command.js";
|
|
21
16
|
import { createTurnRunner } from "../kit/turn-runner.js";
|
|
17
|
+
import { portJoin } from "../../effect-port.js";
|
|
22
18
|
import { createTurnStore } from "../kit/turn-store.js";
|
|
23
19
|
import { discussionBlock } from "../kit/context-buffer.js";
|
|
24
20
|
import { FEISHU_CLOUD } from "./cloud.js";
|
|
25
21
|
import { collectFeishuBufferedAttachments, createFeishuContextBuffer, feishuBufferPlaceKey, feishuBufferText, } from "./context-buffer.js";
|
|
26
22
|
import { decryptEvent, verifySignature } from "./crypto.js";
|
|
27
|
-
import {
|
|
23
|
+
import { feishuTurnStream } from "./invoke-turn.js";
|
|
28
24
|
import { createFeishuApi } from "./feishu-api.js";
|
|
29
25
|
import { normalizeFeishuMessage } from "./normalize.js";
|
|
30
26
|
import { createThreadParticipants } from "../kit/thread-participants.js";
|
|
31
27
|
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_MESSAGE_READ_REQUEST, FEISHU_MESSAGE_READ_SCOPE, scopeSatisfied, } from "./setup-mode.js";
|
|
32
28
|
import { cloudEnvelope, defaultFeishuRoute, feishuEnvelope, placeKey, senderId, senderLabel, } from "./parse.js";
|
|
33
|
-
import { defaultErrorMessage, mountFeishuPreview, settleFeishuPreview,
|
|
29
|
+
import { defaultErrorMessage, mountFeishuPreview, settleFeishuPreview, feishuReply, } from "./preview.js";
|
|
34
30
|
import { connectFeishuWs } from "./ws-ingress.js";
|
|
35
31
|
import { registerFeishuApi } from "./shared-api.js";
|
|
36
32
|
// Canonical public surface; the Lark subpath aliases these types/functions at its compatibility boundary.
|
|
@@ -38,17 +34,14 @@ export { defaultFeishuRoute, feishuEnvelope };
|
|
|
38
34
|
/** Event body cap — events are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
39
35
|
const MAX_EVENT_BYTES = 1 << 20;
|
|
40
36
|
/**
|
|
41
|
-
* Replay window for a SIGNED event, sized by the OPEN PLATFORM'S REDELIVERY SCHEDULE, not by Slack's
|
|
42
|
-
*
|
|
43
|
-
* is re-signed with a fresh timestamp or replays the original one, and only the second case is safe
|
|
44
|
-
* to guess wrong about in one direction — a window under 6h would 401 three of the four retries, i.e.
|
|
45
|
-
* turn one transient fault into a permanently lost user message (and a callback-health alarm). Wide
|
|
46
|
-
* enough to cover the chain, still bounded, and the `seen` ring dedups an event_id inside it.
|
|
37
|
+
* Replay window for a SIGNED event, sized by the OPEN PLATFORM'S REDELIVERY SCHEDULE, not by Slack's 5 minutes: a
|
|
38
|
+
* failed push is retried at 15s / 5min / 1h / 6h.
|
|
47
39
|
*/
|
|
48
40
|
const MAX_SIGNATURE_AGE_S = 7 * 60 * 60;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
41
|
+
/**
|
|
42
|
+
* Queue feedback is immediate by default: it is the user's acknowledgement that this exact ask was accepted behind
|
|
43
|
+
* another turn.
|
|
44
|
+
*/
|
|
52
45
|
const QUEUE_NOTICE_DELAY_MS = 0;
|
|
53
46
|
const QUEUED_PLACEHOLDER = "⏳ Queued — I’ll start once the current task finishes.";
|
|
54
47
|
const DEFERRED_PLACEHOLDER = "⏳ Delayed by a temporary system issue — I’ll retry automatically.";
|
|
@@ -81,135 +74,93 @@ export function feishuChannel(opts) {
|
|
|
81
74
|
export function feishuWebSocketChannel(opts) {
|
|
82
75
|
return buildFeishuWebSocketChannel(FEISHU_CLOUD, opts, feishuWebSocketChannel.name);
|
|
83
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The bot's own open_id — the identity the default route matches group @mentions against. Seeded synchronously from
|
|
79
|
+
* a per-app cache file so a cold start can summon before the network answers, then refreshed once per process by
|
|
80
|
+
* `bot/v3/info`. The two failure directions differ: a FAILED call is transport weather and keeps the cached identity,
|
|
81
|
+
* while a SUCCESSFUL call reporting no open_id is an affirmative "there is no bot here" and clears it (fail-closed).
|
|
82
|
+
*/
|
|
83
|
+
function createBotIdentity(deps) {
|
|
84
|
+
const { api, appId, label, botFile } = deps;
|
|
85
|
+
const stored = loadStateFile(botFile);
|
|
86
|
+
if (stored !== undefined && typeof stored.appId !== "string") {
|
|
87
|
+
log.warn(`${label} unexpected shape in ${botFile} — ignoring the cached bot identity`);
|
|
88
|
+
}
|
|
89
|
+
let cached = stored?.appId === appId && typeof stored.openId === "string" ? stored.openId : undefined;
|
|
90
|
+
let current = cached;
|
|
91
|
+
// `{ appId }` with no openId reads as "no cache" at the loader — the atomic write is reused instead of
|
|
92
|
+
// introducing a deletion path.
|
|
93
|
+
const persist = (openId, failure) => {
|
|
94
|
+
if (openId === cached)
|
|
95
|
+
return;
|
|
96
|
+
cached = openId;
|
|
97
|
+
try {
|
|
98
|
+
saveStateFile(botFile, openId === undefined ? { appId } : { appId, openId });
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
log.warn(`${label} ${failure}: ${String(e)}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
void api.botInfo().then((me) => {
|
|
105
|
+
if (me.openId) {
|
|
106
|
+
if (current !== undefined && current !== me.openId) {
|
|
107
|
+
log.info(`${label} bot open_id changed (${current} → ${me.openId}) — updating the cached identity`);
|
|
108
|
+
}
|
|
109
|
+
current = me.openId;
|
|
110
|
+
persist(me.openId, `could not persist the bot identity to ${botFile} — the next cold start races bot/v3/info again`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
log.warn(current === undefined
|
|
114
|
+
? `${label} bot/v3/info returned no open_id — group @mention summon stays off`
|
|
115
|
+
: `${label} bot/v3/info returned no open_id — cached identity cleared; group @mention summon stays off`);
|
|
116
|
+
current = undefined;
|
|
117
|
+
persist(undefined, `could not clear the cached bot identity ${botFile}`);
|
|
118
|
+
}, (e) => log.warn(current === undefined
|
|
119
|
+
? `${label} bot/v3/info failed; group @mention summon stays off until restart: ${String(e)}`
|
|
120
|
+
: `${label} bot/v3/info failed; running on the cached identity (bot.json): ${String(e)}`));
|
|
121
|
+
return () => current;
|
|
122
|
+
}
|
|
84
123
|
function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
85
124
|
const { appId, appSecret, route, onError, queueNoticeDelayMs = QUEUE_NOTICE_DELAY_MS } = opts;
|
|
86
125
|
const baseUrl = opts.apiBaseUrl ?? profile.apiBase;
|
|
87
126
|
const { kind } = profile;
|
|
88
127
|
const label = `[${kind}]`;
|
|
89
128
|
return ({ agent, stateRoot, control }) => {
|
|
90
|
-
// Credential checks run when serving starts, not while the authored module is imported
|
|
91
|
-
// can inspect the module shape before secrets exist, while serving still fails before ready.
|
|
129
|
+
// Credential checks run when serving starts, not while the authored module is imported.
|
|
92
130
|
if (!appId || !appSecret) {
|
|
93
131
|
throw new Error(`${factoryName} requires appId + appSecret (developer console → Credentials & Basic Info)`);
|
|
94
132
|
}
|
|
95
133
|
const formatError = onError ?? defaultErrorMessage;
|
|
96
134
|
const api = createFeishuApi({ kind, baseUrl, appId, appSecret });
|
|
97
|
-
// One bot/v3/info per process refreshes the bot's own open_id — the identity the default route
|
|
98
|
-
// matches group @mentions against. The CACHED copy (bot.json, seeded synchronously once the state
|
|
99
|
-
// home exists below) is what makes the first envelope safe: this fetch is fire-and-forget, and
|
|
100
|
-
// under the AgentCore posture channel construction happens INSIDE the first envelope
|
|
101
|
-
// (channels/agentcore.ts lazy construction), so a network round trip can never beat that same
|
|
102
|
-
// envelope's own dispatch — without the seed, every cold start's FIRST group mention raced this
|
|
103
|
-
// fetch and lost (field-observed: an explicit @ buffered as bystander context). The open_id is a
|
|
104
|
-
// stable property of the app, so disk beats network; the only envelope a deployment ever serves
|
|
105
|
-
// without the file is its first one, which is the deploy driver's probe — it carries no mention.
|
|
106
|
-
// No identity at all (fresh dir, no cache, fetch pending/failed) keeps today's fail-closed
|
|
107
|
-
// behavior: unmatched mentions buffer as context — delayed, never lost.
|
|
108
|
-
let botOpenId;
|
|
109
|
-
let persistBotIdentity = () => { }; // bound once the state home exists
|
|
110
|
-
let invalidateBotIdentity = () => { }; // likewise
|
|
111
|
-
void api.botInfo().then((me) => {
|
|
112
|
-
if (me.openId) {
|
|
113
|
-
if (botOpenId !== undefined && botOpenId !== me.openId) {
|
|
114
|
-
log.info(`${label} bot open_id changed (${botOpenId} → ${me.openId}) — updating the cached identity`);
|
|
115
|
-
}
|
|
116
|
-
botOpenId = me.openId;
|
|
117
|
-
persistBotIdentity(me.openId);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// The call SUCCEEDED and the platform reported no identity — an affirmative "there is no
|
|
121
|
-
// bot here" (capability off, app reconfigured), not transport weather. This is the one
|
|
122
|
-
// answer that must also INVALIDATE the cache: keeping a summon identity the platform just
|
|
123
|
-
// declined to confirm would quietly turn fail-closed into fail-open.
|
|
124
|
-
log.warn(botOpenId === undefined
|
|
125
|
-
? `${label} bot/v3/info returned no open_id — group @mention summon stays off`
|
|
126
|
-
: `${label} bot/v3/info returned no open_id — cached identity cleared; group @mention summon stays off`);
|
|
127
|
-
botOpenId = undefined;
|
|
128
|
-
invalidateBotIdentity();
|
|
129
|
-
}
|
|
130
|
-
}, (e) =>
|
|
131
|
-
// A FAILED call is transport weather (network, rate limit): the platform said nothing about
|
|
132
|
-
// the identity, so a cached one keeps serving — the degradation this cache exists for.
|
|
133
|
-
log.warn(botOpenId === undefined
|
|
134
|
-
? `${label} bot/v3/info failed; group @mention summon stays off until restart: ${String(e)}`
|
|
135
|
-
: `${label} bot/v3/info failed; running on the cached identity (bot.json): ${String(e)}`));
|
|
136
135
|
void api.listAppScopes().then((scopes) => {
|
|
137
136
|
const grantedScope = (name) => scopes.some((scope) => scope.name === name && scope.grantStatus === 1 && (scope.type === undefined || scope.type === "tenant"));
|
|
138
137
|
if (grantedScope(FEISHU_GROUP_CONTEXT_SCOPE)) {
|
|
139
|
-
// This scope settles the rule's whole input: it delivers the un-mentioned group messages the
|
|
140
|
-
//
|
|
141
|
-
// is pending — the only bootstrap left is social, one mention inside a thread. The read scope
|
|
142
|
-
// is a separate, softer dependency, so it is reported separately rather than folded in.
|
|
138
|
+
// This scope settles the rule's whole input: it delivers the un-mentioned group messages the channel
|
|
139
|
+
// buffers, which is also what lets it HEAR a thread.
|
|
143
140
|
log.info(`${label} group visibility: context-aware — buffered discussion enabled; bare replies work in a thread once the agent has been mentioned in it`);
|
|
144
141
|
}
|
|
145
142
|
else {
|
|
146
143
|
log.warn(`${label} group visibility: @mentions only — ${FEISHU_GROUP_CONTEXT_SCOPE} is not granted; bare replies in the agent's threads + group context buffering are unavailable`);
|
|
147
144
|
}
|
|
148
|
-
// Reported OUTSIDE the branch above: the quoted-message read runs in every chat type and every
|
|
149
|
-
//
|
|
150
|
-
//
|
|
145
|
+
// Reported OUTSIDE the branch above: the quoted-message read runs in every chat type and every posture (a p2p
|
|
146
|
+
// thread's opening ask, any quoted @mention in a group), so pairing this warning with the group scope would
|
|
147
|
+
// leave a mention-only deployment silently losing every referent.
|
|
151
148
|
if (!scopeSatisfied(FEISHU_MESSAGE_READ_REQUEST, grantedScope)) {
|
|
152
149
|
log.warn(`${label} ${FEISHU_MESSAGE_READ_SCOPE} is not granted — a message quoted by an ask cannot be read, and degrades to a marker in the prompt`);
|
|
153
150
|
}
|
|
154
151
|
}, (error) => log.warn(`${label} could not inspect group visibility: ${String(error)}`));
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
// (engine state at the root, channel state under `channels/<kind>/`) — derived, not an option, so
|
|
158
|
-
// the operator's ONE state knob (FASTAGENT_STATE_DIR) can never be silently bypassed by glue.
|
|
152
|
+
// The channel-state convention: this channel's durable home is `<stateRoot>/channels/<kind>` (engine state at the
|
|
153
|
+
// root, channel state under `channels/<kind>/`).
|
|
159
154
|
if (!isAbsolute(stateRoot)) {
|
|
160
155
|
throw new Error(`${factoryName} requires an absolute ctx.stateRoot, got "${stateRoot}"`);
|
|
161
156
|
}
|
|
162
157
|
const stateHome = join(stateRoot, "channels", kind);
|
|
163
158
|
ensureStateHome(stateHome); // buffers/files may carry chat content; the agent .gitignore covers .state/
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// the seed is what the first envelope's dispatch sees. Refresh keeps the file current.
|
|
167
|
-
//
|
|
168
|
-
// BOUND TO THE APP: the state home is per channel KIND, and an operator can point kept state at
|
|
169
|
-
// a different app (a recreated app, a tenant migration). A cached identity from another app
|
|
170
|
-
// would make THIS bot treat mentions of the OLD bot as its own summons — identity impersonation,
|
|
171
|
-
// strictly worse than the race the cache removes — so the cache counts only when it names the
|
|
172
|
-
// current appId. A mismatch is not noise worth warning about: the next persist IS the migration.
|
|
173
|
-
const botFile = join(stateHome, "bot.json");
|
|
174
|
-
const storedBot = loadStateFile(botFile);
|
|
175
|
-
let cachedOpenId;
|
|
176
|
-
if (storedBot !== undefined) {
|
|
177
|
-
if (typeof storedBot.appId !== "string") {
|
|
178
|
-
log.warn(`${label} unexpected shape in ${botFile} — ignoring the cached bot identity`);
|
|
179
|
-
}
|
|
180
|
-
else if (storedBot.appId === appId && typeof storedBot.openId === "string") {
|
|
181
|
-
cachedOpenId = storedBot.openId;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
botOpenId ??= cachedOpenId;
|
|
185
|
-
persistBotIdentity = (openId) => {
|
|
186
|
-
if (openId === cachedOpenId)
|
|
187
|
-
return;
|
|
188
|
-
cachedOpenId = openId;
|
|
189
|
-
try {
|
|
190
|
-
saveStateFile(botFile, { appId, openId });
|
|
191
|
-
}
|
|
192
|
-
catch (e) {
|
|
193
|
-
log.warn(`${label} could not persist the bot identity to ${botFile} — the next cold start races bot/v3/info again: ${String(e)}`);
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
invalidateBotIdentity = () => {
|
|
197
|
-
if (cachedOpenId === undefined)
|
|
198
|
-
return;
|
|
199
|
-
cachedOpenId = undefined;
|
|
200
|
-
try {
|
|
201
|
-
// `{ appId }` with no openId reads as "no cache" at the loader — the atomic write is reused
|
|
202
|
-
// instead of introducing a deletion path.
|
|
203
|
-
saveStateFile(botFile, { appId });
|
|
204
|
-
}
|
|
205
|
-
catch (e) {
|
|
206
|
-
log.warn(`${label} could not clear the cached bot identity ${botFile}: ${String(e)}`);
|
|
207
|
-
}
|
|
208
|
-
};
|
|
159
|
+
const botOpenId = createBotIdentity({ api, appId, label, botFile: join(stateHome, "bot.json") });
|
|
160
|
+
const decide = route ?? ((event) => defaultFeishuRoute(event, { botOpenId: botOpenId() }));
|
|
209
161
|
const threadParticipants = createThreadParticipants(join(stateHome, "thread-participants.json"), label);
|
|
210
162
|
/** This channel's place key for a thread (the shared store is key-agnostic). */
|
|
211
|
-
// The SAME identity the session uses (`placeKey`) — a thread's place.
|
|
212
|
-
// future re-keying silently split participation from the sessions it is supposed to describe.
|
|
163
|
+
// The SAME identity the session uses (`placeKey`) — a thread's place.
|
|
213
164
|
const threadKey = (chatId, threadId) => placeKey(kind, { chat_id: chatId, thread_id: threadId });
|
|
214
165
|
const buffer = createFeishuContextBuffer(join(stateHome, "buffers.json"), label);
|
|
215
166
|
const store = createTurnStore(join(stateHome, "turns.json"), {
|
|
@@ -230,9 +181,8 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
230
181
|
replyTo: r.queueReplyTo,
|
|
231
182
|
replyInThread: r.replyInThread,
|
|
232
183
|
});
|
|
233
|
-
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal,
|
|
234
|
-
//
|
|
235
|
-
// leaving it pinned at "Queued" while sending a separate failure would double-post.
|
|
184
|
+
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not just an
|
|
185
|
+
// operator log line.
|
|
236
186
|
const notifyDropped = (r) => {
|
|
237
187
|
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
238
188
|
void settleFeishuPreview(api, targetOf(r), r.preview, body).catch((e) => log.warn(`${label} could not notify a dropped turn (session=${r.session}): ${String(e)}`));
|
|
@@ -246,12 +196,7 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
246
196
|
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, preview: undefined }),
|
|
247
197
|
bufferKey: (rec) => rec.bufferKey,
|
|
248
198
|
where: (rec) => `chat=${rec.chatId}`,
|
|
249
|
-
// Queue feedback: mount that turn's preview early with a queue status.
|
|
250
|
-
// source message (including p2p), then the runner mutates the SAME card/text into Thinking →
|
|
251
|
-
// final answer. Immediate by default; with an explicit delay it mounts only if the turn is still
|
|
252
|
-
// waiting when the timer fires and is cancelled unsent otherwise. Best-effort and post-ACK: a
|
|
253
|
-
// failed mount is a log line, never a failed event delivery; the turn later mounts its normal
|
|
254
|
-
// preview.
|
|
199
|
+
// Queue feedback: mount that turn's preview early with a queue status.
|
|
255
200
|
onQueuedBehind: (rec) => {
|
|
256
201
|
let fired = false;
|
|
257
202
|
let settle = () => { };
|
|
@@ -281,82 +226,64 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
281
226
|
},
|
|
282
227
|
};
|
|
283
228
|
},
|
|
284
|
-
// Do not recall an existing queue preview (the client exposes a confusing tombstone): settle it in
|
|
285
|
-
//
|
|
229
|
+
// Do not recall an existing queue preview (the client exposes a confusing tombstone): settle it in place to an
|
|
230
|
+
// honest delayed status.
|
|
286
231
|
onDeferred: (rec) => {
|
|
287
232
|
if (rec.preview !== undefined) {
|
|
288
233
|
void settleFeishuPreview(api, targetOf(rec), rec.preview, DEFERRED_PLACEHOLDER).catch((e) => log.warn(`${label} could not update a deferred turn's queue preview: ${String(e)}`));
|
|
289
234
|
}
|
|
290
235
|
},
|
|
291
236
|
notifyDropped,
|
|
292
|
-
|
|
237
|
+
// `rec.preview`: the queue card THIS process mounted if the record waited behind another turn.
|
|
238
|
+
deliverAnswer: (rec, answer) => portJoin(() => settleFeishuPreview(api, targetOf(rec), rec.preview, answer)),
|
|
239
|
+
execute: (rec, discussion, onAnswered) => {
|
|
293
240
|
// PEEK and never commit: the room still owes this discussion to its OWN memory (§8).
|
|
294
|
-
// ponytail: independent threaded roots in one main chat dequeue concurrently and may both fold
|
|
295
|
-
//
|
|
296
|
-
//
|
|
241
|
+
// ponytail: independent threaded roots in one main chat dequeue concurrently and may both fold the room's
|
|
242
|
+
// snapshot before either commits it. That fan-out loses nothing; claiming by buffer key would instead couple
|
|
243
|
+
// otherwise-independent root sessions and require failure rollback.
|
|
297
244
|
const room = rec.roomBufferKey !== undefined ? buffer.peek(rec.roomBufferKey) : undefined;
|
|
298
245
|
const roomBlock = room?.text
|
|
299
246
|
? `[recent discussion in the room this thread branched from — not yet answered there:\n${room.text}\n]\n\n`
|
|
300
247
|
: "";
|
|
301
248
|
const prompt = `${roomBlock}${discussionBlock(discussion.text)}${rec.baseText}`;
|
|
302
|
-
// Room entries FIRST: the collector keeps the TAIL under its cap, so the thread's own
|
|
303
|
-
//
|
|
249
|
+
// Room entries FIRST: the collector keeps the TAIL under its cap, so the thread's own attachments win the
|
|
250
|
+
// slots.
|
|
304
251
|
const buffered = collectFeishuBufferedAttachments([...(room?.consumed ?? []), ...discussion.consumed], {
|
|
305
252
|
images: rec.images.map((ref) => ({ messageId: ref.msg, key: ref.key })),
|
|
306
253
|
files: rec.files.map((ref) => ({ messageId: ref.msg, key: ref.key, name: ref.name })),
|
|
307
254
|
});
|
|
308
|
-
// Recorded at ingress (see submit) — never re-derived from the session key, which may be a
|
|
309
|
-
//
|
|
255
|
+
// Recorded at ingress (see submit) — never re-derived from the session key, which may be a routed OPAQUE id
|
|
256
|
+
// that only looks like a place key.
|
|
310
257
|
const parentSession = rec.parentSession;
|
|
311
|
-
return
|
|
258
|
+
return feishuReply(feishuTurnStream(agent, rec.session, prompt, {
|
|
312
259
|
api,
|
|
313
260
|
chatId: rec.chatId,
|
|
314
|
-
filesDir:
|
|
261
|
+
filesDir: attachmentsDir(stateHome),
|
|
315
262
|
label,
|
|
316
263
|
appId,
|
|
317
264
|
...(parentSession !== undefined ? { parentSession } : {}),
|
|
318
|
-
}, { primary: { images: rec.images, files: rec.files, parentId: rec.parentId }, buffered }
|
|
265
|
+
}, { primary: { images: rec.images, files: rec.files, parentId: rec.parentId }, buffered }), api, targetOf(rec), formatError, rec.preview, label, onAnswered);
|
|
319
266
|
},
|
|
320
267
|
});
|
|
321
268
|
registerFeishuApi(stateRoot, kind, api);
|
|
322
269
|
let seqCounter = runner.recover().reduce((max, r) => Math.max(max, r.seq), 0);
|
|
323
|
-
// Who the agent has heard in a thread decides whether a bare message addresses it (participant
|
|
324
|
-
//
|
|
270
|
+
// Who the agent has heard in a thread decides whether a bare message addresses it (participant model §3), and it
|
|
271
|
+
// comes from what this channel observed.
|
|
325
272
|
let warnedUnidentified = false;
|
|
326
|
-
/**
|
|
327
|
-
* What this delivery contributes to thread participation, or undefined when it contributes nothing.
|
|
328
|
-
*
|
|
329
|
-
* ONE gate for BOTH writes (the humans observation on the way in, and the `agentSpoke` merge once
|
|
330
|
-
* the turn is durable), and one definition of the synthetic speaker id — hand-written conditions in
|
|
331
|
-
* two places is the drift that has already bitten this branch once. Structural facts only; see
|
|
332
|
-
* thread-participants.ts for why configuration must not appear here.
|
|
333
|
-
*
|
|
334
|
-
* Feishu-specific: p2p is excluded (nothing reads those records), and a custom route may admit a
|
|
335
|
-
* bot the default route filters out — answering one is not participation the summon rule should
|
|
336
|
-
* act on, so such a thread keeps no record and the first human still needs the mention bootstrap.
|
|
337
|
-
*/
|
|
273
|
+
/** What this delivery contributes to thread participation, or undefined when it contributes nothing. */
|
|
338
274
|
const heardIn = (m, sender) => {
|
|
339
275
|
if (m.chat_type !== "group" || m.thread_id === undefined || sender?.sender_type !== "user")
|
|
340
276
|
return undefined;
|
|
341
277
|
const speakerId = senderId(sender);
|
|
342
278
|
if (speakerId === undefined && !warnedUnidentified) {
|
|
343
|
-
// Once per MOUNT — the flag lives in this channel's closure on purpose
|
|
344
|
-
// property of THIS app's event configuration, so a `feishu` and a `lark` mount must each be
|
|
345
|
-
// able to report it. A per-thread set would instead grow without bound to repeat one fact.
|
|
279
|
+
// Once per MOUNT — the flag lives in this channel's closure on purpose.
|
|
346
280
|
warnedUnidentified = true;
|
|
347
281
|
log.warn(`${label} human senders arrive with no usable id (first seen in thread ${m.thread_id}) — each counts as a distinct speaker, so affected threads permanently require an @mention until thread-participants.json is deleted`);
|
|
348
282
|
}
|
|
349
|
-
// A human whose id no tenant flavour carries still SPOKE, and no human may speak unrecorded.
|
|
350
|
-
// per-MESSAGE synthetic id keeps them distinct; a per-thread one would collapse every human on an
|
|
351
|
-
// id-less tenant into one, which is the direction that barges into a crowd. Its cost is PERMANENT
|
|
352
|
-
// — two such messages fill MAX_HUMANS and records never shed, so that thread needs an @mention
|
|
353
|
-
// from then on and only deleting the file resets it (§3).
|
|
283
|
+
// A human whose id no tenant flavour carries still SPOKE, and no human may speak unrecorded.
|
|
354
284
|
return { key: threadKey(m.chat_id, m.thread_id), speaker: speakerId ?? `unidentified:${m.message_id}` };
|
|
355
285
|
};
|
|
356
|
-
// Transport-neutral acceptance boundary: normalize, route, persist intent/context, enqueue.
|
|
357
|
-
// touches no network, so it stays synchronous inside the platform's ACK window and the delivery
|
|
358
|
-
// dedup ring alone is enough — there is no await for a duplicate push to race through. The
|
|
359
|
-
// minutes-long Agent turn remains fire-and-forget.
|
|
286
|
+
// Transport-neutral acceptance boundary: normalize, route, persist intent/context, enqueue.
|
|
360
287
|
const acceptEvent = (event) => {
|
|
361
288
|
const m = event.message;
|
|
362
289
|
if (!m?.message_id || !m.chat_id)
|
|
@@ -371,9 +298,8 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
371
298
|
return;
|
|
372
299
|
const bufferKey = feishuBufferPlaceKey(normalized.conversation);
|
|
373
300
|
const isHumanGroup = event.sender?.sender_type === "user" && m.chat_type === "group";
|
|
374
|
-
// Listening is not speaking: every message the channel can see refines who takes part in its
|
|
375
|
-
//
|
|
376
|
-
// second human speaking is exactly what makes addressing ambiguous again.
|
|
301
|
+
// Listening is not speaking: every message the channel can see refines who takes part in its thread, whether or
|
|
302
|
+
// not it is answered.
|
|
377
303
|
const heard = heardIn(m, event.sender);
|
|
378
304
|
if (heard)
|
|
379
305
|
threadParticipants.merge(heard.key, { humans: [heard.speaker] });
|
|
@@ -400,8 +326,7 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
400
326
|
key: resource.key,
|
|
401
327
|
name: resource.name,
|
|
402
328
|
}));
|
|
403
|
-
// A write failure escapes this boundary.
|
|
404
|
-
// SDK turns it into a 500 ACK frame. Both transports therefore ask the platform to re-push.
|
|
329
|
+
// A write failure escapes this boundary.
|
|
405
330
|
buffer.push(bufferKey, {
|
|
406
331
|
sender: senderLabel(event.sender) ?? "someone",
|
|
407
332
|
body: bodyText,
|
|
@@ -423,33 +348,23 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
423
348
|
return;
|
|
424
349
|
}
|
|
425
350
|
// Memory follows the place (participant model §5): one session per chat, and one per thread.
|
|
426
|
-
// Keyed by `thread_id`, never `root_id` — the platform's root_id tracks the reply chain and can
|
|
427
|
-
// differ between messages of ONE thread, which would split a side conversation in two.
|
|
428
351
|
const routed = r.session;
|
|
429
352
|
const session = routed ?? placeKey(kind, m);
|
|
430
|
-
// Lineage is recorded ONLY for the default place-derived session.
|
|
431
|
-
// OPAQUE (the route contract), and re-parsing it as a place key would let a three-segment id
|
|
432
|
-
// like "tenant:user:alice" masquerade as a thread and inherit from "tenant:user" — a
|
|
433
|
-
// cross-session injection. Derived from the MESSAGE (the fact this channel owns), at record
|
|
434
|
-
// time, where routed-ness is still known; the dequeue path only reads it back.
|
|
353
|
+
// Lineage is recorded ONLY for the default place-derived session.
|
|
435
354
|
const parentSession = routed === undefined && m.thread_id !== undefined ? placeKey(kind, { chat_id: m.chat_id }) : undefined;
|
|
436
|
-
// Read BEFORE this turn records its own participation below, or it is always true.
|
|
437
|
-
// SOURCE chat, never the answer target a route may name (§8).
|
|
355
|
+
// Read BEFORE this turn records its own participation below, or it is always true.
|
|
438
356
|
const roomBufferKey = parentSession !== undefined && !threadParticipants.agentSpokeIn(session)
|
|
439
357
|
? feishuBufferPlaceKey({ chatId: m.chat_id })
|
|
440
358
|
: undefined;
|
|
441
359
|
const chatId = r.chatId ?? m.chat_id;
|
|
442
360
|
const sameTarget = chatId === m.chat_id;
|
|
443
|
-
// Answer where asked (§4): quote in a group so the ask is identifiable among many speakers,
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// move the model refuses.
|
|
361
|
+
// Answer where asked (§4): quote in a group so the ask is identifiable among many speakers, stay plain in an
|
|
362
|
+
// ordinary direct message, and stay inside a thread whenever the question came from one — a direct message's
|
|
363
|
+
// thread is a place too, and relocating out of it is the silent move the model refuses.
|
|
447
364
|
const replyTo = sameTarget && (m.chat_type === "group" || m.thread_id !== undefined) ? m.message_id : undefined;
|
|
448
365
|
const replyInThread = replyTo !== undefined && m.thread_id !== undefined ? true : undefined;
|
|
449
366
|
const queueReplyTo = sameTarget ? m.message_id : undefined;
|
|
450
|
-
// Explicit user stop: a control action, never a turn — it must not queue behind the run it
|
|
451
|
-
// stops. Mentions arrive as @name tokens; strip them before matching the bare word. Record the
|
|
452
|
-
// message id so a platform re-push doesn't double-abort or double-notify.
|
|
367
|
+
// Explicit user stop: a control action, never a turn — it must not queue behind the run it stops.
|
|
453
368
|
if (isStopText(normalized.content.text.replace(/@\S+/g, " "))) {
|
|
454
369
|
seen.add(m.message_id);
|
|
455
370
|
sideTasks.track(dispatchStop(control, session, label)
|
|
@@ -477,46 +392,19 @@ function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
|
477
392
|
replyTo,
|
|
478
393
|
queueReplyTo,
|
|
479
394
|
replyInThread,
|
|
480
|
-
// A quote is the user explicitly pointing at something that may predate this session
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// There used to be an exception: skip it inside a thread the agent had already answered in,
|
|
484
|
-
// since the session would hold it. That needed a second fact — did the channel RECEIVE the
|
|
485
|
-
// messages in between? — which depends on a scope that changes over time, while the record it
|
|
486
|
-
// was read against is durable. Every attempt to gate it correctly failed in the same
|
|
487
|
-
// direction (a silently missing quote), for an optimisation worth one `getMessage` on a
|
|
488
|
-
// quote-reply inside an active thread. Loading it costs a call and some text the session may
|
|
489
|
-
// already have; it also pins WHICH message is being answered, which a long thread benefits
|
|
490
|
-
// from anyway.
|
|
395
|
+
// A quote is the user explicitly pointing at something that may predate this session (§8 rung 2), so it is
|
|
396
|
+
// always loaded.
|
|
491
397
|
parentId: m.parent_id,
|
|
492
398
|
...(parentSession !== undefined ? { parentSession } : {}),
|
|
493
399
|
...(roomBufferKey !== undefined ? { roomBufferKey } : {}),
|
|
494
400
|
images,
|
|
495
401
|
files,
|
|
496
402
|
}, true);
|
|
497
|
-
// Answering inside a thread makes the agent a participant of it, which is what lets the NEXT
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
// `r.session === undefined` is what makes this fact mean what its reader assumes. Participation
|
|
501
|
-
// is keyed by THREAD while the memory it stands in for is keyed by SESSION, and those agree only
|
|
502
|
-
// when the session is derived from the place. A route supplying its own (the scaffold's
|
|
503
|
-
// `session: user:<open_id>` example) can put two people's turns in one thread into different
|
|
504
|
-
// sessions — recording `agentSpoke` from one of them would tell the summon rule the agent took
|
|
505
|
-
// part in a conversation it cannot remember. So the flag records "the agent answered into THIS
|
|
506
|
-
// THREAD'S session". Such a thread keeps a bystander record and needs the ordinary mention to
|
|
507
|
-
// bootstrap if the route is later dropped. `group` matches the observation above so the record is
|
|
508
|
-
// never half-written.
|
|
509
|
-
//
|
|
510
|
-
// Recorded only once the intent is durable: `submit` can throw, and a redelivery must still see
|
|
511
|
-
// the thread as the agent has actually left it. A later delivery failure does not undo it —
|
|
512
|
-
// entering the conversation is the intent, not the send.
|
|
513
|
-
// Reuses `heardIn` from the way in, so both writes share ONE gate by construction. The extra
|
|
514
|
-
// conditions are about the ANSWER, not the speaker: it must land in this thread (`sameTarget`,
|
|
515
|
-
// `replyInThread`) and in the place's own session, or the flag would claim a memory that never
|
|
516
|
-
// held the turn.
|
|
403
|
+
// Answering inside a thread makes the agent a participant of it, which is what lets the NEXT bare message
|
|
404
|
+
// address it without a mention (§3).
|
|
517
405
|
if (heard && replyInThread === true && sameTarget && r.session === undefined) {
|
|
518
|
-
// Both halves in ONE merge, like Slack's: a record that needed an earlier merge to survive
|
|
519
|
-
//
|
|
406
|
+
// Both halves in ONE merge, like Slack's: a record that needed an earlier merge to survive could otherwise
|
|
407
|
+
// say "answered here, heard nobody".
|
|
520
408
|
threadParticipants.merge(heard.key, { agentSpoke: true, humans: [heard.speaker] });
|
|
521
409
|
}
|
|
522
410
|
};
|
|
@@ -528,9 +416,9 @@ function createFeishuWebhookRoutes(profile, opts, runtime) {
|
|
|
528
416
|
const { kind, envPrefix } = profile;
|
|
529
417
|
const label = `[${kind}]`;
|
|
530
418
|
if (!encryptKey) {
|
|
531
|
-
// Said once at wiring, not per request: without an encrypt key events arrive in plaintext and
|
|
532
|
-
//
|
|
533
|
-
//
|
|
419
|
+
// Said once at wiring, not per request: without an encrypt key events arrive in plaintext and carry no signature,
|
|
420
|
+
// so the freshness window below never runs and a captured body replays for as long as the verification token
|
|
421
|
+
// lives.
|
|
534
422
|
log.warn(`${label} no ${envPrefix}_ENCRYPT_KEY: events are accepted unsigned, with no replay window — set one in the console to enable it`);
|
|
535
423
|
}
|
|
536
424
|
const handler = async (req) => {
|
|
@@ -560,10 +448,8 @@ function createFeishuWebhookRoutes(profile, opts, runtime) {
|
|
|
560
448
|
signature: req.headers.get("x-lark-signature") ?? "",
|
|
561
449
|
};
|
|
562
450
|
if (sig.signature) {
|
|
563
|
-
// Freshness BEFORE the signature: the signature covers the timestamp but proves nothing about
|
|
564
|
-
//
|
|
565
|
-
// `seen` ring is not that defence — it is bounded, and past its rollover a replay re-runs the
|
|
566
|
-
// turn (a re-sent message, a re-fired tool).
|
|
451
|
+
// Freshness BEFORE the signature: the signature covers the timestamp but proves nothing about it, so without
|
|
452
|
+
// a window a captured body + its three x-lark-* headers replays forever.
|
|
567
453
|
if (!signatureIsFresh(sig.timestamp, MAX_SIGNATURE_AGE_S)) {
|
|
568
454
|
log.warn(`${label} rejected an event: X-Lark-Request-Timestamp outside the ±${MAX_SIGNATURE_AGE_S / 3600} h replay window`);
|
|
569
455
|
return text("stale signature\n", 401);
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* or hide its still-readable siblings.
|
|
14
14
|
*/
|
|
15
15
|
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
16
|
+
import type * as Stream from "effect/Stream";
|
|
17
|
+
import type { PortFailure } from "../../effect-port.ts";
|
|
16
18
|
import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
|
|
17
19
|
import type { FeishuBufferedRef } from "./context-buffer.ts";
|
|
18
20
|
import type { FeishuApi } from "./feishu-api.ts";
|
|
@@ -54,9 +56,8 @@ export interface FeishuTurnAttachments {
|
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
|
-
* Run one turn: resolve its inputs, then
|
|
58
|
-
*
|
|
59
|
-
* primary-input failure surfaces as a `failed` event (never a silent drop).
|
|
59
|
+
* Run one turn: resolve its inputs, then ask the agent (invoke-turn-kit — a primary-input failure
|
|
60
|
+
* surfaces as a `failed` event, never a silent drop).
|
|
60
61
|
*/
|
|
61
|
-
export declare function
|
|
62
|
+
export declare function feishuTurnStream(agent: Agent, session: string, text: string, transport: FeishuTurnTransport, attachments: FeishuTurnAttachments, busyRetry?: BusyRetry): Stream.Stream<AgentEvent, PortFailure>;
|
|
62
63
|
export {};
|