@fastagent-sh/fastagent 0.19.0 → 0.21.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 +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Shared SSE response lifecycle for invoke and session observation. Fetch-only, with no Node dependencies. */
|
|
2
|
+
/** The remote client's idle watchdog allows three missed heartbeats. */
|
|
3
|
+
export const SSE_HEARTBEAT_MS = 30_000;
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const heartbeat = encoder.encode(": ping\n\n");
|
|
6
|
+
export function sseResponse(events, project = (event) => event) {
|
|
7
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
8
|
+
// Subscribe before headers; synchronous startup errors belong to the HTTP error boundary.
|
|
9
|
+
let pending = Promise.resolve(iterator.next());
|
|
10
|
+
// pull() surfaces asynchronous failures. Observe rejection even if the caller cancels before that pull.
|
|
11
|
+
pending.catch(() => { });
|
|
12
|
+
let closed = false;
|
|
13
|
+
let timer;
|
|
14
|
+
const stop = () => {
|
|
15
|
+
closed = true;
|
|
16
|
+
clearInterval(timer);
|
|
17
|
+
};
|
|
18
|
+
const cancel = async () => {
|
|
19
|
+
if (closed)
|
|
20
|
+
return;
|
|
21
|
+
stop();
|
|
22
|
+
await iterator.return?.();
|
|
23
|
+
};
|
|
24
|
+
const stream = new ReadableStream({
|
|
25
|
+
start(controller) {
|
|
26
|
+
timer = setInterval(() => controller.enqueue(heartbeat), SSE_HEARTBEAT_MS);
|
|
27
|
+
},
|
|
28
|
+
async pull(controller) {
|
|
29
|
+
try {
|
|
30
|
+
const next = await (pending ?? iterator.next());
|
|
31
|
+
pending = undefined;
|
|
32
|
+
// Cancellation can settle a pending next(); its value no longer has a reader.
|
|
33
|
+
if (closed)
|
|
34
|
+
return;
|
|
35
|
+
if (next.done) {
|
|
36
|
+
stop();
|
|
37
|
+
controller.close();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(project(next.value))}\n\n`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (closed)
|
|
45
|
+
return;
|
|
46
|
+
// Errored streams never call cancel(), so iterator and timer cleanup belongs here too.
|
|
47
|
+
try {
|
|
48
|
+
await cancel();
|
|
49
|
+
}
|
|
50
|
+
catch (cleanupError) {
|
|
51
|
+
controller.error(new AggregateError([error, cleanupError], "SSE source and cleanup failed", { cause: error }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
controller.error(error);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
cancel,
|
|
58
|
+
});
|
|
59
|
+
return new Response(stream, {
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "text/event-stream",
|
|
62
|
+
"cache-control": "no-cache",
|
|
63
|
+
connection: "keep-alive",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -139,12 +139,32 @@ export declare function messageText(m: TelegramMessage): string;
|
|
|
139
139
|
* `text: `${telegramEnvelope(m)}\n\n[extra]``. The channel still appends downloaded attachments.
|
|
140
140
|
*/
|
|
141
141
|
export declare function telegramEnvelope(m: TelegramMessage): string;
|
|
142
|
+
/**
|
|
143
|
+
* Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a
|
|
144
|
+
* stop, because an unaddressed one is not a command it may act on. Four ways to address it, all
|
|
145
|
+
* equivalent: a bare `/stop` in a private chat (no one else is there), `/stop@thisbot`, an `@thisbot`
|
|
146
|
+
* mention beside the command, and a bare `/stop` replying to one of this bot's messages.
|
|
147
|
+
*
|
|
148
|
+
* A BARE `/stop` in a group is deliberately NOT one of them. Telegram hands it to every bot in the
|
|
149
|
+
* chat — and only promises to deliver it at all when this bot spoke last — so it names no one: acting
|
|
150
|
+
* on it lets a bystander's ask for a different bot abort this bot's run, and answering it lets that
|
|
151
|
+
* bystander make this bot talk. It stays ordinary discussion, which is what the group sees anyway.
|
|
152
|
+
*
|
|
153
|
+
* Takes the UPDATE, like `route` itself, so a custom route can ask it directly — that route is the
|
|
154
|
+
* gate a stop must pass, and one that refuses this update silences the command.
|
|
155
|
+
*/
|
|
156
|
+
export declare function telegramStop(update: TelegramUpdate, options?: {
|
|
157
|
+
botUsername?: string;
|
|
158
|
+
botId?: number;
|
|
159
|
+
}): boolean;
|
|
142
160
|
/**
|
|
143
161
|
* The default routing policy (used when `route` is omitted; exported so a custom route can reuse it):
|
|
144
162
|
* answer private chats always; a group only on a reply to THIS bot (by `botId`) or a `mention` entity
|
|
145
163
|
* naming it (when `botUsername` is supplied — telegramChannel parses the id from the token and resolves
|
|
146
164
|
* the username via getMe). A bare or directed slash command does NOT summon in a group (that was noisy;
|
|
147
|
-
* a bot author who wants commands adds a custom route)
|
|
165
|
+
* a bot author who wants commands adds a custom route) — except a `/stop` ADDRESSED to this bot, which
|
|
166
|
+
* must reach the run it stops rather than sit in the buffer behind it (see {@link telegramStop}; a
|
|
167
|
+
* bare group `/stop` addresses no one and stays ordinary discussion). Returns `{}` (act; the channel fills
|
|
148
168
|
* session/target/prompt from the message) or `null` (ignore).
|
|
149
169
|
*/
|
|
150
170
|
export declare function defaultTelegramRoute(update: TelegramUpdate, options?: {
|
|
@@ -108,20 +108,66 @@ function botName(botUsername) {
|
|
|
108
108
|
return s || undefined;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
111
|
+
* The `mention` ENTITIES naming THIS bot — read from what Telegram's server already parsed, not a
|
|
112
|
+
* regex over the raw text. The entity type excludes by construction what a text scan false-matches:
|
|
113
|
+
* `@bot` inside a code block or a URL is not a `mention` entity, a glued `/cmd@bot` is a `bot_command`
|
|
114
|
+
* — and slicing the exact offset/length range makes `@fast` vs `@fastagent` confusion impossible.
|
|
115
|
+
* (Offsets are UTF-16 code units = native JS string indexing.) No text fallback: mention entities are
|
|
116
|
+
* produced server-side, so their absence means there is no mention. Empty when the bot does not know
|
|
117
|
+
* its own username — "which of these names me?" has no answer then, and guessing would mis-summon.
|
|
117
118
|
*/
|
|
118
|
-
function
|
|
119
|
+
function botMentions(m, botUsername) {
|
|
119
120
|
const name = botName(botUsername);
|
|
120
121
|
if (!name)
|
|
121
|
-
return
|
|
122
|
+
return [];
|
|
122
123
|
const text = m.text ?? m.caption ?? "";
|
|
123
124
|
const entities = (m.text !== undefined ? m.entities : m.caption_entities) ?? [];
|
|
124
|
-
return entities.
|
|
125
|
+
return entities.filter((e) => e.type === "mention" && text.slice(e.offset, e.offset + e.length).toLowerCase() === `@${name}`);
|
|
126
|
+
}
|
|
127
|
+
/** Whether the message @mentions the bot. */
|
|
128
|
+
function mentionsBot(m, botUsername) {
|
|
129
|
+
return botMentions(m, botUsername).length > 0;
|
|
130
|
+
}
|
|
131
|
+
/** The message text with THIS bot's mentions cut out, so a command addressed to it reads as the bare
|
|
132
|
+
* command. Only its own: a message that names someone else (`@otherbot /stop`) must keep that name,
|
|
133
|
+
* or cutting it would turn an ask aimed elsewhere into this bot's command. Cut from the end so the
|
|
134
|
+
* earlier offsets stay valid (they are UTF-16 code units = native JS indexing). */
|
|
135
|
+
function textWithoutBotMentions(m, botUsername) {
|
|
136
|
+
const text = m.text ?? m.caption ?? "";
|
|
137
|
+
return botMentions(m, botUsername)
|
|
138
|
+
.sort((a, b) => b.offset - a.offset)
|
|
139
|
+
.reduce((out, e) => out.slice(0, e.offset) + out.slice(e.offset + e.length), text)
|
|
140
|
+
.trim();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether the update is a `/stop` ADDRESSED TO THIS BOT — the one question the channel asks about a
|
|
144
|
+
* stop, because an unaddressed one is not a command it may act on. Four ways to address it, all
|
|
145
|
+
* equivalent: a bare `/stop` in a private chat (no one else is there), `/stop@thisbot`, an `@thisbot`
|
|
146
|
+
* mention beside the command, and a bare `/stop` replying to one of this bot's messages.
|
|
147
|
+
*
|
|
148
|
+
* A BARE `/stop` in a group is deliberately NOT one of them. Telegram hands it to every bot in the
|
|
149
|
+
* chat — and only promises to deliver it at all when this bot spoke last — so it names no one: acting
|
|
150
|
+
* on it lets a bystander's ask for a different bot abort this bot's run, and answering it lets that
|
|
151
|
+
* bystander make this bot talk. It stays ordinary discussion, which is what the group sees anyway.
|
|
152
|
+
*
|
|
153
|
+
* Takes the UPDATE, like `route` itself, so a custom route can ask it directly — that route is the
|
|
154
|
+
* gate a stop must pass, and one that refuses this update silences the command.
|
|
155
|
+
*/
|
|
156
|
+
export function telegramStop(update, options) {
|
|
157
|
+
const m = pickMessage(update);
|
|
158
|
+
if (!m)
|
|
159
|
+
return false;
|
|
160
|
+
// This bot's own mentions are cut out before matching: `@thisbot /stop` is the same command as
|
|
161
|
+
// `/stop@thisbot`. Anyone else's stays in the text, so `@otherbot /stop` never reduces to a command
|
|
162
|
+
// here — not even when it also replies to this bot.
|
|
163
|
+
const match = /^\/stop(?:@([A-Za-z0-9_]+))?$/i.exec(textWithoutBotMentions(m, options?.botUsername));
|
|
164
|
+
if (!match)
|
|
165
|
+
return false;
|
|
166
|
+
// `/stop@name` states its addressee: ours only when the name is ours, and never when this bot does
|
|
167
|
+
// not know its own username (fail closed — the same rule reply/mention summon follows).
|
|
168
|
+
if (match[1] !== undefined)
|
|
169
|
+
return match[1].toLowerCase() === botName(options?.botUsername);
|
|
170
|
+
return m.chat.type === "private" || mentionsBot(m, options?.botUsername) || repliesToBot(m, options);
|
|
125
171
|
}
|
|
126
172
|
/** Whether the message replies to THIS bot — not just any bot: in a multi-bot group, replying to
|
|
127
173
|
* another bot must not summon ours. Identity is the bot's numeric id (stable; a username is a mutable
|
|
@@ -143,13 +189,21 @@ function repliesToBot(m, options) {
|
|
|
143
189
|
* answer private chats always; a group only on a reply to THIS bot (by `botId`) or a `mention` entity
|
|
144
190
|
* naming it (when `botUsername` is supplied — telegramChannel parses the id from the token and resolves
|
|
145
191
|
* the username via getMe). A bare or directed slash command does NOT summon in a group (that was noisy;
|
|
146
|
-
* a bot author who wants commands adds a custom route)
|
|
192
|
+
* a bot author who wants commands adds a custom route) — except a `/stop` ADDRESSED to this bot, which
|
|
193
|
+
* must reach the run it stops rather than sit in the buffer behind it (see {@link telegramStop}; a
|
|
194
|
+
* bare group `/stop` addresses no one and stays ordinary discussion). Returns `{}` (act; the channel fills
|
|
147
195
|
* session/target/prompt from the message) or `null` (ignore).
|
|
148
196
|
*/
|
|
149
197
|
export function defaultTelegramRoute(update, options) {
|
|
150
198
|
const m = pickMessage(update);
|
|
151
199
|
if (!m)
|
|
152
200
|
return null;
|
|
153
|
-
const summoned = m.chat.type === "private" ||
|
|
201
|
+
const summoned = m.chat.type === "private" ||
|
|
202
|
+
repliesToBot(m, options) ||
|
|
203
|
+
mentionsBot(m, options?.botUsername) ||
|
|
204
|
+
// Adds exactly ONE case to the three above: the `/stop@thisbot` suffix form. Every other way to
|
|
205
|
+
// address a stop (private, mention, reply) is already a summon on its own — telegramStop repeats
|
|
206
|
+
// those three by design, since it must answer "addressed to me?" without a route to lean on.
|
|
207
|
+
telegramStop(update, options);
|
|
154
208
|
return summoned ? {} : null;
|
|
155
209
|
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RegistrationOutcome } from "../registration.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Register `<baseUrl>/telegram` as the bot's webhook (with the .env secret).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* fixes the race that made the first real deploy need a manual `setWebhook`. Missing tokens print the
|
|
8
|
-
* manual instruction instead of failing. `opts` (timeouts) exist for tests; production uses the defaults.
|
|
3
|
+
* Register `<baseUrl>/telegram` as the bot's webhook (with the .env secret). Missing tokens print the
|
|
4
|
+
* manual instruction instead of failing. `opts` carries the attempt budget: `--tunnel` takes the
|
|
5
|
+
* default, `deploy --run` passes `DEPLOY_REGISTRATION_ATTEMPTS` (a host starts slower than a tunnel),
|
|
6
|
+
* and tests shrink it.
|
|
9
7
|
*
|
|
10
8
|
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
11
9
|
*/
|
|
12
10
|
export declare function registerTelegramWebhook(baseUrl: string, opts?: {
|
|
13
|
-
|
|
14
|
-
readyIntervalMs?: number;
|
|
11
|
+
attempts?: number;
|
|
15
12
|
retryMs?: number;
|
|
16
13
|
}): Promise<RegistrationOutcome>;
|
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
* Telegram webhook registration — the TELEGRAM-domain step both `--tunnel` (dev, tunnel.ts) and
|
|
3
3
|
* `deploy … --run` (the host runners' post-deploy step) invoke. What "registering telegram" means lives
|
|
4
4
|
* here, beside the channel it serves; it reads the same .env tokens the channel uses.
|
|
5
|
+
*
|
|
6
|
+
* READINESS IS setWebhook'S OWN VERDICT, not a local probe. Telegram VERIFIES the URL during the call,
|
|
7
|
+
* from Telegram's network — which is the only network that has to reach it. This used to poll
|
|
8
|
+
* `<baseUrl>/health` from here first, and that probe answered a different question badly: a freshly
|
|
9
|
+
* minted hostname (a quick tunnel, a fresh deploy) is routinely unreachable from the machine running
|
|
10
|
+
* the CLI for a minute or more — behind a proxy, or while its own resolver still says ENOTFOUND — long
|
|
11
|
+
* after Telegram can reach it (#421). So the retry loop below IS the wait: Telegram's reachability
|
|
12
|
+
* verdicts ("Failed to resolve host", "Wrong response from the webhook") are retried, and only a
|
|
13
|
+
* configuration error the operator must fix is reported once.
|
|
5
14
|
*/
|
|
6
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
15
|
import { log } from "../../log.js";
|
|
8
|
-
import {
|
|
16
|
+
import { retryWhile } from "../registration.js";
|
|
9
17
|
import { callApi } from "./telegram-api.js";
|
|
10
18
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Whether a setWebhook failure is the URL still warming up rather than a configuration error. Two
|
|
20
|
+
* families: transport failures reaching Telegram at all, and Telegram's own verdict after it fails to
|
|
21
|
+
* reach the webhook — which carries its connection layer's words verbatim (`Bad Request: bad webhook:
|
|
22
|
+
* Connection timed out` / `Connection refused` for a host whose DNS answers before anything listens,
|
|
23
|
+
* `Wrong response from the webhook: 530` for a tunnel edge without its origin). This is the ONLY gate
|
|
24
|
+
* between "spend the caller's retry budget" and "fail now", so it is wide on the reachability side.
|
|
25
|
+
* A permanent "bad webhook" — an http:// URL, an unsupported port — matches nothing here and is reported.
|
|
26
|
+
*/
|
|
27
|
+
function isTransientRegistrationError(error) {
|
|
28
|
+
return /resolve host|getaddrinfo|ENOTFOUND|fetch failed|ECONNRESET|timeout|timed out|connection refused|can't connect|connection to the host|wrong response from the webhook/i.test(error);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register `<baseUrl>/telegram` as the bot's webhook (with the .env secret). Missing tokens print the
|
|
32
|
+
* manual instruction instead of failing. `opts` carries the attempt budget: `--tunnel` takes the
|
|
33
|
+
* default, `deploy --run` passes `DEPLOY_REGISTRATION_ATTEMPTS` (a host starts slower than a tunnel),
|
|
34
|
+
* and tests shrink it.
|
|
17
35
|
*
|
|
18
36
|
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
19
37
|
*/
|
|
@@ -25,40 +43,24 @@ export async function registerTelegramWebhook(baseUrl, opts = {}) {
|
|
|
25
43
|
log.info(`[fastagent] telegram: set TELEGRAM_BOT_TOKEN + TELEGRAM_SECRET_TOKEN in .env, then re-run to auto-register. Webhook URL: ${webhookUrl}`);
|
|
26
44
|
return "manual";
|
|
27
45
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
log.
|
|
36
|
-
|
|
37
|
-
return "failed";
|
|
46
|
+
log.info(`[fastagent] telegram: registering the webhook — Telegram verifies ${webhookUrl} as it does…`);
|
|
47
|
+
try {
|
|
48
|
+
await retryWhile(() => callApi("https://api.telegram.org", botToken, "setWebhook", { url: webhookUrl, secret_token: secret }), (error) => isTransientRegistrationError(String(error)), {
|
|
49
|
+
attempts: opts.attempts,
|
|
50
|
+
retryMs: opts.retryMs,
|
|
51
|
+
onRetry: ({ attempt, attempts }) => log.info(`[fastagent] telegram: Telegram cannot reach ${webhookUrl} yet (attempt ${attempt}/${attempts}); retrying…`),
|
|
52
|
+
});
|
|
53
|
+
log.info(`[fastagent] telegram: webhook registered → ${webhookUrl}`);
|
|
54
|
+
return "registered";
|
|
38
55
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return "registered";
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
const error = String(e);
|
|
52
|
-
if (!/resolve host|getaddrinfo|ENOTFOUND|fetch failed|ECONNRESET|timeout/i.test(error)) {
|
|
53
|
-
log.error(`[fastagent] telegram: setWebhook failed (${error}). Register manually with url=${webhookUrl}`);
|
|
54
|
-
return "failed";
|
|
55
|
-
}
|
|
56
|
-
lastTransientError = error;
|
|
57
|
-
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// Both endings leave the webhook unregistered and the operator with work to do, so both are ERROR;
|
|
58
|
+
// they differ only in what to do about it.
|
|
59
|
+
const error = String(e);
|
|
60
|
+
log.error(isTransientRegistrationError(error)
|
|
61
|
+
? `[fastagent] telegram: Telegram could not reach ${webhookUrl} after retries (last error: ${error}). ` +
|
|
62
|
+
`Once it is up, register manually: curl "https://api.telegram.org/bot<token>/setWebhook" -d url=${webhookUrl} -d secret_token=<secret>`
|
|
63
|
+
: `[fastagent] telegram: setWebhook failed (${error}). Register manually with url=${webhookUrl}`);
|
|
64
|
+
return "failed";
|
|
58
65
|
}
|
|
59
|
-
// Exhausted retries end in the same state as a permanent error (webhook not registered, manual
|
|
60
|
-
// action required) — report at the same level.
|
|
61
|
-
log.error(`[fastagent] telegram: setWebhook still failing after retries (last error: ${lastTransientError}). ` +
|
|
62
|
-
`Register manually with url=${webhookUrl}`);
|
|
63
|
-
return "failed";
|
|
64
66
|
}
|
|
@@ -15,11 +15,15 @@ export default telegramChannel({
|
|
|
15
15
|
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
16
16
|
// full details always go to the server log regardless.
|
|
17
17
|
onError: (failed) => `⚠️ ${failed.details}`,
|
|
18
|
+
// Your bot's @username. Optional — resolved via getMe when omitted — but set it if you pass a `route`
|
|
19
|
+
// below: the channel needs its own copy to recognise a `/stop` addressed to it.
|
|
20
|
+
// botUsername: "your_bot",
|
|
18
21
|
// The channel owns transport + format (HTML) + attachments (photo→vision, file→disk) + streaming.
|
|
19
22
|
// `route` (POLICY) is OPTIONAL — omitted, it uses defaultTelegramRoute: private chats always answer,
|
|
20
|
-
// groups only on a reply to THIS bot
|
|
21
|
-
// export — but pass your bot's identity
|
|
22
|
-
// the channel; a bare `defaultTelegramRoute(u)` answers only
|
|
23
|
+
// groups only on a reply to THIS bot, an @mention of it, or a `/stop` addressed to it. Override to
|
|
24
|
+
// customise, reusing the export — but pass your bot's identity BOTH here and as `botUsername` above
|
|
25
|
+
// (the omitted default gets it from the channel; a bare `defaultTelegramRoute(u)` answers only
|
|
26
|
+
// private chats):
|
|
23
27
|
// route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { session: `user:${u.message?.from?.id}` },
|
|
24
28
|
// route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { text: `${telegramEnvelope(u.message!)}\n[extra]` },
|
|
25
29
|
});
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* parse fallback, and the getFile→download dance. (telegram.ts orchestrates; no agent, no rendering.)
|
|
17
17
|
*/
|
|
18
18
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
19
|
-
import { basename
|
|
19
|
+
import { basename } from "node:path";
|
|
20
|
+
import { attachmentPath } from "../kit/attachment-path.js";
|
|
20
21
|
/** Telegram's hard text limit per message. */
|
|
21
22
|
export const TELEGRAM_MAX_TEXT = 4096;
|
|
22
23
|
const PARSE_ERROR = /can't parse|entit|unsupported|unclosed|tag/i;
|
|
@@ -317,12 +318,10 @@ async function fetchTelegramImage(api, botToken, fileId) {
|
|
|
317
318
|
/** Download a Telegram file_id to <filesDir>/<chat>/<name>. Throws on failure / oversize. */
|
|
318
319
|
async function downloadTelegramFile(api, botToken, fileId, chatId, filesDir) {
|
|
319
320
|
const { bytes, remotePath } = await getFileBytes(api, botToken, fileId);
|
|
320
|
-
const name = basename(remotePath);
|
|
321
|
-
const dir = join(filesDir, String(chatId));
|
|
321
|
+
const { dir, name, path } = attachmentPath(filesDir, chatId, basename(remotePath));
|
|
322
322
|
await mkdir(dir, { recursive: true });
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return { path: dest, name, size: bytes.byteLength };
|
|
323
|
+
await writeFile(path, bytes);
|
|
324
|
+
return { path, name, size: bytes.byteLength };
|
|
326
325
|
}
|
|
327
326
|
/** Fetch the message's images. Throws if any cannot be loaded — the caller surfaces it (no silent drop). */
|
|
328
327
|
export async function resolveImages(api, botToken, fileIds) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChannelModule } from "../../channel.ts";
|
|
2
|
-
import { type TelegramMessage, type TelegramRoute, type TelegramUpdate, defaultTelegramRoute, telegramEnvelope } from "./parse.ts";
|
|
2
|
+
import { type TelegramMessage, type TelegramRoute, type TelegramUpdate, defaultTelegramRoute, telegramEnvelope, telegramStop } from "./parse.ts";
|
|
3
3
|
import { type TelegramFailure } from "./preview.ts";
|
|
4
|
-
export { defaultTelegramRoute, telegramEnvelope };
|
|
4
|
+
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
5
5
|
export type { TelegramFailure, TelegramMessage, TelegramRoute, TelegramUpdate };
|
|
6
6
|
export interface TelegramChannelOptions {
|
|
7
7
|
/** Webhook secret token (the `secret_token` you set via setWebhook); verifies inbound updates. */
|