@fastagent-sh/fastagent 0.12.0 → 0.13.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 +42 -36
- package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
- package/dist/channels/feishu/bootstrap-token.js +94 -0
- package/dist/channels/feishu/card.d.ts +32 -0
- package/dist/channels/feishu/card.js +66 -0
- package/dist/channels/feishu/cloud.d.ts +17 -0
- package/dist/channels/feishu/cloud.js +19 -0
- package/dist/channels/feishu/crypto.d.ts +13 -0
- package/dist/channels/feishu/crypto.js +41 -0
- package/dist/channels/feishu/feishu-api.d.ts +108 -0
- package/dist/channels/feishu/feishu-api.js +325 -0
- package/dist/channels/feishu/feishu.d.ts +36 -0
- package/dist/channels/feishu/feishu.js +359 -0
- package/dist/channels/feishu/invoke-turn.d.ts +59 -0
- package/dist/channels/feishu/invoke-turn.js +106 -0
- package/dist/channels/feishu/parse.d.ts +125 -0
- package/dist/channels/feishu/parse.js +175 -0
- package/dist/channels/feishu/preview.d.ts +36 -0
- package/dist/channels/feishu/preview.js +387 -0
- package/dist/channels/feishu/register-app.d.ts +70 -0
- package/dist/channels/feishu/register-app.js +141 -0
- package/dist/channels/feishu/register-webhook.d.ts +22 -0
- package/dist/channels/feishu/register-webhook.js +106 -0
- package/dist/channels/feishu/scaffold/channel.ts +34 -0
- package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
- package/dist/channels/feishu/seen.d.ts +5 -0
- package/dist/channels/feishu/seen.js +47 -0
- package/dist/channels/feishu/text.d.ts +13 -0
- package/dist/channels/feishu/text.js +63 -0
- package/dist/channels/lark/lark.d.ts +15 -0
- package/dist/channels/lark/lark.js +10 -0
- package/dist/channels/lark/onboard.d.ts +39 -0
- package/dist/channels/lark/onboard.js +58 -0
- package/dist/channels/lark/scaffold/channel.ts +32 -0
- package/dist/channels/lark/scaffold/lark-send.ts +87 -0
- package/dist/channels/registration.d.ts +15 -0
- package/dist/channels/registration.js +1 -0
- package/dist/channels/{telegram/state.js → state.js} +6 -4
- package/dist/channels/telegram/context-buffer.js +1 -1
- package/dist/channels/telegram/register-webhook.d.ts +4 -1
- package/dist/channels/telegram/register-webhook.js +17 -26
- package/dist/channels/telegram/telegram.js +2 -2
- package/dist/channels/telegram/turn-store.d.ts +8 -21
- package/dist/channels/telegram/turn-store.js +11 -130
- package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
- package/dist/channels/turn-store.d.ts +42 -0
- package/dist/channels/turn-store.js +139 -0
- package/dist/channels/wait-health.d.ts +6 -0
- package/dist/channels/wait-health.js +27 -0
- package/dist/cli-add-feishu.d.ts +8 -0
- package/dist/cli-add-feishu.js +223 -0
- package/dist/cli.js +68 -23
- package/dist/deploy/container.js +10 -6
- package/dist/deploy/fly/plan.d.ts +1 -1
- package/dist/deploy/fly/plan.js +15 -4
- package/dist/deploy/fly/run.d.ts +7 -4
- package/dist/deploy/fly/run.js +26 -5
- package/dist/deploy/railway/plan.d.ts +1 -1
- package/dist/deploy/railway/plan.js +17 -5
- package/dist/deploy/railway/run.d.ts +6 -3
- package/dist/deploy/railway/run.js +26 -4
- package/dist/deploy/registration-gate.d.ts +20 -0
- package/dist/deploy/registration-gate.js +20 -0
- package/dist/deploy/secrets.d.ts +10 -9
- package/dist/deploy/secrets.js +15 -14
- package/dist/dev-supervisor.js +2 -1
- package/dist/engines/pi/chat.js +3 -3
- package/dist/engines/pi/create.d.ts +0 -1
- package/dist/engines/pi/create.js +8 -7
- package/dist/feishu.d.ts +2 -0
- package/dist/feishu.js +2 -0
- package/dist/lark.d.ts +3 -0
- package/dist/lark.js +3 -0
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +6 -0
- package/dist/scaffold/add-channel.d.ts +9 -5
- package/dist/scaffold/add-channel.js +73 -7
- package/dist/tunnel.d.ts +9 -6
- package/dist/tunnel.js +48 -31
- package/package.json +19 -8
- /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
- /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://
|
|
2
|
+
<a href="https://fastagent.sh"><img src="https://raw.githubusercontent.com/fastagent-sh/fastagent/main/assets/hero.png" alt="FastAgent — Vibe first. Then FastAgent. An agent directory becomes a live service in your app, on GitHub, in Telegram, or any channel." width="860"></a>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
[](https://github.com/fastagent-sh/fastagent/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@fastagent-sh/fastagent)
|
|
7
|
-
[](LICENSE)
|
|
7
|
+
[](https://github.com/fastagent-sh/fastagent/blob/main/LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](https://pi.dev)
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
A file-defined agent directory can become a live service. FastAgent takes it out of the terminal and serves it in your Next/Astro app, Telegram, GitHub/webhook events, an API endpoint, or your own channel.
|
|
18
18
|
|
|
19
|
-
Leave the terminal. Become a
|
|
19
|
+
Leave the terminal. Become a live service.
|
|
20
20
|
|
|
21
21
|
- **Add it to your app** — one route, your auth, your database, your host.
|
|
22
22
|
- **Run it as a live service** — Telegram support, GitHub PR review, webhook handler, API endpoint, or custom channel.
|
|
@@ -24,53 +24,57 @@ Leave the terminal. Become a real service.
|
|
|
24
24
|
FastAgent is not a new agent-authoring DSL. You bring the existing definition and project layout; FastAgent provides the serving runtime and adapters around it.
|
|
25
25
|
|
|
26
26
|
<p align="center">
|
|
27
|
-
<img src="https://cdn.jsdelivr.net/gh/fastagent-sh/fastagent@main/assets/demo.svg" alt="
|
|
27
|
+
<img src="https://cdn.jsdelivr.net/gh/fastagent-sh/fastagent@main/assets/demo.svg" alt="Three acts in one terminal: a coding agent vibes a support agent into a directory; fastagent dev serves it live, answering a GitHub pull request, a Telegram message, and an HTTP invoke; fastagent deploy fly --run ships it to a live URL." width="860">
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
30
|
## Why FastAgent
|
|
31
31
|
|
|
32
32
|
Coding agents made it cheap to vibe useful agent directories. The hard part is the next step: local agents live in terminals, but real services receive webhooks, join Telegram, serve product users, and expose stable APIs.
|
|
33
33
|
|
|
34
|
-
FastAgent is the missing bridge from local agent directory to
|
|
34
|
+
FastAgent is the missing bridge from local agent directory to live service.
|
|
35
35
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
38
|
- **Vibe first — a directory is an agent.** Point FastAgent at the `AGENTS.md` + `skills/` you already vibed in a coding agent. Markdown instructions, reusable skills, and TypeScript tools stay as files you inspect, edit, and commit — no new DSL, no framework rewrite.
|
|
39
39
|
- **Channels.** Serve the same agent as a GitHub PR reviewer, a Telegram bot, an HTTP/SSE endpoint, or your own adapter — verified webhooks, streaming replies, group-aware.
|
|
40
40
|
- **Models, tools & skills.** Any model provider (OpenAI, Anthropic, Google, …) via OAuth or API key; typed tools discovered from `tools/` (the filename is the name, Zod-validated); Agent Skills loaded on demand. Built on the open-source [pi](https://github.com/earendil-works/pi) harness.
|
|
41
|
-
- **App embedding — your stack, we plug in.**
|
|
42
|
-
- **Deploy anywhere.**
|
|
43
|
-
|
|
44
|
-
Using a coding agent? Give it [`docs/ai-start.md`](docs/ai-start.md) for an AI-guided setup path.
|
|
41
|
+
- **App embedding — your stack, we plug in.** Mount the agent in your Next / Astro / Hono / Bun / Node route with one handler, or call `invoke` like any function from your own code — your auth, your database, your infra. FastAgent composes with your app, never owns it.
|
|
42
|
+
- **Deploy anywhere.** No build step — the directory is the deployable unit. `fastagent deploy fly|railway` generates the deploy config + a runbook (`--run` drives it to completion); idle agents scale to zero and resume in hundreds of milliseconds on the next webhook. The generated container runs on any Docker host.
|
|
45
43
|
|
|
46
44
|
## Design philosophy
|
|
47
45
|
|
|
48
46
|
FastAgent is built around a small serving contract, app-owned runtime concerns, typed boundaries, and composable adapters.
|
|
49
47
|
|
|
50
|
-
- **Small serving core** — `invoke` decouples channels, agents,
|
|
48
|
+
- **Small serving core** — `invoke` decouples channels, agents, harnesses, and infra.
|
|
51
49
|
- **App-owned runtime** — no takeover of your auth, database, routes, or deployment.
|
|
52
50
|
- **Typed edges** — typed tools, explicit events, boundary validation.
|
|
53
51
|
- **Agent-native shape** — the directory is the deployable unit, and channels drive the same contract.
|
|
54
52
|
|
|
55
|
-
Read [Design principles](docs/principles
|
|
53
|
+
Read the [Design principles](https://fastagent.sh/docs/principles/) for the full rationale.
|
|
56
54
|
|
|
57
55
|
## What we didn't build
|
|
58
56
|
|
|
59
|
-
FastAgent stays a small serving layer, so it never dictates your stack. Capabilities other agent frameworks bake into a platform, we leave to your app, your
|
|
57
|
+
FastAgent stays a small serving layer, so it never dictates your stack. Capabilities other agent frameworks bake into a platform, we leave to your app, your infra, or the agent itself — composed in, not locked in.
|
|
60
58
|
|
|
61
|
-
- **No platform to move to.** No dashboard, no control plane, no runtime you deploy *into* — run it locally, embed it in your app, or ship the directory
|
|
59
|
+
- **No platform to move to.** No dashboard, no control plane, no runtime you deploy *into* — run it locally, embed it in your app, or ship the directory anywhere.
|
|
62
60
|
- **No new format or DSL.** `AGENTS.md`, Agent Skills, TypeScript tools, HTTP/SSE — FastAgent consumes the standards you already use instead of a parallel ecosystem.
|
|
63
61
|
- **No workflow engine.** The agent decides its own steps; for deterministic multi-step orchestration, call `invoke` from your own queue or workflow.
|
|
64
|
-
- **No model or cloud lock-in.** The Agent Handler contract is
|
|
62
|
+
- **No model or cloud lock-in.** The Agent Handler contract is harness-neutral (the [SPEC](https://fastagent.sh/docs/spec/) says *engine* — same seam), with [pi](https://pi.dev) as the built-in harness; bring your own harness and every channel keeps working unchanged.
|
|
65
63
|
|
|
66
64
|
## Install
|
|
67
65
|
|
|
66
|
+
For agents — paste this into Claude Code, Codex, Cursor, or any coding agent that reads the web:
|
|
67
|
+
|
|
68
|
+
> Read https://fastagent.sh/start.md and build an agent in this project.
|
|
69
|
+
|
|
70
|
+
For humans:
|
|
71
|
+
|
|
68
72
|
```bash
|
|
69
73
|
npm i -g @fastagent-sh/fastagent # CLI: fastagent init/dev/start/...
|
|
70
74
|
npm i @fastagent-sh/fastagent # library API for embedding or code tools
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
Requires **Node >= 22.19** (the floor is inherited from the pi
|
|
77
|
+
Requires **Node >= 22.19** (the floor is inherited from the pi harness and `undici`), and also runs under **Bun** (smoke-tested in CI on Bun 1.3; its native fetch replaces the undici path). The npm package ships compiled JavaScript and type declarations.
|
|
74
78
|
|
|
75
79
|
## Quickstart
|
|
76
80
|
|
|
@@ -133,20 +137,20 @@ const agent = createPiAgent({
|
|
|
133
137
|
|
|
134
138
|
| Document | Purpose |
|
|
135
139
|
|---|---|
|
|
136
|
-
| [
|
|
137
|
-
| [
|
|
138
|
-
| [
|
|
139
|
-
| [
|
|
140
|
-
| [
|
|
141
|
-
| [
|
|
142
|
-
| [
|
|
143
|
-
| [
|
|
144
|
-
| [
|
|
145
|
-
| [
|
|
146
|
-
| [
|
|
147
|
-
| [
|
|
148
|
-
| [
|
|
149
|
-
| [
|
|
140
|
+
| [Documentation index](https://fastagent.sh/docs/) | Documentation map |
|
|
141
|
+
| [Quickstart](https://fastagent.sh/docs/quickstart/) | Scaffold, run, add a tool, and start |
|
|
142
|
+
| [Configuration](https://fastagent.sh/docs/configuration/) | Configure model, auth, ports, sessions, tools, and channels |
|
|
143
|
+
| [Design principles](https://fastagent.sh/docs/principles/) | Design choices, core primitives, and non-goals |
|
|
144
|
+
| [CLI reference](https://fastagent.sh/docs/cli/) | CLI commands and flags |
|
|
145
|
+
| [Embedding](https://fastagent.sh/docs/embedding/) | Use FastAgent as a library inside your own app |
|
|
146
|
+
| [Channels](https://fastagent.sh/docs/channels/) | Add webhook/bot channels |
|
|
147
|
+
| [Deploy](https://fastagent.sh/docs/deploy/) | Ship the directory to Fly, Railway, or any Docker host |
|
|
148
|
+
| [GitHub](https://fastagent.sh/docs/github/) / [Telegram](https://fastagent.sh/docs/telegram/) / [Feishu and Lark](https://fastagent.sh/docs/feishu/) | First-party channel guides |
|
|
149
|
+
| [Channel development](https://fastagent.sh/docs/channel-development/) | Build custom channel adapters |
|
|
150
|
+
| [API reference](https://fastagent.sh/docs/api-reference/) | Public TypeScript API reference |
|
|
151
|
+
| [Troubleshooting](https://fastagent.sh/docs/troubleshooting/) | Common setup/runtime issues |
|
|
152
|
+
| [Agent Handler SPEC](https://fastagent.sh/docs/spec/) | Agent Handler protocol v0.1 |
|
|
153
|
+
| [Core design](https://fastagent.sh/docs/design/core/) | Maintainer architecture notes |
|
|
150
154
|
|
|
151
155
|
## Public API surface & stability
|
|
152
156
|
|
|
@@ -166,7 +170,9 @@ Subpath exports:
|
|
|
166
170
|
- `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel/host kit, schedules;
|
|
167
171
|
- `@fastagent-sh/fastagent/pi` — the pi reference implementation;
|
|
168
172
|
- `@fastagent-sh/fastagent/github` — GitHub webhook channel;
|
|
169
|
-
- `@fastagent-sh/fastagent/telegram` — Telegram bot channel
|
|
173
|
+
- `@fastagent-sh/fastagent/telegram` — Telegram bot channel;
|
|
174
|
+
- `@fastagent-sh/fastagent/feishu` — canonical Feishu bot channel (飞书, open.feishu.cn);
|
|
175
|
+
- `@fastagent-sh/fastagent/lark` — Lark-international compatibility profile over the Feishu engine.
|
|
170
176
|
|
|
171
177
|
## Repository layout
|
|
172
178
|
|
|
@@ -191,19 +197,19 @@ The neutral contract leaves room for capabilities that are not complete product
|
|
|
191
197
|
- **Durable execution** — Telegram accepted turns replay at least once today; general durability and exactly-once execution remain future backend work.
|
|
192
198
|
- **Sandboxed execution** — `ExecutionEnv` is an assembly seam, but the pi coding tools and project-context loader are still local; a complete sandbox adapter is future work.
|
|
193
199
|
- **Observability export** — leveled logs and per-turn traces exist today; an OpenTelemetry exporter does not.
|
|
194
|
-
- **More
|
|
200
|
+
- **More harness bindings and channels** — pi is the built-in harness; another harness can implement the Agent contract, and community channels can use the channel kit.
|
|
195
201
|
- **More deploy targets** — Fly and Railway ship today; the generated container is the portable path for other hosts.
|
|
196
202
|
|
|
197
|
-
See [Contributing](CONTRIBUTING.md) if one of these is the problem you want to work on.
|
|
203
|
+
See [Contributing](https://github.com/fastagent-sh/fastagent/blob/main/CONTRIBUTING.md) if one of these is the problem you want to work on.
|
|
198
204
|
|
|
199
205
|
## Project
|
|
200
206
|
|
|
201
|
-
- [Contributing](CONTRIBUTING.md)
|
|
202
|
-
- [Security policy](SECURITY.md)
|
|
207
|
+
- [Contributing](https://github.com/fastagent-sh/fastagent/blob/main/CONTRIBUTING.md)
|
|
208
|
+
- [Security policy](https://github.com/fastagent-sh/fastagent/blob/main/SECURITY.md)
|
|
203
209
|
|
|
204
210
|
## Acknowledgements
|
|
205
211
|
|
|
206
|
-
FastAgent stands on open source. The
|
|
212
|
+
FastAgent stands on open source. The built-in harness is **[pi](https://github.com/earendil-works/pi)** ([pi.dev](https://pi.dev)) — its agent loop, multi-provider LLM API, and the interactive TUI that `fastagent chat` drives.
|
|
207
213
|
|
|
208
214
|
It also depends on, and is grateful to, [zod](https://github.com/colinhacks/zod), [undici](https://github.com/nodejs/undici), [chokidar](https://github.com/paulmillr/chokidar), [giget](https://github.com/unjs/giget), [@clack/prompts](https://github.com/bombshell-dev/clack), [ignore](https://github.com/kaelzhang/node-ignore), and [octokit/webhooks](https://github.com/octokit/webhooks).
|
|
209
215
|
|
|
@@ -211,4 +217,4 @@ The scaffolded `writing-great-skills` skill is vendored from [mattpocock/skills]
|
|
|
211
217
|
|
|
212
218
|
## License
|
|
213
219
|
|
|
214
|
-
[MIT](LICENSE). Runtime dependencies use permissive open-source licenses and are installed as separate npm packages; the vendored `writing-great-skills` scaffold includes its own license.
|
|
220
|
+
[MIT](https://github.com/fastagent-sh/fastagent/blob/main/LICENSE). Runtime dependencies use permissive open-source licenses and are installed as separate npm packages; the vendored `writing-great-skills` scaffold includes its own license.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FeishuCloudKind } from "./cloud.ts";
|
|
2
|
+
/** What the bootstrap needs from the API pipeline (subset of FeishuApi; injectable in tests). */
|
|
3
|
+
interface EventSubscriptionPatcher {
|
|
4
|
+
updateEventSubscription(appId: string, cfg: {
|
|
5
|
+
subscriptionType: "webhook";
|
|
6
|
+
requestUrl: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface FeishuBootstrapTokenOptions {
|
|
10
|
+
api: EventSubscriptionPatcher;
|
|
11
|
+
appId: string;
|
|
12
|
+
/** Canonical Feishu by default; Lark compatibility binds its own route explicitly. */
|
|
13
|
+
kind?: FeishuCloudKind;
|
|
14
|
+
/** Expose local `port` on a public URL (production: startCloudflareTunnel; tests: loopback). */
|
|
15
|
+
startTunnel: (port: number) => Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
close(): void;
|
|
18
|
+
} | undefined>;
|
|
19
|
+
/** Budget for the whole capture (the challenge normally lands within the PATCH round-trip). */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** PATCH attempts × delay — the PATCH is the one and only readiness probe (see below). */
|
|
22
|
+
patchAttempts?: number;
|
|
23
|
+
patchRetryMs?: number;
|
|
24
|
+
/** Retry classifier. Default: retry every PATCH failure (Feishu edge warm-up compatibility).
|
|
25
|
+
* Lark onboarding rejects a definitive config-route 404 immediately so it can fall back by hand. */
|
|
26
|
+
shouldRetryPatch?: (error: unknown) => boolean;
|
|
27
|
+
/** Progress hooks: production prints the assigned URL/retries; tests keep transport IO injected. */
|
|
28
|
+
onTunnelReady?: (url: string) => void;
|
|
29
|
+
onPatchRetry?: (info: {
|
|
30
|
+
error: unknown;
|
|
31
|
+
attempt: number;
|
|
32
|
+
attempts: number;
|
|
33
|
+
retryMs: number;
|
|
34
|
+
}) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run the bootstrap (module header). Resolves with the app's Verification Token; rejects with a
|
|
38
|
+
* plain, actionable Error (tunnel unavailable, PATCH refused, challenge never arrived) — the caller
|
|
39
|
+
* degrades to the manual console instruction.
|
|
40
|
+
*/
|
|
41
|
+
export declare function bootstrapFeishuVerificationToken(options: FeishuBootstrapTokenOptions): Promise<string>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification-Token bootstrap — the token has NO read API (field-tested 2026-07: the v6 app detail
|
|
3
|
+
* never returns `encryption`, and the v7 config route only supports PATCH). The platform's only
|
|
4
|
+
* programmatic delivery is the `url_verification` challenge it POSTs to the request URL while an
|
|
5
|
+
* event-subscription PATCH is verified — the challenge body carries `token`, the app's Verification
|
|
6
|
+
* Token. So `add feishu`'s create flow finishes the job by: standing up a throwaway local responder,
|
|
7
|
+
* exposing it on an ephemeral quick tunnel, PATCHing the app's event subscription at it, capturing
|
|
8
|
+
* the token from the challenge, and tearing everything down. `dev --tunnel` / `deploy … --run`
|
|
9
|
+
* re-PATCH the request URL onto the real server later (the token is app-level and survives that).
|
|
10
|
+
*
|
|
11
|
+
* Security: the serving path keeps its strict non-empty-token requirement — no bootstrap mode there.
|
|
12
|
+
* The throwaway URL is random (unguessable), lives for seconds, and only the FIRST challenge is
|
|
13
|
+
* accepted, immediately after our own credential-authenticated PATCH.
|
|
14
|
+
*/
|
|
15
|
+
import { createServer } from "node:http";
|
|
16
|
+
/**
|
|
17
|
+
* Run the bootstrap (module header). Resolves with the app's Verification Token; rejects with a
|
|
18
|
+
* plain, actionable Error (tunnel unavailable, PATCH refused, challenge never arrived) — the caller
|
|
19
|
+
* degrades to the manual console instruction.
|
|
20
|
+
*/
|
|
21
|
+
export async function bootstrapFeishuVerificationToken(options) {
|
|
22
|
+
let capturedToken;
|
|
23
|
+
const token = new Promise((resolve) => {
|
|
24
|
+
capturedToken = resolve;
|
|
25
|
+
});
|
|
26
|
+
// The throwaway responder: answers /health (diagnostics) and the url_verification challenge
|
|
27
|
+
// (echo `challenge` back), capturing `token` from the FIRST challenge only.
|
|
28
|
+
const server = createServer((req, res) => {
|
|
29
|
+
if (req.method === "GET") {
|
|
30
|
+
res.end("ok");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let body = "";
|
|
34
|
+
req.on("data", (d) => {
|
|
35
|
+
body += String(d);
|
|
36
|
+
});
|
|
37
|
+
req.on("end", () => {
|
|
38
|
+
try {
|
|
39
|
+
const j = JSON.parse(body);
|
|
40
|
+
if (j.type === "url_verification" && typeof j.challenge === "string" && typeof j.token === "string") {
|
|
41
|
+
capturedToken?.(j.token);
|
|
42
|
+
capturedToken = undefined; // first challenge only
|
|
43
|
+
res.setHeader("content-type", "application/json");
|
|
44
|
+
res.end(JSON.stringify({ challenge: j.challenge }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* not the challenge — fall through to the empty ACK */
|
|
50
|
+
}
|
|
51
|
+
res.end("{}");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
55
|
+
const address = server.address();
|
|
56
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
57
|
+
const tunnel = await options.startTunnel(port);
|
|
58
|
+
try {
|
|
59
|
+
if (!tunnel)
|
|
60
|
+
throw new Error("no tunnel came up (is cloudflared installed?)");
|
|
61
|
+
options.onTunnelReady?.(tunnel.url);
|
|
62
|
+
// Do NOT health-poll the edge here: "can WE reach it" says nothing about platform→edge, the path
|
|
63
|
+
// the challenge uses (field-tested: local negative-DNS caching failed while that path worked).
|
|
64
|
+
// PATCH immediately; it is the real probe because success requires the challenge round-trip.
|
|
65
|
+
const attempts = options.patchAttempts ?? 8;
|
|
66
|
+
for (let attempt = 1;; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
await options.api.updateEventSubscription(options.appId, {
|
|
69
|
+
subscriptionType: "webhook",
|
|
70
|
+
requestUrl: `${tunnel.url}/${options.kind ?? "feishu"}`,
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (attempt >= attempts || options.shouldRetryPatch?.(e) === false)
|
|
76
|
+
throw e;
|
|
77
|
+
const retryMs = options.patchRetryMs ?? 10_000;
|
|
78
|
+
options.onPatchRetry?.({ error: e, attempt, attempts, retryMs });
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const winner = await Promise.race([
|
|
83
|
+
token,
|
|
84
|
+
new Promise((resolve) => setTimeout(() => resolve(undefined), options.timeoutMs ?? 30_000)),
|
|
85
|
+
]);
|
|
86
|
+
if (winner === undefined)
|
|
87
|
+
throw new Error("the registration challenge never arrived");
|
|
88
|
+
return winner;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
tunnel?.close();
|
|
92
|
+
server.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card JSON builders — PURE: the streaming-card entity (JSON 2.0) the live preview creates, the settled
|
|
3
|
+
* final card, and the message content that mounts a card entity into a chat. Kept out of preview.ts so
|
|
4
|
+
* the card DSL is data-in → string-out and testable without the pump.
|
|
5
|
+
*
|
|
6
|
+
* The preview is ONE markdown element (`element_id` below) inside a card with `streaming_mode` on:
|
|
7
|
+
* the pump PUTs full-text snapshots at that element (feishu-api.ts `updateCardElement`) and the client
|
|
8
|
+
* renders the typewriter effect. Settling replaces the whole entity (`updateCard`) with the same
|
|
9
|
+
* element, `streaming_mode` off — one write flips content and mode together.
|
|
10
|
+
*
|
|
11
|
+
* Budget: a card entity is capped at 30 KB, so the final answer's card chunk (and the live view) stay
|
|
12
|
+
* well under it; longer answers overflow into follow-up messages (preview.ts owns that policy).
|
|
13
|
+
*/
|
|
14
|
+
/** The one streamed element's id — shared by create (card.ts) and update (preview.ts). */
|
|
15
|
+
export declare const ANSWER_ELEMENT_ID = "answer";
|
|
16
|
+
/** Byte budget for markdown carried by ONE card (entity cap 30 KB minus JSON envelope + escaping room). */
|
|
17
|
+
export declare const CARD_MARKDOWN_MAX_BYTES: number;
|
|
18
|
+
/**
|
|
19
|
+
* The answer's first line as plain text — what the chat list and the push notification show for the
|
|
20
|
+
* settled card (`config.summary.content`). Without it a card message previews as a generic "[Card]"
|
|
21
|
+
* placeholder: the user's notification would never carry the actual answer. Markdown is stripped
|
|
22
|
+
* lightly (this is a one-line teaser, not a renderer): fenced code dropped, links/images → their text,
|
|
23
|
+
* emphasis/heading/list markers removed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function cardSummary(markdown: string): string;
|
|
26
|
+
/** The live-preview card entity: streaming on, seeded with the placeholder/first view. */
|
|
27
|
+
export declare function streamingCardJson(initial: string): string;
|
|
28
|
+
/** The settled card: final markdown, streaming off (stops the client's streaming affordance), plus
|
|
29
|
+
* the answer-derived summary so the chat list / notification shows the reply, not "[Card]". */
|
|
30
|
+
export declare function finalCardJson(markdown: string): string;
|
|
31
|
+
/** The `interactive` message content that mounts a card ENTITY (vs an inline static card). */
|
|
32
|
+
export declare function cardEntityContent(cardId: string): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card JSON builders — PURE: the streaming-card entity (JSON 2.0) the live preview creates, the settled
|
|
3
|
+
* final card, and the message content that mounts a card entity into a chat. Kept out of preview.ts so
|
|
4
|
+
* the card DSL is data-in → string-out and testable without the pump.
|
|
5
|
+
*
|
|
6
|
+
* The preview is ONE markdown element (`element_id` below) inside a card with `streaming_mode` on:
|
|
7
|
+
* the pump PUTs full-text snapshots at that element (feishu-api.ts `updateCardElement`) and the client
|
|
8
|
+
* renders the typewriter effect. Settling replaces the whole entity (`updateCard`) with the same
|
|
9
|
+
* element, `streaming_mode` off — one write flips content and mode together.
|
|
10
|
+
*
|
|
11
|
+
* Budget: a card entity is capped at 30 KB, so the final answer's card chunk (and the live view) stay
|
|
12
|
+
* well under it; longer answers overflow into follow-up messages (preview.ts owns that policy).
|
|
13
|
+
*/
|
|
14
|
+
import { truncateCodePointPrefix } from "./text.js";
|
|
15
|
+
/** The one streamed element's id — shared by create (card.ts) and update (preview.ts). */
|
|
16
|
+
export const ANSWER_ELEMENT_ID = "answer";
|
|
17
|
+
/** Byte budget for markdown carried by ONE card (entity cap 30 KB minus JSON envelope + escaping room). */
|
|
18
|
+
export const CARD_MARKDOWN_MAX_BYTES = 20 * 1024;
|
|
19
|
+
/** Character budget for the settled card's summary (the chat-list / push-notification preview). */
|
|
20
|
+
const SUMMARY_MAX_CHARS = 60;
|
|
21
|
+
/**
|
|
22
|
+
* The answer's first line as plain text — what the chat list and the push notification show for the
|
|
23
|
+
* settled card (`config.summary.content`). Without it a card message previews as a generic "[Card]"
|
|
24
|
+
* placeholder: the user's notification would never carry the actual answer. Markdown is stripped
|
|
25
|
+
* lightly (this is a one-line teaser, not a renderer): fenced code dropped, links/images → their text,
|
|
26
|
+
* emphasis/heading/list markers removed.
|
|
27
|
+
*/
|
|
28
|
+
export function cardSummary(markdown) {
|
|
29
|
+
const line = markdown
|
|
30
|
+
.replace(/```[\s\S]*?(```|$)/g, " ") // fenced code is never a readable one-line preview
|
|
31
|
+
.split("\n")
|
|
32
|
+
.map((l) => l
|
|
33
|
+
.replace(/^\s{0,3}(?:#{1,6}|>|[-*+]|\d+[.)])\s+/, "") // heading / quote / list markers
|
|
34
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") // images → alt text
|
|
35
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links → link text
|
|
36
|
+
.replace(/[*_~`]/g, "")
|
|
37
|
+
.trim())
|
|
38
|
+
.find((l) => l !== "") ?? "";
|
|
39
|
+
return truncateCodePointPrefix(line, SUMMARY_MAX_CHARS);
|
|
40
|
+
}
|
|
41
|
+
function cardJson(markdown, streaming, summary) {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
schema: "2.0",
|
|
44
|
+
config: {
|
|
45
|
+
streaming_mode: streaming,
|
|
46
|
+
update_multi: true,
|
|
47
|
+
// Only the settled card sets a summary — while streaming, the platform's default (a localized
|
|
48
|
+
// "[Generating…]") is better than any fixed text we could pin.
|
|
49
|
+
...(summary ? { summary: { content: summary } } : {}),
|
|
50
|
+
},
|
|
51
|
+
body: { elements: [{ tag: "markdown", content: markdown, element_id: ANSWER_ELEMENT_ID }] },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The live-preview card entity: streaming on, seeded with the placeholder/first view. */
|
|
55
|
+
export function streamingCardJson(initial) {
|
|
56
|
+
return cardJson(initial, true);
|
|
57
|
+
}
|
|
58
|
+
/** The settled card: final markdown, streaming off (stops the client's streaming affordance), plus
|
|
59
|
+
* the answer-derived summary so the chat list / notification shows the reply, not "[Card]". */
|
|
60
|
+
export function finalCardJson(markdown) {
|
|
61
|
+
return cardJson(markdown, false, cardSummary(markdown));
|
|
62
|
+
}
|
|
63
|
+
/** The `interactive` message content that mounts a card ENTITY (vs an inline static card). */
|
|
64
|
+
export function cardEntityContent(cardId) {
|
|
65
|
+
return JSON.stringify({ type: "card", data: { card_id: cardId } });
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu is the canonical implementation. Lark international speaks the same protocol but trails the
|
|
3
|
+
* primary cloud in control-plane capabilities, so it binds the Feishu engine through this explicit
|
|
4
|
+
* compatibility profile instead of becoming the engine's naming/design center.
|
|
5
|
+
*/
|
|
6
|
+
export type FeishuCloudKind = "feishu" | "lark";
|
|
7
|
+
export interface FeishuCloudProfile {
|
|
8
|
+
kind: FeishuCloudKind;
|
|
9
|
+
envPrefix: "FEISHU" | "LARK";
|
|
10
|
+
apiBase: string;
|
|
11
|
+
capabilities: {
|
|
12
|
+
appCreation: "scan-to-create" | "guided-console";
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare const FEISHU_CLOUD: FeishuCloudProfile;
|
|
16
|
+
export declare const LARK_COMPAT_CLOUD: FeishuCloudProfile;
|
|
17
|
+
export declare function cloudFor(kind: FeishuCloudKind): FeishuCloudProfile;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FEISHU_CLOUD = {
|
|
2
|
+
kind: "feishu",
|
|
3
|
+
envPrefix: "FEISHU",
|
|
4
|
+
apiBase: "https://open.feishu.cn",
|
|
5
|
+
capabilities: {
|
|
6
|
+
appCreation: "scan-to-create",
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
export const LARK_COMPAT_CLOUD = {
|
|
10
|
+
kind: "lark",
|
|
11
|
+
envPrefix: "LARK",
|
|
12
|
+
apiBase: "https://open.larksuite.com",
|
|
13
|
+
capabilities: {
|
|
14
|
+
appCreation: "guided-console",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export function cloudFor(kind) {
|
|
18
|
+
return kind === "feishu" ? FEISHU_CLOUD : LARK_COMPAT_CLOUD;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Constant-time string equality (padded to equal length first — timingSafeEqual demands it). */
|
|
2
|
+
export declare function timingSafeEqualStr(a: string, b: string): boolean;
|
|
3
|
+
/** Decrypt an `{"encrypt": …}` event payload to its plaintext JSON string. Throws on malformed input
|
|
4
|
+
* or a wrong key (bad padding) — the caller turns that into a 4xx, never a silent drop. */
|
|
5
|
+
export declare function decryptEvent(encryptKey: string, encryptB64: string): string;
|
|
6
|
+
/** The expected `X-Lark-Signature` for a request: sha256(timestamp + nonce + encryptKey + rawBody) hex. */
|
|
7
|
+
export declare function eventSignature(encryptKey: string, timestamp: string, nonce: string, rawBody: string): string;
|
|
8
|
+
/** Whether a request's signature headers verify against the raw body (constant-time). */
|
|
9
|
+
export declare function verifySignature(encryptKey: string, headers: {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
nonce: string;
|
|
12
|
+
signature: string;
|
|
13
|
+
}, rawBody: string): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Feishu webhook security (reused by Lark compatibility) — PURE: AES event decryption and request
|
|
3
|
+
* signature, exactly as the open platform defines them. When an Encrypt Key is configured in the
|
|
4
|
+
* developer console, every event arrives as `{"encrypt": "<base64>"}` with signature headers; without
|
|
5
|
+
* one, events arrive in plaintext and carry only the verification token in the body. feishu.ts owns the
|
|
6
|
+
* fail-closed policy (which checks run when); this module owns the math.
|
|
7
|
+
*
|
|
8
|
+
* - Decryption: AES-256-CBC. The key is sha256(encryptKey); the base64 payload is IV (16 bytes) ‖
|
|
9
|
+
* ciphertext; the plaintext is the event JSON (PKCS#7 padding handled by the cipher).
|
|
10
|
+
* - Signature: `X-Lark-Signature = sha256(timestamp + nonce + encryptKey + rawBody)` hex, where
|
|
11
|
+
* rawBody is the VERBATIM request body (the encrypted form) — computed over bytes, so the caller
|
|
12
|
+
* must pass the raw text, never a re-serialization.
|
|
13
|
+
*
|
|
14
|
+
* Comparisons are constant-time (timingSafeEqual) so neither the signature check nor the verification-
|
|
15
|
+
* token check leaks a timing signal.
|
|
16
|
+
*/
|
|
17
|
+
import { createDecipheriv, createHash, timingSafeEqual } from "node:crypto";
|
|
18
|
+
/** Constant-time string equality (padded to equal length first — timingSafeEqual demands it). */
|
|
19
|
+
export function timingSafeEqualStr(a, b) {
|
|
20
|
+
const ab = Buffer.from(a);
|
|
21
|
+
const bb = Buffer.from(b);
|
|
22
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
23
|
+
}
|
|
24
|
+
/** Decrypt an `{"encrypt": …}` event payload to its plaintext JSON string. Throws on malformed input
|
|
25
|
+
* or a wrong key (bad padding) — the caller turns that into a 4xx, never a silent drop. */
|
|
26
|
+
export function decryptEvent(encryptKey, encryptB64) {
|
|
27
|
+
const key = createHash("sha256").update(encryptKey, "utf8").digest();
|
|
28
|
+
const buf = Buffer.from(encryptB64, "base64");
|
|
29
|
+
if (buf.length <= 16)
|
|
30
|
+
throw new Error("encrypted event payload is too short to carry an IV + ciphertext");
|
|
31
|
+
const decipher = createDecipheriv("aes-256-cbc", key, buf.subarray(0, 16));
|
|
32
|
+
return Buffer.concat([decipher.update(buf.subarray(16)), decipher.final()]).toString("utf8");
|
|
33
|
+
}
|
|
34
|
+
/** The expected `X-Lark-Signature` for a request: sha256(timestamp + nonce + encryptKey + rawBody) hex. */
|
|
35
|
+
export function eventSignature(encryptKey, timestamp, nonce, rawBody) {
|
|
36
|
+
return createHash("sha256").update(`${timestamp}${nonce}${encryptKey}${rawBody}`, "utf8").digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/** Whether a request's signature headers verify against the raw body (constant-time). */
|
|
39
|
+
export function verifySignature(encryptKey, headers, rawBody) {
|
|
40
|
+
return timingSafeEqualStr(eventSignature(encryptKey, headers.timestamp, headers.nonce, rawBody), headers.signature);
|
|
41
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ImageRef } from "../../agent.ts";
|
|
2
|
+
import type { FeishuCloudKind } from "./cloud.ts";
|
|
3
|
+
/** Where a reply goes: a chat, optionally quote-replying a message (in-thread in topic groups). */
|
|
4
|
+
export interface FeishuTarget {
|
|
5
|
+
chatId: string;
|
|
6
|
+
/** Message to reply to (the summoning message). Set in groups so the answer threads under the asker. */
|
|
7
|
+
replyTo?: string;
|
|
8
|
+
/** Reply into the message's topic thread (topic groups). */
|
|
9
|
+
replyInThread?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** A downloaded inbound file: an absolute local path the agent's tools (read/bash) can open. */
|
|
12
|
+
export interface DownloadedFile {
|
|
13
|
+
path: string;
|
|
14
|
+
name: string;
|
|
15
|
+
size: number;
|
|
16
|
+
}
|
|
17
|
+
/** Whether an error is the platform's "card streaming closed/timed out" reject — preview.ts re-enables
|
|
18
|
+
* streaming once when it sees this. A plain code check on the typed error, exported instead of the
|
|
19
|
+
* class so no caller can construct/throw one. */
|
|
20
|
+
export declare function isCardStreamingClosed(e: unknown): boolean;
|
|
21
|
+
/** Whether the platform origin has no application-config route at all. Onboarding uses this narrow
|
|
22
|
+
* signal to fall back to a manual token/mode setup; auth/scope/network failures must remain visible. */
|
|
23
|
+
export declare function isFeishuConfigApiMissing(e: unknown): boolean;
|
|
24
|
+
/** Whether a registration-PATCH failure is transient weather worth retrying: network/DNS/timeouts, or
|
|
25
|
+
* the platform's 210042 "request_url validation failed" while its own path to a fresh tunnel edge
|
|
26
|
+
* warms up. Everything else (scope, auth, app under review, the config-route 404 above) is definitive
|
|
27
|
+
* — the registrars and the token bootstrap share this one classifier. */
|
|
28
|
+
export declare function isTransientFeishuRegistrationError(e: unknown): boolean;
|
|
29
|
+
export interface FeishuApiOptions {
|
|
30
|
+
/** Branded diagnostics; defaults to the canonical Feishu cloud. */
|
|
31
|
+
kind?: FeishuCloudKind;
|
|
32
|
+
/** API origin: `https://open.feishu.cn` (Feishu) or `https://open.larksuite.com` (Lark intl). */
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
appId: string;
|
|
35
|
+
appSecret: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The Feishu Open API client: one instance per channel, holding the token cache. Every method rides the
|
|
39
|
+
* single pipeline (module header). Throws {@link FeishuApiError} on any failure.
|
|
40
|
+
*/
|
|
41
|
+
export interface FeishuApi {
|
|
42
|
+
/** Validate appId/appSecret by acquiring the tenant token through this pipeline. Does not require
|
|
43
|
+
* the bot capability (unlike botInfo), so guided onboarding can fail before persisting a typo. */
|
|
44
|
+
verifyCredentials(): Promise<void>;
|
|
45
|
+
/** GET /bot/v3/info — the bot's own identity (open_id drives @mention summon). */
|
|
46
|
+
botInfo(): Promise<{
|
|
47
|
+
openId?: string;
|
|
48
|
+
appName?: string;
|
|
49
|
+
}>;
|
|
50
|
+
/** Send a message to a chat; returns the new message_id (undefined if the body carried none). */
|
|
51
|
+
sendMessage(chatId: string, msgType: string, content: string): Promise<string | undefined>;
|
|
52
|
+
/** Reply to a message (quote; `replyInThread` stays inside a topic group's thread). */
|
|
53
|
+
replyMessage(messageId: string, msgType: string, content: string, opts?: {
|
|
54
|
+
replyInThread?: boolean;
|
|
55
|
+
}): Promise<string | undefined>;
|
|
56
|
+
/** Send `text`, split at the platform's size cap: ordinary groups quote only the first chunk; topic
|
|
57
|
+
* groups reply_in_thread on every chunk. Returns the FIRST message_id. */
|
|
58
|
+
sendText(target: FeishuTarget, text: string): Promise<string | undefined>;
|
|
59
|
+
/** Edit a sent text message in place (PUT; the platform caps edits at 20 per message). */
|
|
60
|
+
editTextMessage(messageId: string, text: string): Promise<void>;
|
|
61
|
+
/** Recall (delete) a message the bot sent. */
|
|
62
|
+
deleteMessage(messageId: string): Promise<void>;
|
|
63
|
+
/** Fetch one message (the reply-referent path). Undefined when the API returns no item. */
|
|
64
|
+
getMessage(messageId: string): Promise<{
|
|
65
|
+
message_id?: string;
|
|
66
|
+
msg_type?: string;
|
|
67
|
+
body?: {
|
|
68
|
+
content?: string;
|
|
69
|
+
};
|
|
70
|
+
mentions?: unknown[];
|
|
71
|
+
sender?: unknown;
|
|
72
|
+
} | undefined>;
|
|
73
|
+
/** Download a message resource (image/file bytes). Caps at {@link MAX_DOWNLOAD_BYTES}. */
|
|
74
|
+
downloadResource(messageId: string, fileKey: string, type: "image" | "file"): Promise<{
|
|
75
|
+
bytes: Buffer;
|
|
76
|
+
contentType?: string;
|
|
77
|
+
}>;
|
|
78
|
+
/** Download an image resource as a vision ImageRef. */
|
|
79
|
+
fetchImage(messageId: string, fileKey: string): Promise<ImageRef>;
|
|
80
|
+
/** Download a file resource to `<filesDir>/<chat>/<name>`. */
|
|
81
|
+
fetchFile(messageId: string, fileKey: string, name: string, chatId: string, filesDir: string): Promise<DownloadedFile>;
|
|
82
|
+
/** Read the app's own event-security config (the platform-generated verification token / encrypt
|
|
83
|
+
* key) — the scan-to-create flow copies these into .env so the operator never opens the console. */
|
|
84
|
+
getAppConfig(appId: string): Promise<{
|
|
85
|
+
verificationToken?: string;
|
|
86
|
+
encryptionKey?: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** Update the app's own event subscription (application-v7 config PATCH — tenant token can only
|
|
89
|
+
* operate on itself; the request-URL change takes effect immediately, no version publish). The
|
|
90
|
+
* platform VERIFIES `requestUrl` with a url_verification challenge during this call, so the server
|
|
91
|
+
* behind it must already be answering. */
|
|
92
|
+
updateEventSubscription(appId: string, cfg: {
|
|
93
|
+
subscriptionType: "webhook";
|
|
94
|
+
requestUrl: string;
|
|
95
|
+
}): Promise<void>;
|
|
96
|
+
/** Create a card entity (card JSON 2.0). Returns its card_id. */
|
|
97
|
+
createCard(cardJson: string): Promise<string>;
|
|
98
|
+
/** Stream-update a card element's text (full-content snapshot + strictly increasing sequence). */
|
|
99
|
+
updateCardElement(cardId: string, elementId: string, content: string, sequence: number): Promise<void>;
|
|
100
|
+
/** Replace a card entity's content (the settle write; also flips streaming_mode off via the JSON). */
|
|
101
|
+
updateCard(cardId: string, cardJson: string, sequence: number): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
/** The platform caps a text-message request body at 150 KB; stay well under it (the content is a JSON
|
|
104
|
+
* envelope around the text, and multi-byte characters inflate the byte count). */
|
|
105
|
+
export declare const FEISHU_MAX_TEXT_BYTES: number;
|
|
106
|
+
/** Split text into chunks whose UTF-8 size fits the message cap, preferring a newline boundary. */
|
|
107
|
+
export declare function chunkFeishuText(text: string, maxBytes?: number): string[];
|
|
108
|
+
export declare function createFeishuApi(opts: FeishuApiOptions): FeishuApi;
|