@fastagent-sh/fastagent 0.18.0 → 0.20.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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- 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 +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- 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 +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- 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 +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** Slack Web API transport: one JSON pipeline plus authenticated, capped private-file downloads. */
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import { codePointPrefix } from "../text.js";
|
|
3
|
+
import { attachmentPath } from "../kit/attachment-path.js";
|
|
4
|
+
import { codePointPrefix } from "../kit/text.js";
|
|
5
5
|
const API_TIMEOUT_MS = 30_000;
|
|
6
6
|
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
7
7
|
const MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
8
8
|
const RETRIES = 3;
|
|
9
9
|
const MAX_RETRY_AFTER_S = 30;
|
|
10
|
-
const MAX_REDIRECTS = 3;
|
|
11
10
|
/** Slack's standard-Markdown fields cap each call at 12,000 characters. Keep headroom for
|
|
12
11
|
* code-fence balancing and future server-side transformations. */
|
|
13
12
|
const SLACK_MAX_MARKDOWN = 10_000;
|
|
@@ -95,11 +94,6 @@ export function chunkSlackMarkdown(markdown, maxPoints = SLACK_MAX_MARKDOWN) {
|
|
|
95
94
|
}
|
|
96
95
|
return output;
|
|
97
96
|
}
|
|
98
|
-
function safeFileName(file) {
|
|
99
|
-
const raw = file.name ?? file.title ?? file.id ?? "file";
|
|
100
|
-
const safe = raw.replace(/[/\\]/g, "_").replace(/^\.+/, "_") || "file";
|
|
101
|
-
return `${file.id ?? "slack"}-${safe}`;
|
|
102
|
-
}
|
|
103
97
|
function fileDownloadUrl(file) {
|
|
104
98
|
if (file.file_access === "check_file_info")
|
|
105
99
|
throw new Error(`Slack file ${file.id ?? "(unknown)"} is not ready`);
|
|
@@ -140,23 +134,7 @@ async function readBytesCapped(response) {
|
|
|
140
134
|
}
|
|
141
135
|
export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" }) {
|
|
142
136
|
const apiBase = baseUrl.replace(/\/$/, "");
|
|
143
|
-
const configuredOrigin = new URL(apiBase).origin;
|
|
144
137
|
const currentToken = typeof botToken === "string" ? async () => botToken : botToken;
|
|
145
|
-
const trustedDownloadUrl = (value) => {
|
|
146
|
-
const url = new URL(value);
|
|
147
|
-
const host = url.hostname.toLowerCase();
|
|
148
|
-
const slackHost = host === "slack-files.com" ||
|
|
149
|
-
host.endsWith(".slack-files.com") ||
|
|
150
|
-
host === "slack.com" ||
|
|
151
|
-
host.endsWith(".slack.com") ||
|
|
152
|
-
host === "slack-edge.com" ||
|
|
153
|
-
host.endsWith(".slack-edge.com");
|
|
154
|
-
if (!slackHost && url.origin !== configuredOrigin)
|
|
155
|
-
throw new Error(`refusing non-Slack file URL host ${host}`);
|
|
156
|
-
if (url.protocol !== "https:" && url.origin !== configuredOrigin)
|
|
157
|
-
throw new Error("refusing non-HTTPS Slack file URL");
|
|
158
|
-
return url;
|
|
159
|
-
};
|
|
160
138
|
const call = async (method, body, httpMethod = "POST") => {
|
|
161
139
|
const url = new URL(`${apiBase}/${method}`);
|
|
162
140
|
if (httpMethod === "GET") {
|
|
@@ -211,36 +189,26 @@ export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" })
|
|
|
211
189
|
throw new SlackApiError(method, response.status, detail, data.error);
|
|
212
190
|
}
|
|
213
191
|
};
|
|
192
|
+
// Slack owns both halves of this download: the URL comes from its own files.info response, and it
|
|
193
|
+
// documents url_private as taking our bearer token. Where that URL leads is Slack's call; whether a
|
|
194
|
+
// redirect hop still carries the token is fetch's, which drops the header cross-origin.
|
|
214
195
|
const download = async (file) => {
|
|
215
|
-
let
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (response.status >= 300 && response.status < 400) {
|
|
230
|
-
const location = response.headers.get("location");
|
|
231
|
-
if (!location || redirect >= MAX_REDIRECTS) {
|
|
232
|
-
throw new SlackApiError("file download", response.status, "invalid or excessive redirect");
|
|
233
|
-
}
|
|
234
|
-
await response.body?.cancel().catch(() => { });
|
|
235
|
-
url = trustedDownloadUrl(new URL(location, url).toString());
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (!response.ok) {
|
|
239
|
-
const detail = codePointPrefix(await response.text().catch(() => ""), 300) || "file bytes were rejected";
|
|
240
|
-
throw new SlackApiError("file download", response.status, detail);
|
|
241
|
-
}
|
|
242
|
-
return { bytes: await readBytesCapped(response), contentType: response.headers.get("content-type") ?? undefined };
|
|
196
|
+
let response;
|
|
197
|
+
try {
|
|
198
|
+
const token = await currentToken();
|
|
199
|
+
response = await fetch(fileDownloadUrl(file), {
|
|
200
|
+
headers: { authorization: `Bearer ${token}` },
|
|
201
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw new SlackApiError("file download", 0, String(error), undefined, { cause: error });
|
|
206
|
+
}
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
const detail = codePointPrefix(await response.text().catch(() => ""), 300) || "file bytes were rejected";
|
|
209
|
+
throw new SlackApiError("file download", response.status, detail);
|
|
243
210
|
}
|
|
211
|
+
return { bytes: await readBytesCapped(response), contentType: response.headers.get("content-type") ?? undefined };
|
|
244
212
|
};
|
|
245
213
|
const api = {
|
|
246
214
|
async authTest() {
|
|
@@ -387,10 +355,10 @@ export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" })
|
|
|
387
355
|
},
|
|
388
356
|
async fetchFile(file, channelId, filesDir) {
|
|
389
357
|
const { bytes } = await download(file);
|
|
390
|
-
|
|
358
|
+
// The id prefix keeps two same-named uploads in one channel apart.
|
|
359
|
+
const suggested = `${file.id ?? "slack"}-${file.name ?? file.title ?? file.id ?? "file"}`;
|
|
360
|
+
const { dir, name, path } = attachmentPath(filesDir, channelId, suggested);
|
|
391
361
|
await mkdir(dir, { recursive: true });
|
|
392
|
-
const name = safeFileName(file);
|
|
393
|
-
const path = join(dir, name);
|
|
394
362
|
await writeFile(path, bytes);
|
|
395
363
|
return { path, name, size: bytes.byteLength };
|
|
396
364
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelModule } from "../../
|
|
1
|
+
import type { ChannelModule } from "../../channel.ts";
|
|
2
2
|
import { type SlackEventEnvelope, type SlackFile, type SlackMessageEvent, type SlackRoute, defaultSlackRoute, slackEnvelope } from "./parse.ts";
|
|
3
3
|
import { type SlackFailure, type SlackRendering } from "./preview.ts";
|
|
4
4
|
export { defaultSlackRoute, slackEnvelope };
|
|
@@ -4,14 +4,16 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { log } from "../../log.js";
|
|
5
5
|
import { readBodyCapped } from "../body.js";
|
|
6
6
|
import { text } from "../respond.js";
|
|
7
|
-
import { createSeenRing } from "../seen.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
7
|
+
import { createSeenRing } from "../kit/seen.js";
|
|
8
|
+
import { signatureIsFresh } from "../kit/signature.js";
|
|
9
|
+
import { createThreadParticipants } from "../kit/thread-participants.js";
|
|
10
|
+
import { createTaskTracker } from "../kit/tasks.js";
|
|
11
|
+
import { ensureStateHome } from "../kit/state.js";
|
|
12
|
+
import { dispatchStop, isStopText } from "../kit/stop-command.js";
|
|
13
|
+
import { codePointPrefix } from "../kit/text.js";
|
|
14
|
+
import { createTurnQueue } from "../kit/turn-queue.js";
|
|
15
|
+
import { commitAnsweredTurn, createTurnStore } from "../kit/turn-store.js";
|
|
16
|
+
import { discussionBlock } from "../kit/context-buffer.js";
|
|
15
17
|
import { createSlackBotTokenProvider } from "./bot-auth.js";
|
|
16
18
|
import { collectSlackBufferedFiles, createSlackContextBuffer } from "./context-buffer.js";
|
|
17
19
|
import { invokeSlackTurn } from "./invoke-turn.js";
|
|
@@ -22,7 +24,8 @@ import { createSlackApi } from "./slack-api.js";
|
|
|
22
24
|
import { createWelcomedUsers } from "./welcomed.js";
|
|
23
25
|
export { defaultSlackRoute, slackEnvelope };
|
|
24
26
|
const MAX_EVENT_BYTES = 1 << 20;
|
|
25
|
-
|
|
27
|
+
/** Slack's own documented window — it re-signs every redelivery with a current timestamp, so a tight
|
|
28
|
+
* one costs nothing. The Feishu ingress reads hours off the same helper for the opposite reason. */
|
|
26
29
|
const MAX_SIGNATURE_AGE_S = 5 * 60;
|
|
27
30
|
const QUEUED_PLACEHOLDER = "⏳ Queued — I’ll start once the current task finishes.";
|
|
28
31
|
const DEFERRED_PLACEHOLDER = "⏳ Delayed by a temporary system issue — I’ll retry automatically.";
|
|
@@ -45,10 +48,9 @@ function isStoredSlackTurn(value) {
|
|
|
45
48
|
}
|
|
46
49
|
/** Verify Slack's v0 HMAC over the exact raw body and reject timestamps outside the replay window. */
|
|
47
50
|
export function verifySlackSignature(signingSecret, timestamp, signature, rawBody, nowMs = Date.now()) {
|
|
48
|
-
if (
|
|
51
|
+
if (!/^v0=[a-f0-9]{64}$/i.test(signature))
|
|
49
52
|
return false;
|
|
50
|
-
|
|
51
|
-
if (!Number.isSafeInteger(seconds) || Math.abs(Math.floor(nowMs / 1000) - seconds) > MAX_SIGNATURE_AGE_S)
|
|
53
|
+
if (!signatureIsFresh(timestamp, MAX_SIGNATURE_AGE_S, nowMs))
|
|
52
54
|
return false;
|
|
53
55
|
const expected = `v0=${createHmac("sha256", signingSecret).update(`v0:${timestamp}:${rawBody}`).digest("hex")}`;
|
|
54
56
|
const actualBytes = Buffer.from(signature);
|
|
@@ -134,6 +136,9 @@ export function slackChannel(options) {
|
|
|
134
136
|
});
|
|
135
137
|
});
|
|
136
138
|
};
|
|
139
|
+
// Side tasks (stop feedback, DM welcomes) run off the ACK path but drain in turnsIdle. Declared
|
|
140
|
+
// ahead of its consumers so no construction-time path can reach it in the temporal dead zone.
|
|
141
|
+
const sideTasks = createTaskTracker(label);
|
|
137
142
|
const seen = createSeenRing(join(stateHome, "seen.json"), label);
|
|
138
143
|
const threadParticipants = createThreadParticipants(join(stateHome, "thread-participants.json"), label);
|
|
139
144
|
/** A thread's participation is keyed by the SESSION it describes — "the agent answered here" is a
|
|
@@ -186,7 +191,7 @@ export function slackChannel(options) {
|
|
|
186
191
|
log.error(`${label} deferring durable turn ${turn.id} because Slack authentication failed: ${String(error)}`);
|
|
187
192
|
return;
|
|
188
193
|
}
|
|
189
|
-
const attempt = store.startAttempt(turn.id
|
|
194
|
+
const attempt = store.startAttempt(turn.id);
|
|
190
195
|
if (attempt === "exceeded") {
|
|
191
196
|
notifyDropped(turn);
|
|
192
197
|
return;
|
|
@@ -205,7 +210,7 @@ export function slackChannel(options) {
|
|
|
205
210
|
const startedAt = Date.now();
|
|
206
211
|
log.info(`${label} turn start: turn=${turn.id} session=${turn.session} channel=${turn.channelId}`);
|
|
207
212
|
const { text: recent, consumed } = buffer.peek(turn.bufferKey);
|
|
208
|
-
const prompt =
|
|
213
|
+
const prompt = `${discussionBlock(recent)}${turn.baseText}`;
|
|
209
214
|
const buffered = collectSlackBufferedFiles(consumed, new Set(turn.fileIds));
|
|
210
215
|
const messageRef = messageRefOf(turn.id);
|
|
211
216
|
const reaction = reactionEmojis && messageRef
|
|
@@ -218,10 +223,7 @@ export function slackChannel(options) {
|
|
|
218
223
|
})
|
|
219
224
|
: undefined;
|
|
220
225
|
try {
|
|
221
|
-
await streamSlackReply(invokeSlackTurn(agent, turn.session, prompt, { api, channelId: turn.channelId, filesDir: join(stateHome, "files"), label }, { primaryFileIds: turn.fileIds, buffered }, () => {
|
|
222
|
-
store.remove(turn.id);
|
|
223
|
-
buffer.commit(turn.bufferKey, consumed);
|
|
224
|
-
}), api, targetOf(turn), formatError, {
|
|
226
|
+
await streamSlackReply(invokeSlackTurn(agent, turn.session, prompt, { api, channelId: turn.channelId, filesDir: join(stateHome, "files"), label }, { primaryFileIds: turn.fileIds, buffered }, () => commitAnsweredTurn(store, buffer, { id: turn.id, bufferKey: turn.bufferKey, consumed })), api, targetOf(turn), formatError, {
|
|
225
227
|
rendering,
|
|
226
228
|
initialPreviewTs: turn.previewTs,
|
|
227
229
|
threadTitle: turn.threadTitle,
|
|
@@ -410,8 +412,6 @@ export function slackChannel(options) {
|
|
|
410
412
|
});
|
|
411
413
|
}
|
|
412
414
|
};
|
|
413
|
-
// Side tasks (stop feedback, DM welcomes) run off the ACK path but drain in turnsIdle.
|
|
414
|
-
const sideTasks = createTaskTracker();
|
|
415
415
|
// First-run DM welcome: app_home_opened(tab="messages") signals a DM open. Post once per user.
|
|
416
416
|
const welcomeInFlight = new Set();
|
|
417
417
|
const maybeWelcome = (envelope) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Durable set of users who have already received the first-run DM welcome, so it is sent at most once. */
|
|
2
2
|
import { log } from "../../log.js";
|
|
3
|
-
import { loadStateFile, saveStateFile } from "../state.js";
|
|
3
|
+
import { loadStateFile, saveStateFile } from "../kit/state.js";
|
|
4
4
|
function key(teamId, userId) {
|
|
5
5
|
return `${teamId}:${userId}`;
|
|
6
6
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
|
|
2
|
+
* Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
|
|
3
3
|
* the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
|
|
4
4
|
* "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
|
|
5
5
|
* feeds whatever turn answers that place.
|
|
6
6
|
*/
|
|
7
|
-
import { type ContextBuffer as GenericContextBuffer } from "../context-buffer.ts";
|
|
7
|
+
import { type ContextBuffer as GenericContextBuffer } from "../kit/context-buffer.ts";
|
|
8
8
|
/** One buffered un-summoned message (object identity is the commit key). Besides the sender label and
|
|
9
9
|
* one-line body, it carries what a LATER summon needs to resolve references into the discussion:
|
|
10
10
|
* message ids ("reply to the one Alex answered"), and attachment file_ids so "summarize the file from
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
|
|
2
|
+
* Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
|
|
3
3
|
* the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
|
|
4
4
|
* "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
|
|
5
5
|
* feeds whatever turn answers that place.
|
|
6
6
|
*/
|
|
7
|
-
import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../context-buffer.js";
|
|
7
|
+
import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
|
|
8
8
|
function bufferLine(e) {
|
|
9
9
|
const meta = [
|
|
10
10
|
e.messageId !== undefined ? `msg ${e.messageId}` : undefined,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* wiring and the per-turn lifecycle.
|
|
7
7
|
*/
|
|
8
8
|
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
9
|
-
import { type BusyRetry } from "../invoke-turn-kit.ts";
|
|
9
|
+
import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
|
|
10
10
|
import type { BufferedRef } from "./context-buffer.ts";
|
|
11
11
|
/** Everything the transport needs to fetch a turn's attachments. */
|
|
12
12
|
export interface TurnTransport {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { log } from "../../log.js";
|
|
2
|
-
import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../invoke-turn-kit.js";
|
|
2
|
+
import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../kit/invoke-turn-kit.js";
|
|
3
3
|
import { resolveFiles, resolveImages } from "./telegram-api.js";
|
|
4
4
|
/** Appended to the prompt (not the system prompt): the channel owns Telegram-HTML formatting. */
|
|
5
5
|
const HTML_INSTRUCTION = "\n\n(Format your reply in Telegram-supported HTML — <b> <i> <u> <s> <code> <pre> <a href> — not Markdown.)";
|
|
@@ -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?: {
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* "decide" and prompt-building half; telegram.ts wires it in and owns the stateful lifecycle. Kept
|
|
6
6
|
* separate so this layer tests as plain functions and reads without the factory's noise.
|
|
7
7
|
*/
|
|
8
|
-
import { BUFFER_LINE_MAX_CHARS } from "../context-buffer.js";
|
|
9
|
-
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../text.js";
|
|
8
|
+
import { BUFFER_LINE_MAX_CHARS } from "../kit/context-buffer.js";
|
|
9
|
+
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../kit/text.js";
|
|
10
10
|
/** The actionable message in an update (a fresh message or channel post). Edits (`edited_message` /
|
|
11
11
|
* `edited_channel_post`) are deliberately NOT actionable: answering them re-answers every typo fix (a
|
|
12
12
|
* duplicate reply per edit), so an edited message changes nothing — the standard bot behavior. The
|
|
@@ -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
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* message, works in groups and private (unlike sendMessageDraft, which is private/forum-topic only).
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentEvent } from "../../agent.ts";
|
|
9
|
-
import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
|
|
9
|
+
import { type ChannelFailure, defaultErrorMessage } from "../kit/preview-kit.ts";
|
|
10
10
|
import { type Target } from "./telegram-api.ts";
|
|
11
11
|
/** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
|
|
12
12
|
export type TelegramFailure = ChannelFailure;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../preview-kit.js";
|
|
1
|
+
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
|
|
2
2
|
import { log } from "../../log.js";
|
|
3
3
|
import { TELEGRAM_MAX_TEXT, callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
4
4
|
export { defaultErrorMessage };
|
|
@@ -120,7 +120,11 @@ export async function streamReply(events, api, botToken, target, formatError, pr
|
|
|
120
120
|
// below regardless.
|
|
121
121
|
finalized = true;
|
|
122
122
|
{
|
|
123
|
-
const msg = formatError({
|
|
123
|
+
const msg = formatError({
|
|
124
|
+
details: e.details,
|
|
125
|
+
retryable: e.retryable,
|
|
126
|
+
...(e.code !== undefined ? { code: e.code } : {}),
|
|
127
|
+
}) ?? "";
|
|
124
128
|
await finalize(api, botToken, target, messageId, msg).catch(() => { });
|
|
125
129
|
}
|
|
126
130
|
throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
|
|
@@ -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
|
-
import type { ChannelModule } from "../../
|
|
2
|
-
import { type TelegramMessage, type TelegramRoute, type TelegramUpdate, defaultTelegramRoute, telegramEnvelope } from "./parse.ts";
|
|
1
|
+
import type { ChannelModule } from "../../channel.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. */
|