@fastagent-sh/fastagent 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/kernel.js +3 -0
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** What a Telegram bot's setup asks for (channels/setup.ts): its token from BotFather, and for a webhook a secret. */
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { SetupRefused, pasteOne } from "../setup.js";
|
|
4
|
+
import { TelegramApiError, callApi } from "./telegram-api.js";
|
|
5
|
+
const TOKEN = /^\d+:[A-Za-z0-9_-]{30,}$/;
|
|
6
|
+
export const telegramSetup = ({ way }) => ({
|
|
7
|
+
finish: () => [],
|
|
8
|
+
values: [
|
|
9
|
+
pasteOne({
|
|
10
|
+
name: "TELEGRAM_BOT_TOKEN",
|
|
11
|
+
label: "Bot token",
|
|
12
|
+
guide: {
|
|
13
|
+
instructions: "Message @BotFather, send /newbot, follow its questions, and copy the token it gives you.",
|
|
14
|
+
url: "https://t.me/BotFather",
|
|
15
|
+
},
|
|
16
|
+
async check(value) {
|
|
17
|
+
if (!TOKEN.test(value)) {
|
|
18
|
+
throw new SetupRefused("That is not a bot token.", {
|
|
19
|
+
fix: "BotFather's token looks like 123456789:AAH…, a number, a colon and a long string.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const me = await callApi("https://api.telegram.org", value, "getMe", {});
|
|
24
|
+
return me.username ? `bot @${me.username}` : "bot";
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof TelegramApiError && (error.status === 401 || error.status === 404)) {
|
|
28
|
+
throw new SetupRefused("Telegram does not accept this token.", {
|
|
29
|
+
fix: "Copy it again from BotFather (/mybots → the bot → API Token).",
|
|
30
|
+
why: error.message,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
// Only a webhook has an inbound request to verify: a polling bot asks Telegram for its updates.
|
|
38
|
+
...(way === "webhook"
|
|
39
|
+
? [{ kind: "make", name: "TELEGRAM_SECRET_TOKEN", make: () => randomBytes(24).toString("hex") }]
|
|
40
|
+
: []),
|
|
41
|
+
],
|
|
42
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ImageAttachment as ImageRef } from "../kit/invoke-turn-kit.ts";
|
|
2
2
|
import type { CallOptions } from "../kit/transport.ts";
|
|
3
|
+
import type { TelegramUpdate } from "./parse.ts";
|
|
3
4
|
/** Telegram's hard text limit per message. */
|
|
4
5
|
export declare const TELEGRAM_MAX_TEXT = 4096;
|
|
5
6
|
/** Where a reply goes: a chat, optionally a thread (Threaded Mode), optionally replying to a message. */
|
|
@@ -41,6 +42,31 @@ interface Api {
|
|
|
41
42
|
file_size?: number;
|
|
42
43
|
};
|
|
43
44
|
setWebhook: unknown;
|
|
45
|
+
deleteWebhook: unknown;
|
|
46
|
+
getWebhookInfo: {
|
|
47
|
+
url?: string;
|
|
48
|
+
};
|
|
49
|
+
getUpdates: TelegramUpdate[];
|
|
50
|
+
}
|
|
51
|
+
/** What a Telegram call may be told beyond the shared budget: a longer wait (a long poll) and a way to be cut short. */
|
|
52
|
+
export interface TelegramCallOptions extends CallOptions {
|
|
53
|
+
/** Replaces the per-attempt timeout; a long poll must outlast the wait it asks Telegram for. */
|
|
54
|
+
readonly timeoutMs?: number;
|
|
55
|
+
/** Ends the call early, as a failure the caller reads as its own cancellation. */
|
|
56
|
+
readonly signal?: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
/** A named Bot API failure. `status` 0 = the transport itself failed (network error / timeout) before
|
|
59
|
+
* any HTTP status existed; otherwise the HTTP status with Telegram's own description (or a note that
|
|
60
|
+
* the body carried no usable answer). A typed class (not a plain Error) because callers tell failures apart:
|
|
61
|
+
* sendMessage's parse-mode fallback, editMessageText's "not modified" swallow, and the long poll's reading of a
|
|
62
|
+
* 401 or a 409 — each does `instanceof` + reads `.status` / `.description`. */
|
|
63
|
+
export declare class TelegramApiError extends Error {
|
|
64
|
+
readonly method: string;
|
|
65
|
+
readonly status: number;
|
|
66
|
+
readonly description: string;
|
|
67
|
+
constructor(method: string, status: number, description: string, options?: {
|
|
68
|
+
cause?: unknown;
|
|
69
|
+
});
|
|
44
70
|
}
|
|
45
71
|
/**
|
|
46
72
|
* The pipeline: one Bot API call, carrying every transport invariant (see the module header). Returns
|
|
@@ -48,7 +74,7 @@ interface Api {
|
|
|
48
74
|
*/
|
|
49
75
|
export declare function callApi<M extends keyof Api>(api: string, botToken: string, method: M,
|
|
50
76
|
/** JSON params, or a multipart form for an upload (a file's bytes; its own, longer timeout). */
|
|
51
|
-
params: Record<string, unknown> | FormData, opts?:
|
|
77
|
+
params: Record<string, unknown> | FormData, opts?: TelegramCallOptions): Promise<Api[M]>;
|
|
52
78
|
/**
|
|
53
79
|
* Split text into ≤4096-char chunks (Telegram's limit), preferring a newline boundary. When `html`, it is
|
|
54
80
|
* tag-aware: a tag that would SPAN a boundary is CLOSED at the chunk's end and REOPENED (attributes and
|
|
@@ -37,11 +37,10 @@ const RETRIES = 3;
|
|
|
37
37
|
const MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
38
38
|
/** A named Bot API failure. `status` 0 = the transport itself failed (network error / timeout) before
|
|
39
39
|
* any HTTP status existed; otherwise the HTTP status with Telegram's own description (or a note that
|
|
40
|
-
* the body carried no usable answer).
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
class TelegramApiError extends Error {
|
|
40
|
+
* the body carried no usable answer). A typed class (not a plain Error) because callers tell failures apart:
|
|
41
|
+
* sendMessage's parse-mode fallback, editMessageText's "not modified" swallow, and the long poll's reading of a
|
|
42
|
+
* 401 or a 409 — each does `instanceof` + reads `.status` / `.description`. */
|
|
43
|
+
export class TelegramApiError extends Error {
|
|
45
44
|
method;
|
|
46
45
|
status;
|
|
47
46
|
description;
|
|
@@ -57,6 +56,8 @@ class TelegramApiError extends Error {
|
|
|
57
56
|
this.description = description;
|
|
58
57
|
}
|
|
59
58
|
}
|
|
59
|
+
/** The per-attempt timeout, also ended by the caller's own signal when it gave one. */
|
|
60
|
+
const withCancel = (timeout, cancel) => cancel === undefined ? timeout : AbortSignal.any([timeout, cancel]);
|
|
60
61
|
/** Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers. */
|
|
61
62
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
63
|
/**
|
|
@@ -77,7 +78,7 @@ params, opts = {}) {
|
|
|
77
78
|
...(upload
|
|
78
79
|
? { body: params }
|
|
79
80
|
: { headers: { "content-type": "application/json" }, body: JSON.stringify(params) }),
|
|
80
|
-
signal: AbortSignal.timeout(upload ? DOWNLOAD_TIMEOUT_MS : API_TIMEOUT_MS),
|
|
81
|
+
signal: withCancel(AbortSignal.timeout(opts.timeoutMs ?? (upload ? DOWNLOAD_TIMEOUT_MS : API_TIMEOUT_MS)), opts.signal),
|
|
81
82
|
});
|
|
82
83
|
raw = await res.text(); // the body read shares the timeout — a mid-body stall is a transport failure too
|
|
83
84
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelModule } from "../../channel.ts";
|
|
1
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
|
|
2
2
|
import { type TelegramMessage, type TelegramRoute, type TelegramUpdate, defaultTelegramRoute, telegramEnvelope, telegramStop } from "./parse.ts";
|
|
3
3
|
import { type TelegramFailure } from "./preview.ts";
|
|
4
4
|
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
@@ -17,5 +17,13 @@ export interface TelegramChannelOptions {
|
|
|
17
17
|
/** Bot API base, for tests. */
|
|
18
18
|
apiBaseUrl?: string;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
20
|
+
/** What a channel that polls is given: the webhook one's options without the secret that verifies a webhook. */
|
|
21
|
+
export type TelegramPollingChannelOptions = Omit<TelegramChannelOptions, "secretToken">;
|
|
22
|
+
/** Build a Telegram bot channel that receives by webhook: policy options in, a {@link ChannelModule} out. */
|
|
23
|
+
export declare function telegramChannel(options: TelegramChannelOptions): ChannelModule;
|
|
24
|
+
/**
|
|
25
|
+
* Build a Telegram bot channel that receives by long polling: the process asks Telegram for updates, so no public URL
|
|
26
|
+
* and no webhook secret are involved. It takes the webhook away from the bot first (`getUpdates` does not work while
|
|
27
|
+
* one is set), so a bot is polled by one process or reached by one webhook, never both.
|
|
28
|
+
*/
|
|
29
|
+
export declare function telegramPollingChannel(options: TelegramPollingChannelOptions): LongConnectionChannelModule;
|
|
@@ -19,109 +19,181 @@ import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
|
19
19
|
import { createTurnRunner } from "../kit/turn-runner.js";
|
|
20
20
|
import { discussionBlock } from "../kit/context-buffer.js";
|
|
21
21
|
import { portJoin } from "../../effect-port.js";
|
|
22
|
+
import { pollTelegramUpdates } from "./polling.js";
|
|
22
23
|
import { createTurnStore } from "./turn-store.js";
|
|
23
24
|
// Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
|
|
24
25
|
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
26
|
+
const TELEGRAM_API = "https://api.telegram.org";
|
|
25
27
|
/** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
26
28
|
const MAX_UPDATE_BYTES = 1 << 20;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
/** The channel's state and policy, over the context it is mounted in: everything but how updates arrive. */
|
|
30
|
+
function createTelegramRuntime({ botToken, route, onError, botUsername, apiBaseUrl }, { agent, sessionFor, recordedSession, stateDir: stateHome }) {
|
|
31
|
+
// Validate at activation so deploy may inspect the module shape before secrets are provisioned.
|
|
32
|
+
if (!botToken) {
|
|
33
|
+
throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
|
|
34
|
+
}
|
|
35
|
+
const formatError = onError ?? defaultErrorMessage;
|
|
36
|
+
// One getMe at startup: the bot's @username (for the default route's group @mention summon and for recognising an
|
|
37
|
+
// addressed `/stop`, only when not supplied) and the group-privacy flag — privacy mode off is required to receive
|
|
38
|
+
// the un-summoned group messages that feed the context buffer, so warn if it is on.
|
|
39
|
+
let mentionName = botUsername;
|
|
40
|
+
void callApi(apiBaseUrl, botToken, "getMe", {}).then((me) => {
|
|
41
|
+
if (mentionName === undefined)
|
|
42
|
+
mentionName = me.username;
|
|
43
|
+
if (me.can_read_all_group_messages === false) {
|
|
44
|
+
log.warn("[telegram] privacy mode is on: the bot only sees @mentions / replies / commands, so group " +
|
|
45
|
+
"context (un-summoned messages) won't be captured. Disable it via @BotFather → /setprivacy.");
|
|
36
46
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
},
|
|
48
|
+
// Name every capability the missing username costs, so "the bot ignores /stop@name" is diagnosable from this
|
|
49
|
+
// ONE line.
|
|
50
|
+
(e) => log.warn(`[telegram] getMe failed; @mention summon, addressed /stop, and the privacy check are skipped: ${String(e)}`));
|
|
51
|
+
// A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot targeting is
|
|
52
|
+
// precise from the first update (no getMe race; getMe only resolves the @username).
|
|
53
|
+
const tokenId = Number(botToken.split(":")[0]);
|
|
54
|
+
const botId = Number.isSafeInteger(tokenId) && tokenId > 0 ? tokenId : undefined;
|
|
55
|
+
if (botId === undefined) {
|
|
56
|
+
log.warn("[telegram] bot token has no parseable bot id — reply-to-bot summon disabled until getMe resolves");
|
|
57
|
+
}
|
|
58
|
+
const decide = route ?? ((update) => defaultTelegramRoute(update, { botUsername: mentionName, botId }));
|
|
59
|
+
const buffer = createContextBuffer(join(stateHome, "buffers.json"));
|
|
60
|
+
// The send tool rides this channel's token, and what it sends lands in this buffer (shared-api.ts says why).
|
|
61
|
+
registerTelegramTransport({
|
|
62
|
+
apiBaseUrl,
|
|
63
|
+
botToken,
|
|
64
|
+
record: (placeKey, entry) => buffer.push(placeKey, entry),
|
|
65
|
+
});
|
|
66
|
+
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash leaves it for
|
|
67
|
+
// replay on the next start.
|
|
68
|
+
const store = createTurnStore(join(stateHome, "turns.json"));
|
|
69
|
+
const sideTasks = createTaskTracker("[telegram]");
|
|
70
|
+
const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
|
|
71
|
+
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not just an
|
|
72
|
+
// operator log line.
|
|
73
|
+
const notifyDropped = (r) => {
|
|
74
|
+
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
75
|
+
const sent = r.previewId !== undefined
|
|
76
|
+
? editMessageText(apiBaseUrl, botToken, targetOf(r), r.previewId, body, { html: false })
|
|
77
|
+
: sendMessage(apiBaseUrl, botToken, targetOf(r), body, { html: false }).then(() => { });
|
|
78
|
+
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (conversation=${r.conversation}): ${String(e)}`));
|
|
79
|
+
};
|
|
80
|
+
const runner = createTurnRunner({
|
|
81
|
+
label: "[telegram]",
|
|
82
|
+
store,
|
|
83
|
+
discussion: buffer,
|
|
84
|
+
toStored: ({ previewId: _live, ...intent }) => ({ ...intent, attempts: 0 }),
|
|
85
|
+
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
|
|
86
|
+
discussionKey: (rec) => rec.placeKey,
|
|
87
|
+
where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
|
|
88
|
+
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the bot ignored
|
|
89
|
+
// me" once the current turn runs long.
|
|
90
|
+
onQueuedBehind: (rec) => ({
|
|
91
|
+
done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
|
|
92
|
+
html: false,
|
|
93
|
+
}).then((id) => {
|
|
94
|
+
if (id !== undefined)
|
|
95
|
+
rec.previewId = id;
|
|
96
|
+
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
|
|
97
|
+
}),
|
|
98
|
+
// Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort.
|
|
99
|
+
onDeferred: (rec) => {
|
|
100
|
+
if (rec.previewId !== undefined) {
|
|
101
|
+
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
102
|
+
chat_id: rec.chatId,
|
|
103
|
+
message_id: rec.previewId,
|
|
104
|
+
}).catch(() => { });
|
|
48
105
|
}
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// The send tool rides this channel's token, and what it sends lands in this buffer (shared-api.ts says why).
|
|
63
|
-
registerTelegramTransport({
|
|
64
|
-
apiBaseUrl,
|
|
65
|
-
botToken,
|
|
66
|
-
record: (placeKey, entry) => buffer.push(placeKey, entry),
|
|
67
|
-
});
|
|
68
|
-
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash leaves it for
|
|
69
|
-
// replay on the next start.
|
|
70
|
-
const store = createTurnStore(join(stateHome, "turns.json"));
|
|
71
|
-
const sideTasks = createTaskTracker("[telegram]");
|
|
72
|
-
const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
|
|
73
|
-
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not just an
|
|
74
|
-
// operator log line.
|
|
75
|
-
const notifyDropped = (r) => {
|
|
76
|
-
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
77
|
-
const sent = r.previewId !== undefined
|
|
78
|
-
? editMessageText(apiBaseUrl, botToken, targetOf(r), r.previewId, body, { html: false })
|
|
79
|
-
: sendMessage(apiBaseUrl, botToken, targetOf(r), body, { html: false }).then(() => { });
|
|
80
|
-
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (conversation=${r.conversation}): ${String(e)}`));
|
|
81
|
-
};
|
|
82
|
-
const runner = createTurnRunner({
|
|
83
|
-
label: "[telegram]",
|
|
84
|
-
store,
|
|
85
|
-
discussion: buffer,
|
|
86
|
-
toStored: ({ previewId: _live, ...intent }) => ({ ...intent, attempts: 0 }),
|
|
87
|
-
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
|
|
88
|
-
discussionKey: (rec) => rec.placeKey,
|
|
89
|
-
where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
|
|
90
|
-
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the bot ignored
|
|
91
|
-
// me" once the current turn runs long.
|
|
92
|
-
onQueuedBehind: (rec) => ({
|
|
93
|
-
done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
|
|
94
|
-
html: false,
|
|
95
|
-
}).then((id) => {
|
|
96
|
-
if (id !== undefined)
|
|
97
|
-
rec.previewId = id;
|
|
98
|
-
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
|
|
106
|
+
},
|
|
107
|
+
notifyDropped,
|
|
108
|
+
// `rec.previewId`: the ⏳ notice THIS process put up if the record waited behind another turn.
|
|
109
|
+
deliverAnswer: (rec, answer) => portJoin(() => deliverTelegramAnswer(apiBaseUrl, botToken, targetOf(rec), answer, rec.previewId)),
|
|
110
|
+
execute: (rec, discussion, onAnswered) => telegramReply(telegramTurnStream(agent, {
|
|
111
|
+
session: () => sessionFor(rec.conversation),
|
|
112
|
+
key: rec.id,
|
|
113
|
+
text: `${discussionBlock(discussion.text)}${rec.baseText}`,
|
|
114
|
+
}, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: attachmentsDir(stateHome) }, {
|
|
115
|
+
primary: { imageFileIds: rec.imageFileIds, fileIds: rec.fileIds },
|
|
116
|
+
buffered: collectAttachments(discussion.consumed, {
|
|
117
|
+
files: new Set(rec.fileIds),
|
|
118
|
+
images: new Set(rec.imageFileIds),
|
|
99
119
|
}),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
}), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId, onAnswered),
|
|
121
|
+
});
|
|
122
|
+
runner.recover();
|
|
123
|
+
const accept = (update) => {
|
|
124
|
+
// Decide whether/where to answer, then run the turn.
|
|
125
|
+
const m = pickMessage(update);
|
|
126
|
+
if (!m)
|
|
127
|
+
return;
|
|
128
|
+
const placeKey = telegramPlaceKey(m.chat.id, m.message_thread_id);
|
|
129
|
+
const r = decide(update);
|
|
130
|
+
if (!r) {
|
|
131
|
+
// Not summoned: in a group, record the message so a later summon has the discussion (needs privacy off to be
|
|
132
|
+
// delivered here at all).
|
|
133
|
+
const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
|
|
134
|
+
const content = messageText(m);
|
|
135
|
+
if (isGroup && content) {
|
|
136
|
+
// OWN attachments only: each message is its own buffer entry, so a reply's referenced attachment is already
|
|
137
|
+
// (or will be) the other entry's.
|
|
138
|
+
const fileIds = ownFiles(m);
|
|
139
|
+
const imageIds = ownImages(m);
|
|
140
|
+
// A captioned attachment renders as its caption.
|
|
141
|
+
const summary = attachmentSummary(m);
|
|
142
|
+
const bodyLine = summary && content !== summary ? `${content} ${summary}` : content;
|
|
143
|
+
buffer.push(placeKey, {
|
|
144
|
+
sender: fromLabel(m.from) ?? "someone",
|
|
145
|
+
body: bodyLine,
|
|
146
|
+
messageId: m.message_id,
|
|
147
|
+
replyTo: m.reply_to_message?.message_id,
|
|
148
|
+
fileIds: fileIds.length ? fileIds : undefined,
|
|
149
|
+
imageIds: imageIds.length ? imageIds : undefined,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const conversation = r.conversation ?? placeKey;
|
|
155
|
+
const chatId = r.chatId ?? m.chat.id;
|
|
156
|
+
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no reply-quote.
|
|
157
|
+
const threadId = r.threadId ?? m.message_thread_id;
|
|
158
|
+
const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
|
|
159
|
+
const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
|
|
160
|
+
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the run it stops.
|
|
161
|
+
// `abort` waits until what it stopped has ended, so the update is acknowledged first.
|
|
162
|
+
if (telegramStop(update, { botUsername: mentionName, botId })) {
|
|
163
|
+
sideTasks.track(dispatchStop(agent, recordedSession(conversation), "[telegram]")
|
|
164
|
+
.then((feedback) => sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }))
|
|
165
|
+
.catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`)));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const baseText = r.text ?? telegramEnvelope(m);
|
|
169
|
+
const imageFileIds = extractImages(m);
|
|
170
|
+
const fileIds = extractFiles(m);
|
|
171
|
+
if (baseText.trim() !== "" || imageFileIds.length > 0 || fileIds.length > 0) {
|
|
172
|
+
// Everything the turn needs, as a plain record; persisted pre-ACK then run serially per session.
|
|
173
|
+
runner.submit({
|
|
174
|
+
id: `${update.update_id}`,
|
|
175
|
+
conversation,
|
|
176
|
+
placeKey,
|
|
177
|
+
baseText,
|
|
178
|
+
chatId,
|
|
179
|
+
threadId,
|
|
180
|
+
replyTo,
|
|
181
|
+
imageFileIds,
|
|
182
|
+
fileIds,
|
|
183
|
+
}, true);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
};
|
|
187
|
+
return { accept, turnsIdle: () => runner.idle() };
|
|
188
|
+
}
|
|
189
|
+
/** Build a Telegram bot channel that receives by webhook: policy options in, a {@link ChannelModule} out. */
|
|
190
|
+
export function telegramChannel(options) {
|
|
191
|
+
const { secretToken, apiBaseUrl = TELEGRAM_API } = options;
|
|
192
|
+
return (ctx) => {
|
|
193
|
+
if (!secretToken) {
|
|
194
|
+
throw new Error("telegramChannel requires a non-empty secretToken (the webhook secret_token; an unset one accepts forged updates)");
|
|
195
|
+
}
|
|
196
|
+
const runtime = createTelegramRuntime({ ...options, apiBaseUrl }, ctx);
|
|
125
197
|
const handler = async (req) => {
|
|
126
198
|
if (req.method !== "POST")
|
|
127
199
|
return text("POST only\n", 405);
|
|
@@ -139,71 +211,26 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
139
211
|
catch {
|
|
140
212
|
return text("invalid json\n", 400);
|
|
141
213
|
}
|
|
142
|
-
|
|
143
|
-
const m = pickMessage(update);
|
|
144
|
-
if (!m)
|
|
145
|
-
return new Response(null, { status: 200 });
|
|
146
|
-
const placeKey = telegramPlaceKey(m.chat.id, m.message_thread_id);
|
|
147
|
-
const r = decide(update);
|
|
148
|
-
if (!r) {
|
|
149
|
-
// Not summoned: in a group, record the message so a later summon has the discussion (needs privacy off to be
|
|
150
|
-
// delivered here at all).
|
|
151
|
-
const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
|
|
152
|
-
const content = messageText(m);
|
|
153
|
-
if (isGroup && content) {
|
|
154
|
-
// OWN attachments only: each message is its own buffer entry, so a reply's referenced attachment is already
|
|
155
|
-
// (or will be) the other entry's.
|
|
156
|
-
const fileIds = ownFiles(m);
|
|
157
|
-
const imageIds = ownImages(m);
|
|
158
|
-
// A captioned attachment renders as its caption.
|
|
159
|
-
const summary = attachmentSummary(m);
|
|
160
|
-
const bodyLine = summary && content !== summary ? `${content} ${summary}` : content;
|
|
161
|
-
buffer.push(placeKey, {
|
|
162
|
-
sender: fromLabel(m.from) ?? "someone",
|
|
163
|
-
body: bodyLine,
|
|
164
|
-
messageId: m.message_id,
|
|
165
|
-
replyTo: m.reply_to_message?.message_id,
|
|
166
|
-
fileIds: fileIds.length ? fileIds : undefined,
|
|
167
|
-
imageIds: imageIds.length ? imageIds : undefined,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return new Response(null, { status: 200 });
|
|
171
|
-
}
|
|
172
|
-
const conversation = r.conversation ?? placeKey;
|
|
173
|
-
const chatId = r.chatId ?? m.chat.id;
|
|
174
|
-
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no reply-quote.
|
|
175
|
-
const threadId = r.threadId ?? m.message_thread_id;
|
|
176
|
-
const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
|
|
177
|
-
const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
|
|
178
|
-
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the run it stops.
|
|
179
|
-
// `abort` waits until what it stopped has ended, so the update is acknowledged first.
|
|
180
|
-
if (telegramStop(update, { botUsername: mentionName, botId })) {
|
|
181
|
-
sideTasks.track(dispatchStop(agent, recordedSession(conversation), "[telegram]")
|
|
182
|
-
.then((feedback) => sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }))
|
|
183
|
-
.catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`)));
|
|
184
|
-
return new Response(null, { status: 200 });
|
|
185
|
-
}
|
|
186
|
-
const baseText = r.text ?? telegramEnvelope(m);
|
|
187
|
-
const imageFileIds = extractImages(m);
|
|
188
|
-
const fileIds = extractFiles(m);
|
|
189
|
-
if (baseText.trim() !== "" || imageFileIds.length > 0 || fileIds.length > 0) {
|
|
190
|
-
// Everything the turn needs, as a plain record; persisted pre-ACK then run serially per session.
|
|
191
|
-
runner.submit({
|
|
192
|
-
id: `${update.update_id}`,
|
|
193
|
-
conversation,
|
|
194
|
-
placeKey,
|
|
195
|
-
baseText,
|
|
196
|
-
chatId,
|
|
197
|
-
threadId,
|
|
198
|
-
replyTo,
|
|
199
|
-
imageFileIds,
|
|
200
|
-
fileIds,
|
|
201
|
-
}, true);
|
|
202
|
-
}
|
|
214
|
+
runtime.accept(update);
|
|
203
215
|
return new Response(null, { status: 200 });
|
|
204
216
|
};
|
|
205
217
|
// Test/observability seam: await the fire-and-forget turns this handler enqueues.
|
|
206
|
-
handler.turnsIdle =
|
|
218
|
+
handler.turnsIdle = runtime.turnsIdle;
|
|
207
219
|
return { "POST /telegram": handler };
|
|
208
220
|
};
|
|
209
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Build a Telegram bot channel that receives by long polling: the process asks Telegram for updates, so no public URL
|
|
224
|
+
* and no webhook secret are involved. It takes the webhook away from the bot first (`getUpdates` does not work while
|
|
225
|
+
* one is set), so a bot is polled by one process or reached by one webhook, never both.
|
|
226
|
+
*/
|
|
227
|
+
export function telegramPollingChannel(options) {
|
|
228
|
+
const apiBaseUrl = options.apiBaseUrl ?? TELEGRAM_API;
|
|
229
|
+
return {
|
|
230
|
+
name: "telegram long polling",
|
|
231
|
+
connect(ctx, signal) {
|
|
232
|
+
const runtime = createTelegramRuntime({ ...options, apiBaseUrl }, ctx);
|
|
233
|
+
return pollTelegramUpdates({ apiBaseUrl, botToken: options.botToken, accept: runtime.accept }, signal);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `address` is `<publicUrl><path>`: the scheme, the host and the channel's own path. A mount prefix is
|
|
3
|
+
* `publicUrl`'s, since an embedding app's router may strip it before the channel sees the request.
|
|
4
|
+
*/
|
|
5
|
+
export declare function addressedTo(address: string, publicUrl: string, path: string): boolean;
|
|
6
|
+
/** Record that an authenticated request from the platform reached the channel whose state is `stateDir`. */
|
|
7
|
+
export declare function recordReached(stateDir: string, request: Request): void;
|
|
8
|
+
/** The address an authenticated request last reached the channel at, as recorded, if any. */
|
|
9
|
+
export declare function readReached(stateDir: string): string | undefined;
|
|
10
|
+
/** Hear each address recorded for `stateDir` from now on; the answer stops it. */
|
|
11
|
+
export declare function onReached(stateDir: string, listener: (address: string) => void): () => void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What proves a platform reaches this instance at a URL only a person sets (Slack's Request URL; Lark's where its
|
|
3
|
+
* configuration API is missing): an authenticated request from it, and the address it was sent to (SPEC.md §8,
|
|
4
|
+
* channel-setup.md §5). A platform's signature or token proves who sent a request, not where, so the address is read
|
|
5
|
+
* from the request's host and forwarding headers. The first-party channel records it, durable in its own state
|
|
6
|
+
* directory, and a serve in this process hears it at once.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { writeFileAtomic } from "../atomic-write.js";
|
|
11
|
+
import { log } from "../log.js";
|
|
12
|
+
const EVIDENCE_FILE = "reached.json";
|
|
13
|
+
/** Where the request was sent: its scheme, host and path, as the proxy in front of the server says it received them. */
|
|
14
|
+
function requestAddress(request) {
|
|
15
|
+
const url = new URL(request.url);
|
|
16
|
+
const first = (value) => value?.split(",")[0]?.trim() || undefined;
|
|
17
|
+
const scheme = first(request.headers.get("x-forwarded-proto")) ?? url.protocol.replace(/:$/, "");
|
|
18
|
+
const host = first(request.headers.get("x-forwarded-host")) ?? request.headers.get("host") ?? url.host;
|
|
19
|
+
return `${scheme}://${host}${url.pathname}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether `address` is `<publicUrl><path>`: the scheme, the host and the channel's own path. A mount prefix is
|
|
23
|
+
* `publicUrl`'s, since an embedding app's router may strip it before the channel sees the request.
|
|
24
|
+
*/
|
|
25
|
+
export function addressedTo(address, publicUrl, path) {
|
|
26
|
+
const reached = new URL(address);
|
|
27
|
+
const expected = new URL(publicUrl);
|
|
28
|
+
return reached.protocol === expected.protocol && reached.host === expected.host && reached.pathname.endsWith(path);
|
|
29
|
+
}
|
|
30
|
+
const listeners = new Map();
|
|
31
|
+
const recorded = new Map();
|
|
32
|
+
/** Record that an authenticated request from the platform reached the channel whose state is `stateDir`. */
|
|
33
|
+
export function recordReached(stateDir, request) {
|
|
34
|
+
const address = requestAddress(request);
|
|
35
|
+
if (recorded.get(stateDir) === address)
|
|
36
|
+
return;
|
|
37
|
+
recorded.set(stateDir, address);
|
|
38
|
+
try {
|
|
39
|
+
writeFileAtomic(join(stateDir, EVIDENCE_FILE), `${JSON.stringify({ address, at: new Date().toISOString() })}\n`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// The request is answered either way; a record not kept costs a `manual` after the next restart, said here.
|
|
43
|
+
log.warn(`[fastagent] could not record that ${address} was reached: ${String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
for (const listener of listeners.get(stateDir) ?? [])
|
|
46
|
+
listener(address);
|
|
47
|
+
}
|
|
48
|
+
/** The address an authenticated request last reached the channel at, as recorded, if any. */
|
|
49
|
+
export function readReached(stateDir) {
|
|
50
|
+
try {
|
|
51
|
+
const { address } = JSON.parse(readFileSync(join(stateDir, EVIDENCE_FILE), "utf8"));
|
|
52
|
+
return typeof address === "string" ? address : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return undefined;
|
|
57
|
+
log.warn(`[fastagent] could not read ${join(stateDir, EVIDENCE_FILE)}: ${String(error)}`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Hear each address recorded for `stateDir` from now on; the answer stops it. */
|
|
62
|
+
export function onReached(stateDir, listener) {
|
|
63
|
+
const set = listeners.get(stateDir) ?? new Set();
|
|
64
|
+
set.add(listener);
|
|
65
|
+
listeners.set(stateDir, set);
|
|
66
|
+
return () => {
|
|
67
|
+
set.delete(listener);
|
|
68
|
+
if (set.size === 0)
|
|
69
|
+
listeners.delete(stateDir);
|
|
70
|
+
};
|
|
71
|
+
}
|