@fastagent-sh/fastagent 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +209 -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 +153 -228
- 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
|
@@ -1,57 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical Feishu live-preview rendering (also reused by Lark compatibility). The preview is ONE
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* updates ride the cardkit quota (50 QPS per app, 10 QPS per card entity, no edit ceiling) — NOT the
|
|
9
|
-
* 5 QPS per-chat message quota or
|
|
10
|
-
* the 20-edit cap on text messages, which is why the preview is a card and not an edited text message.
|
|
2
|
+
* Canonical Feishu live-preview rendering (also reused by Lark compatibility). The preview is ONE streaming CARD of
|
|
3
|
+
* two elements — the volatile `process` block and the append-only `answer` (card.ts explains why the split is the
|
|
4
|
+
* prefix-stability fix) — streamed as full-text snapshots per element under a strictly increasing `sequence`, then
|
|
5
|
+
* settled in place with the final answer alone. A queued turn mounts the same card early with its queue status and
|
|
6
|
+
* execution takes the entity over, so there is no second reply. Streaming rides the cardkit quota, not the 5 QPS
|
|
7
|
+
* per-chat message quota or the 20-edit cap on text messages — which is why the preview is a card.
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* one-message lifecycle without trying to change a text message into a card (which the platform does
|
|
15
|
-
* not support), and keeps multiple queued asks attributable even if their card mounts race visually.
|
|
16
|
-
*
|
|
17
|
-
* Fallback tier (fail visibly, degrade per turn): if the card cannot be created or mounted, the turn
|
|
18
|
-
* runs with a TEXT placeholder and NO live updates (text edits are capped at 20 per message, so the
|
|
19
|
-
* text tier spends them only on terminal writes); if the platform closes streaming mid-turn (idle
|
|
20
|
-
* timeout), the preview freezes and the settle still lands. The final write is authoritative either
|
|
21
|
-
* way, mirroring the telegram preview's terminal-write matrix (completed/failed/abnormal ×
|
|
22
|
-
* settle/delete+send/suppress).
|
|
9
|
+
* Degrade tiers (fail visibly, per turn): a card that cannot be created or mounted falls back to a TEXT placeholder
|
|
10
|
+
* with NO live updates; streaming closed mid-turn freezes the preview. The final write is authoritative either way.
|
|
23
11
|
*/
|
|
24
12
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Clock from "effect/Clock";
|
|
15
|
+
import { previewPump, renderReply } from "../kit/delivery.js";
|
|
16
|
+
import { portJoin } from "../../effect-port.js";
|
|
25
17
|
import { log } from "../../log.js";
|
|
26
18
|
import { ANSWER_ELEMENT_ID, CARD_MARKDOWN_MAX_BYTES, PROCESS_ELEMENT_ID, cardEntityContent, finalCardJson, streamingCardJson, } from "./card.js";
|
|
27
19
|
import { chunkFeishuText, isCardStreamingClosed } from "./feishu-api.js";
|
|
28
|
-
import {
|
|
20
|
+
import { DROPPABLE_FRAME } from "../kit/transport.js";
|
|
21
|
+
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
|
|
29
22
|
import { truncateCodePointPrefix, truncateUtf8 } from "../kit/text.js";
|
|
30
23
|
export { defaultErrorMessage };
|
|
31
|
-
/** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop.
|
|
32
|
-
* allows 10 QPS per card entity (50 per app), but one snapshot a second reads smoothly (the client
|
|
33
|
-
* animates between snapshots).
|
|
34
|
-
* Doubles as the answer-preview aging window (see answerView). */
|
|
24
|
+
/** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. */
|
|
35
25
|
const STREAM_THROTTLE_MS = 1000;
|
|
36
26
|
/** How much of the (growing) reasoning to peek at in the live view — the most recent tail. */
|
|
37
27
|
const THINKING_PREVIEW = 280;
|
|
38
|
-
/** Cap (code points) on the whole process block — thinking tail + tool lines + retry notice.
|
|
39
|
-
* redraws wholly on change anyway (it is volatile by nature), so over budget the newest COMPLETE
|
|
40
|
-
* lines win (see tailLines). ≤1000 points is ≤4 KB UTF-8, which together with the answer's byte cap
|
|
41
|
-
* stays inside the 30 KB entity budget. */
|
|
28
|
+
/** Cap (code points) on the whole process block — thinking tail + tool lines + retry notice. */
|
|
42
29
|
const PROCESS_MAX_POINTS = 1000;
|
|
43
|
-
/** Cap the live answer to the card budget, PREFIX-STABLE
|
|
44
|
-
* old text is a prefix of the new, so an over-budget answer freezes at its head rather than sliding a
|
|
45
|
-
* tail window (which would re-type the element every frame). The full answer still lands at settle. */
|
|
30
|
+
/** Cap the live answer to the card budget, PREFIX-STABLE. */
|
|
46
31
|
function capBytes(s, maxBytes) {
|
|
47
32
|
return truncateUtf8(s, maxBytes);
|
|
48
33
|
}
|
|
49
|
-
/** Tail-select COMPLETE lines within a code-point budget — the process block's cap.
|
|
50
|
-
* lines are semantic units (a `🔧` tool call, the `💭` peek, the `⏳` notice): cutting mid-line
|
|
51
|
-
* would orphan a marker or tear a label, so elision happens only at line boundaries, newest lines
|
|
52
|
-
* kept, with a leading `…` line marking what was dropped. The in-line guard cannot trigger with the
|
|
53
|
-
* bounded renderers (a thinking tail ≤ ~283 points, a tool line ≤ ~135) — it exists so a future
|
|
54
|
-
* unbounded line degrades to a head-preserving cut instead of an empty block. */
|
|
34
|
+
/** Tail-select COMPLETE lines within a code-point budget — the process block's cap. */
|
|
55
35
|
function tailLines(text, maxPoints) {
|
|
56
36
|
if (Array.from(text).length <= maxPoints)
|
|
57
37
|
return text;
|
|
@@ -70,13 +50,7 @@ function tailLines(text, maxPoints) {
|
|
|
70
50
|
return truncateCodePointPrefix(lines.at(-1) ?? "", maxPoints);
|
|
71
51
|
return `…\n${kept.join("\n")}`;
|
|
72
52
|
}
|
|
73
|
-
/**
|
|
74
|
-
* The terminal-write POLICY: resolve the preview into `text`. One card → settle it in place (final
|
|
75
|
-
* markdown, streaming off); an over-budget answer settles the card with its first chunk and sends the
|
|
76
|
-
* rest as follow-up messages. A failed settle falls back to delete + fresh send, so no "Thinking…" card
|
|
77
|
-
* is left pinned above the answer. Text tier → ONE edit into the final text (or delete + fresh sends
|
|
78
|
-
* when it doesn't fit). No preview → fresh send. EMPTY text = "say nothing" → just delete the preview.
|
|
79
|
-
*/
|
|
53
|
+
/** The terminal-write POLICY: resolve the preview into `text`. */
|
|
80
54
|
async function finalize(api, target, preview, text, seq) {
|
|
81
55
|
if (text.trim() === "") {
|
|
82
56
|
if (preview.kind !== "none")
|
|
@@ -94,11 +68,7 @@ async function finalize(api, target, preview, text, seq) {
|
|
|
94
68
|
// Settle failed (card expired / rejected) — fall through to delete + fresh send below.
|
|
95
69
|
}
|
|
96
70
|
if (settled) {
|
|
97
|
-
// Threaded continuations must keep reply_in_thread
|
|
98
|
-
// unset — a custom route, or an ask outside a thread) intentionally avoid repeating the quote on
|
|
99
|
-
// every chunk. sendText owns the same distinction for its own chunking.
|
|
100
|
-
// A continuation failure propagates: the card is already authoritative, so deleting it and sending
|
|
101
|
-
// the full answer again would deterministically duplicate every continuation that already landed.
|
|
71
|
+
// Threaded continuations must keep reply_in_thread.
|
|
102
72
|
const continuationTarget = target.replyInThread ? target : { chatId: target.chatId };
|
|
103
73
|
for (const chunk of rest)
|
|
104
74
|
await api.sendText(continuationTarget, chunk);
|
|
@@ -121,10 +91,8 @@ async function finalize(api, target, preview, text, seq) {
|
|
|
121
91
|
await api.sendText(target, text);
|
|
122
92
|
}
|
|
123
93
|
/**
|
|
124
|
-
* Mount one preview message: preferably a streaming card entity (`initial` seeds the process element;
|
|
125
|
-
*
|
|
126
|
-
* and ordinary turn startup share this constructor so a queued card has exactly the same shape the
|
|
127
|
-
* stream pump expects to take over later.
|
|
94
|
+
* Mount one preview message: preferably a streaming card entity (`initial` seeds the process element; the answer
|
|
95
|
+
* element starts empty), with a static text message as the visible fallback.
|
|
128
96
|
*/
|
|
129
97
|
export async function mountFeishuPreview(api, target, initial, label = "[feishu]") {
|
|
130
98
|
try {
|
|
@@ -134,10 +102,7 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
|
|
|
134
102
|
? api.replyMessage(target.replyTo, "interactive", content, { replyInThread: target.replyInThread })
|
|
135
103
|
: api.sendMessage(target.chatId, "interactive", content);
|
|
136
104
|
let messageId;
|
|
137
|
-
// Field-observed: the mount can reject a JUST-minted card id (code 230099 / "cardid is invalid")
|
|
138
|
-
// — the entity is not yet visible to the IM side (eventual consistency between cardkit and IM).
|
|
139
|
-
// That specific rejection gets a short backoff and another try before degrading; anything else
|
|
140
|
-
// degrades immediately.
|
|
105
|
+
// Field-observed: the mount can reject a JUST-minted card id (code 230099 / "cardid is invalid").
|
|
141
106
|
for (let attempt = 1;; attempt++) {
|
|
142
107
|
try {
|
|
143
108
|
messageId = await mountOnce();
|
|
@@ -155,8 +120,8 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
|
|
|
155
120
|
return { kind: "card", cardId, messageId };
|
|
156
121
|
}
|
|
157
122
|
catch (e) {
|
|
158
|
-
// Card tier failed — degrade to a text placeholder with NO live updates (the text tier's 20-edit
|
|
159
|
-
//
|
|
123
|
+
// Card tier failed — degrade to a text placeholder with NO live updates (the text tier's 20-edit cap is spent on
|
|
124
|
+
// terminal writes only).
|
|
160
125
|
log.warn(`${label} streaming card unavailable — live preview degrades to a static placeholder: ${String(e)}`);
|
|
161
126
|
const messageId = target.replyTo !== undefined
|
|
162
127
|
? await api.replyMessage(target.replyTo, "text", JSON.stringify({ text: initial }), {
|
|
@@ -168,163 +133,99 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
|
|
|
168
133
|
return { kind: "text", messageId };
|
|
169
134
|
}
|
|
170
135
|
}
|
|
171
|
-
/**
|
|
172
|
-
*
|
|
136
|
+
/**
|
|
137
|
+
* Settle a preview without starting an Agent stream: a queue/poison/defer notice, or a recovered answer whose own
|
|
138
|
+
* preview died with the run that produced it (`undefined` — then it is one fresh message).
|
|
139
|
+
*/
|
|
173
140
|
export async function settleFeishuPreview(api, target, preview, text) {
|
|
174
141
|
let sequence = 0;
|
|
175
142
|
await finalize(api, target, preview ?? { kind: "none" }, text, () => ++sequence);
|
|
176
143
|
}
|
|
177
144
|
/**
|
|
178
|
-
* Consume one turn's event stream into a Feishu-compatible chat, live
|
|
179
|
-
* model). Preview updates are best-effort (logged once if they fail); the final write is authoritative
|
|
180
|
-
* and surfaces a real failure (bad credentials, etc.). `initialPreview`, when present, is the queued
|
|
181
|
-
* turn's already-mounted card/text message: the pump and terminal write mutate that same message rather
|
|
182
|
-
* than recalling it and posting another reply.
|
|
145
|
+
* Consume one turn's event stream into a Feishu-compatible chat, live.
|
|
183
146
|
*/
|
|
184
|
-
export
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const flushPreview = async () => {
|
|
220
|
-
const process = processView();
|
|
221
|
-
if (!setupAttempted) {
|
|
222
|
-
setupAttempted = true;
|
|
223
|
-
// The mount seeds the process element with the current view; the answer element starts empty
|
|
224
|
-
// (card.ts), so the first answer snapshot is a clean prefix extension.
|
|
225
|
-
preview = await mountFeishuPreview(api, target, process, label);
|
|
226
|
-
lastProcess = process;
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (preview.kind !== "card" || streamDead)
|
|
230
|
-
return; // text tier / dead stream: frozen until the terminal write
|
|
231
|
-
try {
|
|
232
|
-
// `last*` advances BEFORE each write: a frame that fails for a non-streaming reason is logged
|
|
233
|
-
// once (the pump's onError) and not re-sent until its content actually changes. An EMPTY
|
|
234
|
-
// process frame is written like any other — it is the placeholder being cleared (processView).
|
|
235
|
-
if (process !== lastProcess) {
|
|
147
|
+
export function feishuReply(events, api, target, formatError, initialPreview, label = "[feishu]", onAnswered) {
|
|
148
|
+
return Effect.gen(function* () {
|
|
149
|
+
const clock = yield* Clock.Clock;
|
|
150
|
+
const now = () => clock.currentTimeMillisUnsafe();
|
|
151
|
+
// Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal policy, the
|
|
152
|
+
// card-budget caps, and delivery below.
|
|
153
|
+
const turn = createTurnView();
|
|
154
|
+
const processView = () => {
|
|
155
|
+
const v = composeTurnBody([
|
|
156
|
+
thinkingLine(turn, THINKING_PREVIEW),
|
|
157
|
+
toolLines(turn),
|
|
158
|
+
turn.retrying ? RETRY_NOTICE : "",
|
|
159
|
+
]);
|
|
160
|
+
if (v !== "")
|
|
161
|
+
return tailLines(v, PROCESS_MAX_POINTS);
|
|
162
|
+
// No process content: the placeholder covers only the silence BEFORE the answer reveals.
|
|
163
|
+
return revealedAnswer(turn, STREAM_THROTTLE_MS, now()).trim() === "" ? THINKING_PLACEHOLDER : "";
|
|
164
|
+
};
|
|
165
|
+
const answerView = () => capBytes(revealedAnswer(turn, STREAM_THROTTLE_MS, now()), CARD_MARKDOWN_MAX_BYTES);
|
|
166
|
+
// The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview mounted
|
|
167
|
+
// lazily on this turn's first flush.
|
|
168
|
+
let preview = initialPreview ?? { kind: "none" };
|
|
169
|
+
let setupAttempted = initialPreview !== undefined;
|
|
170
|
+
let sequence = 0;
|
|
171
|
+
const nextSeq = () => ++sequence;
|
|
172
|
+
let streamDead = false; // the platform closed streaming (idle timeout) — freeze the live view
|
|
173
|
+
let lastProcess = "";
|
|
174
|
+
let lastAnswer = "";
|
|
175
|
+
const flushPreview = async () => {
|
|
176
|
+
const process = processView();
|
|
177
|
+
if (!setupAttempted) {
|
|
178
|
+
setupAttempted = true;
|
|
179
|
+
// The mount seeds the process element with the current view; the answer element starts empty (card.ts), so
|
|
180
|
+
// the first answer snapshot is a clean prefix extension.
|
|
181
|
+
preview = await mountFeishuPreview(api, target, process, label);
|
|
236
182
|
lastProcess = process;
|
|
237
|
-
await api.updateCardElement(preview.cardId, PROCESS_ELEMENT_ID, process, nextSeq());
|
|
238
|
-
}
|
|
239
|
-
const answer = answerView();
|
|
240
|
-
// Never write an empty answer snapshot — the element is born empty and the answer only grows.
|
|
241
|
-
if (answer !== "" && answer !== lastAnswer) {
|
|
242
|
-
lastAnswer = answer;
|
|
243
|
-
await api.updateCardElement(preview.cardId, ANSWER_ELEMENT_ID, answer, nextSeq());
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
catch (e) {
|
|
247
|
-
if (isCardStreamingClosed(e)) {
|
|
248
|
-
// The platform closed streaming (idle timeout). Freeze the live view; the settle write replaces
|
|
249
|
-
// the whole entity (streaming off) and still lands.
|
|
250
|
-
streamDead = true;
|
|
251
|
-
log.warn(`${label} card streaming closed mid-turn — preview frozen; the final answer still lands`);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
throw e;
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
// The shared single-writer pump (preview-kit) serializes snapshots to the one preview — which also
|
|
258
|
-
// guarantees the card's strictly-increasing `sequence` lands in order (no concurrent frames).
|
|
259
|
-
const { touch, finish } = createPreviewPump({
|
|
260
|
-
flush: flushPreview,
|
|
261
|
-
throttleMs: STREAM_THROTTLE_MS,
|
|
262
|
-
onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
|
|
263
|
-
});
|
|
264
|
-
touch(); // mount the "💭 Thinking…" preview immediately
|
|
265
|
-
/** Terminal write, whatever tier the preview reached. */
|
|
266
|
-
const settle = async (text) => {
|
|
267
|
-
await finalize(api, target, preview, text, nextSeq);
|
|
268
|
-
};
|
|
269
|
-
try {
|
|
270
|
-
for await (const e of events) {
|
|
271
|
-
if (e.type === "completed") {
|
|
272
|
-
await finish();
|
|
273
|
-
// Settle the preview into the final answer; the persisted card is the answer alone — the
|
|
274
|
-
// process (thinking/tools) was preview-only. Mark finalized BEFORE delivering: the terminal was
|
|
275
|
-
// reached, so a delivery failure here is a plain failure, not an "abnormal exit" (which would
|
|
276
|
-
// wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
|
|
277
|
-
finalized = true;
|
|
278
|
-
await settle(turn.answer.trim() !== "" ? turn.answer : "(no reply)");
|
|
279
183
|
return;
|
|
280
184
|
}
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
// Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
|
|
284
|
-
// log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
|
|
285
|
-
// write as completed; an empty notice deletes the preview (suppress = no residue). Best-effort —
|
|
286
|
-
// we throw below regardless.
|
|
287
|
-
finalized = true;
|
|
288
|
-
{
|
|
289
|
-
const msg = formatError({
|
|
290
|
-
details: e.details,
|
|
291
|
-
retryable: e.retryable,
|
|
292
|
-
...(e.code !== undefined ? { code: e.code } : {}),
|
|
293
|
-
}) ?? "";
|
|
294
|
-
try {
|
|
295
|
-
await settle(msg);
|
|
296
|
-
}
|
|
297
|
-
catch (deliveryError) {
|
|
298
|
-
// Preserve the Agent failure as the primary error below, but keep the broken final hop in
|
|
299
|
-
// the operator-visible chain — otherwise the log falsely implies the user saw the notice.
|
|
300
|
-
log.error(`${label} failed to deliver the agent-failure notice: ${String(deliveryError)}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
|
|
304
|
-
}
|
|
305
|
-
if (applyTurnEvent(turn, e))
|
|
306
|
-
touch();
|
|
307
|
-
}
|
|
308
|
-
throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
await finish();
|
|
312
|
-
// Abnormal exit (stream ended without a terminal, the generator threw, or the consumer abandoned):
|
|
313
|
-
// no terminal write ran. Show the SAME neutral notice a `failed` event would — the preview may show
|
|
314
|
-
// real partial work, so don't delete it silently, and don't leave the user in silence. A suppressing
|
|
315
|
-
// onError still collapses to a delete (finalize on empty text).
|
|
316
|
-
if (!finalized) {
|
|
317
|
-
// retryable:false — an abnormal end (no terminal / a throw) is of UNKNOWN retryability, so use the
|
|
318
|
-
// neutral "something went wrong" default rather than promising "try again" that may not help.
|
|
319
|
-
const notice = formatError({ details: "the turn ended without completing", retryable: false }) ?? "";
|
|
185
|
+
if (preview.kind !== "card" || streamDead)
|
|
186
|
+
return; // text tier / dead stream: frozen until the terminal write
|
|
320
187
|
try {
|
|
321
|
-
|
|
188
|
+
// `last*` advances BEFORE each write: a frame that fails for a non-streaming reason is logged once (the
|
|
189
|
+
// pump's onError) and not re-sent until its content actually changes.
|
|
190
|
+
const answer = answerView();
|
|
191
|
+
// Never write an empty answer snapshot — the element is born empty and the answer only grows.
|
|
192
|
+
if (answer !== "" && answer !== lastAnswer) {
|
|
193
|
+
lastAnswer = answer;
|
|
194
|
+
await api.updateCardElement(preview.cardId, ANSWER_ELEMENT_ID, answer, nextSeq(), DROPPABLE_FRAME);
|
|
195
|
+
}
|
|
196
|
+
if (process !== lastProcess) {
|
|
197
|
+
lastProcess = process;
|
|
198
|
+
await api.updateCardElement(preview.cardId, PROCESS_ELEMENT_ID, process, nextSeq(), DROPPABLE_FRAME);
|
|
199
|
+
}
|
|
322
200
|
}
|
|
323
|
-
catch (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (isCardStreamingClosed(e)) {
|
|
203
|
+
// The platform closed streaming (idle timeout).
|
|
204
|
+
streamDead = true;
|
|
205
|
+
log.warn(`${label} card streaming closed mid-turn — preview frozen; the final answer still lands`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw e;
|
|
327
209
|
}
|
|
328
|
-
}
|
|
329
|
-
|
|
210
|
+
};
|
|
211
|
+
// The scoped single writer keeps card sequences ordered through the terminal update.
|
|
212
|
+
const { touch, finish } = yield* previewPump({
|
|
213
|
+
flush: flushPreview,
|
|
214
|
+
throttleMs: STREAM_THROTTLE_MS,
|
|
215
|
+
onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
|
|
216
|
+
});
|
|
217
|
+
touch(); // mount the "💭 Thinking…" preview immediately
|
|
218
|
+
yield* renderReply(events, {
|
|
219
|
+
label,
|
|
220
|
+
finish,
|
|
221
|
+
formatError,
|
|
222
|
+
onEvent: (event) => {
|
|
223
|
+
if (applyTurnEvent(turn, event, now()))
|
|
224
|
+
touch();
|
|
225
|
+
},
|
|
226
|
+
answer: () => (turn.answer.trim() !== "" ? turn.answer : "(no reply)"),
|
|
227
|
+
...(onAnswered ? { onAnswered } : {}),
|
|
228
|
+
settle: (text) => portJoin(() => finalize(api, target, preview, text, nextSeq)),
|
|
229
|
+
});
|
|
230
|
+
});
|
|
330
231
|
}
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Hand-rolled on fetch, no SDK: the wire protocol is two form-encoded POSTs to the accounts endpoint
|
|
8
|
-
* plus RFC 8628's polling error dance — shared verbatim by all four official SDKs (node/python/java/go),
|
|
9
|
-
* which makes it a de-facto stable surface even though only the SDKs document it. Provenance:
|
|
10
|
-
* larksuite/node-sdk `scene/registration` (registerApp). If the platform ever moves this behind
|
|
11
|
-
* something non-trivial (signed payloads, websockets), adopt the official SDK instead of chasing it —
|
|
12
|
-
* the same tripwire as feishu-api.ts.
|
|
13
|
-
*
|
|
14
|
-
* The scanning user's tenant decides the brand: a Lark-tenant user flips polling to the Lark accounts
|
|
15
|
-
* domain mid-flow (`tenant_brand: "lark"`), and the result carries the brand so the caller can point
|
|
16
|
-
* everything else (API origin) at the right cloud.
|
|
2
|
+
* One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628) flow the platform
|
|
3
|
+
* provides for agent apps: `begin` returns a one-time verification URL the user opens in Feishu/Lark and confirms,
|
|
4
|
+
* then polling returns the new app's credentials. Hand-rolled on fetch because the wire protocol (two form-encoded
|
|
5
|
+
* POSTs plus RFC 8628's polling error dance) is shared verbatim by all four official SDKs; if the platform ever moves
|
|
6
|
+
* it behind something non-trivial, adopt the official SDK rather than chasing it.
|
|
17
7
|
*/
|
|
18
8
|
/**
|
|
19
|
-
* Additive app config carried on the confirm-page URL (`addons` query param): extra scopes/events
|
|
20
|
-
*
|
|
21
|
-
* encoding (JSON → gzip → base64url) follow the official SDKs (provenance: node-sdk
|
|
22
|
-
* scene/registration); item names unknown to the platform catalog are silently dropped by the page.
|
|
9
|
+
* Additive app config carried on the confirm-page URL (`addons` query param): extra scopes/events merged ON TOP of the
|
|
10
|
+
* platform's agent template.
|
|
23
11
|
*/
|
|
24
12
|
interface FeishuAppAddons {
|
|
25
13
|
scopes?: {
|
|
@@ -48,7 +36,6 @@ export interface RegisterFeishuAppOptions {
|
|
|
48
36
|
url: string;
|
|
49
37
|
expiresInS: number;
|
|
50
38
|
}) => void;
|
|
51
|
-
/** Cancel the polling. */
|
|
52
39
|
signal?: AbortSignal;
|
|
53
40
|
/** Accounts origins, for tests. */
|
|
54
41
|
accountsBaseUrl?: string;
|
|
@@ -63,9 +50,7 @@ export interface RegisteredFeishuApp {
|
|
|
63
50
|
openId?: string;
|
|
64
51
|
}
|
|
65
52
|
/**
|
|
66
|
-
* Run the scan-to-create flow
|
|
67
|
-
* poll until the user confirms. Resolves with the new app's credentials; rejects on denial, expiry,
|
|
68
|
-
* abort, or a transport failure — every rejection is a plain Error whose message says what to do.
|
|
53
|
+
* Run the scan-to-create flow: begin → hand the verification URL to the caller → poll until the user confirms.
|
|
69
54
|
*/
|
|
70
55
|
export declare function registerFeishuApp(options: RegisterFeishuAppOptions): Promise<RegisteredFeishuApp>;
|
|
71
56
|
export {};
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Hand-rolled on fetch, no SDK: the wire protocol is two form-encoded POSTs to the accounts endpoint
|
|
8
|
-
* plus RFC 8628's polling error dance — shared verbatim by all four official SDKs (node/python/java/go),
|
|
9
|
-
* which makes it a de-facto stable surface even though only the SDKs document it. Provenance:
|
|
10
|
-
* larksuite/node-sdk `scene/registration` (registerApp). If the platform ever moves this behind
|
|
11
|
-
* something non-trivial (signed payloads, websockets), adopt the official SDK instead of chasing it —
|
|
12
|
-
* the same tripwire as feishu-api.ts.
|
|
13
|
-
*
|
|
14
|
-
* The scanning user's tenant decides the brand: a Lark-tenant user flips polling to the Lark accounts
|
|
15
|
-
* domain mid-flow (`tenant_brand: "lark"`), and the result carries the brand so the caller can point
|
|
16
|
-
* everything else (API origin) at the right cloud.
|
|
2
|
+
* One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628) flow the platform
|
|
3
|
+
* provides for agent apps: `begin` returns a one-time verification URL the user opens in Feishu/Lark and confirms,
|
|
4
|
+
* then polling returns the new app's credentials. Hand-rolled on fetch because the wire protocol (two form-encoded
|
|
5
|
+
* POSTs plus RFC 8628's polling error dance) is shared verbatim by all four official SDKs; if the platform ever moves
|
|
6
|
+
* it behind something non-trivial, adopt the official SDK rather than chasing it.
|
|
17
7
|
*/
|
|
18
8
|
import { gzipSync } from "node:zlib";
|
|
19
9
|
/** Feishu accounts endpoint (the flow starts here for every user; a Lark-tenant scan switches over). */
|
|
@@ -32,8 +22,7 @@ function encodeAddons(addons) {
|
|
|
32
22
|
}
|
|
33
23
|
/** RFC 8628 device-flow states whose non-2xx JSON bodies belong to the polling state machine. */
|
|
34
24
|
const DEVICE_FLOW_ERRORS = new Set(["authorization_pending", "slow_down", "access_denied", "expired_token"]);
|
|
35
|
-
/** One registration POST (form-encoded).
|
|
36
|
-
* slow_down, …) as HTTP 400 with a JSON body — those parse as data, not as transport failures. */
|
|
25
|
+
/** One registration POST (form-encoded). */
|
|
37
26
|
async function post(baseUrl, params) {
|
|
38
27
|
let res;
|
|
39
28
|
let raw;
|
|
@@ -67,9 +56,7 @@ async function post(baseUrl, params) {
|
|
|
67
56
|
/** Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers. */
|
|
68
57
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
58
|
/**
|
|
70
|
-
* Run the scan-to-create flow
|
|
71
|
-
* poll until the user confirms. Resolves with the new app's credentials; rejects on denial, expiry,
|
|
72
|
-
* abort, or a transport failure — every rejection is a plain Error whose message says what to do.
|
|
59
|
+
* Run the scan-to-create flow: begin → hand the verification URL to the caller → poll until the user confirms.
|
|
73
60
|
*/
|
|
74
61
|
export async function registerFeishuApp(options) {
|
|
75
62
|
const feishuBase = options.accountsBaseUrl ?? FEISHU_ACCOUNTS;
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { type RegistrationOutcome } from "../registration.ts";
|
|
2
2
|
import { type FeishuCloudKind } from "./cloud.ts";
|
|
3
|
-
/**
|
|
4
|
-
* Register `<baseUrl>/<kind>` as the app's event Request URL (webhook mode). Missing credentials print
|
|
5
|
-
* the manual instruction instead of failing. `opts` carries the attempt budget — `--tunnel` takes the
|
|
6
|
-
* default, `deploy --run` passes `DEPLOY_REGISTRATION_ATTEMPTS` (a host starts slower than a tunnel) —
|
|
7
|
-
* plus `apiBase`, which is a test's fake platform (production derives it from the kind).
|
|
8
|
-
*
|
|
9
|
-
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
10
|
-
*/
|
|
3
|
+
/** Register `<baseUrl>/<kind>` as the app's event Request URL (webhook mode). */
|
|
11
4
|
interface FeishuManualRegistration {
|
|
12
5
|
consoleUrl: string;
|
|
13
6
|
requestUrl: string;
|
|
@@ -16,7 +9,7 @@ export interface RegisterFeishuWebhookOptions {
|
|
|
16
9
|
attempts?: number;
|
|
17
10
|
retryMs?: number;
|
|
18
11
|
apiBase?: string;
|
|
19
|
-
/** Manual fallback after a definitive config error or exhausted retries.
|
|
12
|
+
/** Manual fallback after a definitive config error or exhausted retries. */
|
|
20
13
|
onManualRegistration?: (info: FeishuManualRegistration) => void;
|
|
21
14
|
}
|
|
22
15
|
export declare function registerFeishuWebhook(baseUrl: string, kind: FeishuCloudKind, opts?: RegisterFeishuWebhookOptions): Promise<RegistrationOutcome>;
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Feishu/Lark event-URL registration — the platform-domain step both `--tunnel` (dev, tunnel.ts) and
|
|
3
|
-
*
|
|
4
|
-
* "registering feishu/lark" means lives here, beside the engine it serves; it reads the same .env
|
|
5
|
-
* credentials the channel of that kind uses (`FEISHU_*` / `LARK_*`).
|
|
6
|
-
*
|
|
7
|
-
* Mechanism: the application-v7 config PATCH (`updateEventSubscription`) flips the app's event
|
|
8
|
-
* subscription to webhook mode and points it at `<baseUrl>/<kind>`. Two properties make this the full
|
|
9
|
-
* telegram-setWebhook parity: the platform applies a request-URL change IMMEDIATELY (no version
|
|
10
|
-
* publish), and it VERIFIES the URL with a url_verification challenge during the PATCH — which makes
|
|
11
|
-
* the PATCH its own readiness probe, retried below. (It used to poll `<baseUrl>/health` from here
|
|
12
|
-
* first; that asked whether THIS machine could reach a freshly minted hostname, which is routinely
|
|
13
|
-
* false for a minute or more while the platform reaches it fine — #421.) Requires the
|
|
14
|
-
* `application:application:patch` scope (field-tested: `self_manage` does NOT cover this PATCH) —
|
|
15
|
-
* `add feishu` requests it at creation via addons; without it the PATCH fails visibly and the manual
|
|
16
|
-
* console instruction is printed.
|
|
17
|
-
*
|
|
18
|
-
* CLOUD LAG: the application-v7 config API exists on open.feishu.cn but (as of 2026-07) is NOT
|
|
19
|
-
* deployed on open.larksuite.com — the route 404s there. The registrar still attempts it (the day the
|
|
20
|
-
* platform ships it, registration starts working with no change here) and names the real cause in the
|
|
21
|
-
* fallback instead of blaming the app's scopes.
|
|
2
|
+
* Feishu/Lark event-URL registration — the platform-domain step both `--tunnel` (dev, tunnel.ts) and `deploy … --run`
|
|
3
|
+
* (the host runners' post-deploy step) invoke, once per mounted kind.
|
|
22
4
|
*/
|
|
23
5
|
import { log } from "../../log.js";
|
|
24
6
|
import { retryWhile } from "../registration.js";
|
|
@@ -52,10 +34,7 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
|
|
|
52
34
|
log.warn(`[fastagent] ${kind}: could not open Events & Callbacks: ${String(callbackError)}`);
|
|
53
35
|
}
|
|
54
36
|
};
|
|
55
|
-
// The PATCH is the probe (same lesson as the token bootstrap)
|
|
56
|
-
// with a challenge DURING the call, so its 210042 "request_url validation failed" is the readiness
|
|
57
|
-
// signal and is retried with backoff, alongside transient network errors. Only a permanent config
|
|
58
|
-
// error (missing scope, app under review, the intl 404) is reported once with the manual path.
|
|
37
|
+
// The PATCH is the probe (same lesson as the token bootstrap).
|
|
59
38
|
try {
|
|
60
39
|
await retryWhile(() => api.updateEventSubscription(appId, { subscriptionType: "webhook", requestUrl }), isTransientFeishuRegistrationError, {
|
|
61
40
|
attempts: opts.attempts,
|
|
@@ -63,25 +42,22 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
|
|
|
63
42
|
onRetry: ({ attempt, attempts }) => log.info(`[fastagent] ${kind}: the platform cannot verify ${requestUrl} yet (attempt ${attempt}/${attempts}); retrying…`),
|
|
64
43
|
});
|
|
65
44
|
log.info(`[fastagent] ${kind}: event Request URL registered → ${requestUrl}`);
|
|
66
|
-
// Field-tested: a URL change applies immediately, but the MODE flip (the template's long
|
|
67
|
-
//
|
|
68
|
-
// the published snapshot, and version publishing has no open API. One console click, once.
|
|
45
|
+
// Field-tested: a URL change applies immediately, but the MODE flip (the template's long connection → webhook)
|
|
46
|
+
// only takes effect when a version is published.
|
|
69
47
|
log.info(`[fastagent] ${kind}: if messages do not arrive, publish a version (one click, prompted) — the switch to webhook mode takes effect on publish: ${versionUrl}`);
|
|
70
48
|
return "registered";
|
|
71
49
|
}
|
|
72
50
|
catch (e) {
|
|
73
|
-
// A 404 on the config route is the CLOUD lagging, not this app's configuration: the v7 API is
|
|
74
|
-
//
|
|
75
|
-
// would send the operator hunting for a problem they cannot fix. It stays WARN: on that cloud the
|
|
76
|
-
// manual path is the known norm, not an exceptional failure.
|
|
51
|
+
// A 404 on the config route is the CLOUD lagging, not this app's configuration: the v7 API is live on
|
|
52
|
+
// open.feishu.cn but not yet on open.larksuite.com.
|
|
77
53
|
if (isFeishuConfigApiMissing(e)) {
|
|
78
54
|
log.warn(`[fastagent] ${kind}: this cloud (${apiBase}) returned HTTP 404 for the app-config API — ` +
|
|
79
55
|
`manual registration is required`);
|
|
80
56
|
manualRegistration();
|
|
81
57
|
return "manual"; // that cloud has no config API — the manual path is the norm there
|
|
82
58
|
}
|
|
83
|
-
// Exhausted retries end in the same state as a permanent config error (event URL not registered,
|
|
84
|
-
//
|
|
59
|
+
// Exhausted retries end in the same state as a permanent config error (event URL not registered, manual action
|
|
60
|
+
// required) — report at the same level.
|
|
85
61
|
log.error(isTransientFeishuRegistrationError(e)
|
|
86
62
|
? `[fastagent] ${kind}: the platform could not verify ${requestUrl} after retries — manual registration is required`
|
|
87
63
|
: `[fastagent] ${kind}: could not register the event URL (${String(e)}). ` +
|