@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RegistrationOutcome } from "../registration.ts";
|
|
2
2
|
import { type FeishuCloudKind } from "./cloud.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Register `<baseUrl>/<kind>` as the app's event Request URL (webhook mode). Missing credentials print
|
|
5
|
-
* the manual instruction instead of failing. `opts`
|
|
6
|
-
*
|
|
5
|
+
* the manual instruction instead of failing. `opts` carries the attempt budget — `--tunnel` takes the
|
|
6
|
+
* default, `deploy --run` passes `DEPLOY_REGISTRATION_ATTEMPTS` (a host starts slower than a tunnel) —
|
|
7
|
+
* plus `apiBase`, which is a test's fake platform (production derives it from the kind).
|
|
7
8
|
*
|
|
8
9
|
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
9
10
|
*/
|
|
@@ -12,8 +13,7 @@ interface FeishuManualRegistration {
|
|
|
12
13
|
requestUrl: string;
|
|
13
14
|
}
|
|
14
15
|
export interface RegisterFeishuWebhookOptions {
|
|
15
|
-
|
|
16
|
-
readyIntervalMs?: number;
|
|
16
|
+
attempts?: number;
|
|
17
17
|
retryMs?: number;
|
|
18
18
|
apiBase?: string;
|
|
19
19
|
/** Manual fallback after a definitive config error or exhausted retries. Local dev opens this App. */
|
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
* Mechanism: the application-v7 config PATCH (`updateEventSubscription`) flips the app's event
|
|
8
8
|
* subscription to webhook mode and points it at `<baseUrl>/<kind>`. Two properties make this the full
|
|
9
9
|
* telegram-setWebhook parity: the platform applies a request-URL change IMMEDIATELY (no version
|
|
10
|
-
* publish), and it VERIFIES the URL with a url_verification challenge during the PATCH — which
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* publish), and it VERIFIES the URL with a url_verification challenge during the PATCH — which makes
|
|
11
|
+
* the PATCH its own readiness probe, retried below. (It used to poll `<baseUrl>/health` from here
|
|
12
|
+
* first; that asked whether THIS machine could reach a freshly minted hostname, which is routinely
|
|
13
|
+
* false for a minute or more while the platform reaches it fine — #421.) Requires the
|
|
14
|
+
* `application:application:patch` scope (field-tested: `self_manage` does NOT cover this PATCH) —
|
|
15
|
+
* `add feishu` requests it at creation via addons; without it the PATCH fails visibly and the manual
|
|
16
|
+
* console instruction is printed.
|
|
15
17
|
*
|
|
16
18
|
* CLOUD LAG: the application-v7 config API exists on open.feishu.cn but (as of 2026-07) is NOT
|
|
17
19
|
* deployed on open.larksuite.com — the route 404s there. The registrar still attempts it (the day the
|
|
18
20
|
* platform ships it, registration starts working with no change here) and names the real cause in the
|
|
19
21
|
* fallback instead of blaming the app's scopes.
|
|
20
22
|
*/
|
|
21
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
22
23
|
import { log } from "../../log.js";
|
|
23
|
-
import {
|
|
24
|
+
import { retryWhile } from "../registration.js";
|
|
24
25
|
import { cloudFor } from "./cloud.js";
|
|
25
26
|
import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError } from "./feishu-api.js";
|
|
26
27
|
export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
|
|
@@ -35,16 +36,6 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
|
|
|
35
36
|
log.info(`[fastagent] ${kind}: set ${envPrefix}_APP_ID + ${envPrefix}_APP_SECRET in .env, then re-run to auto-register. Or ${manual}`);
|
|
36
37
|
return "manual";
|
|
37
38
|
}
|
|
38
|
-
// Align registration with the server actually serving: the PATCH triggers the platform's
|
|
39
|
-
// url_verification challenge against requestUrl — registering before /health serves would fail.
|
|
40
|
-
log.info(`[fastagent] ${kind}: waiting for ${baseUrl} to be reachable before registering the event URL…`);
|
|
41
|
-
const ready = await waitForHealth(`${baseUrl}/health`, opts.readyTimeoutMs ?? 120_000, opts.readyIntervalMs ?? 3_000);
|
|
42
|
-
if (!ready) {
|
|
43
|
-
// Terminal for this run (registration will not be retried) — error, not warn: the event URL is NOT
|
|
44
|
-
// registered and the operator must act. Same taxonomy as the permanent PATCH failure below.
|
|
45
|
-
log.error(`[fastagent] ${kind}: ${baseUrl}/health did not come up in time — the app may still be starting. ${manual}`);
|
|
46
|
-
return "failed";
|
|
47
|
-
}
|
|
48
39
|
const api = createFeishuApi({ kind, baseUrl: apiBase, appId, appSecret });
|
|
49
40
|
const consoleUrl = `${apiBase}/app/${encodeURIComponent(appId)}/event`;
|
|
50
41
|
const versionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/version`;
|
|
@@ -61,46 +52,41 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
|
|
|
61
52
|
log.warn(`[fastagent] ${kind}: could not open Events & Callbacks: ${String(callbackError)}`);
|
|
62
53
|
}
|
|
63
54
|
};
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
if (!isTransientFeishuRegistrationError(e)) {
|
|
93
|
-
log.error(`[fastagent] ${kind}: could not register the event URL (${String(e)}). ` +
|
|
94
|
-
`The app may lack the "application:application:patch" scope (console → Permissions) or be under review; manual registration is available below.`);
|
|
95
|
-
manualRegistration();
|
|
96
|
-
return "failed";
|
|
97
|
-
}
|
|
55
|
+
// The PATCH is the probe (same lesson as the token bootstrap): the platform verifies request_url
|
|
56
|
+
// with a challenge DURING the call, so its 210042 "request_url validation failed" is the readiness
|
|
57
|
+
// signal and is retried with backoff, alongside transient network errors. Only a permanent config
|
|
58
|
+
// error (missing scope, app under review, the intl 404) is reported once with the manual path.
|
|
59
|
+
try {
|
|
60
|
+
await retryWhile(() => api.updateEventSubscription(appId, { subscriptionType: "webhook", requestUrl }), isTransientFeishuRegistrationError, {
|
|
61
|
+
attempts: opts.attempts,
|
|
62
|
+
retryMs: opts.retryMs,
|
|
63
|
+
onRetry: ({ attempt, attempts }) => log.info(`[fastagent] ${kind}: the platform cannot verify ${requestUrl} yet (attempt ${attempt}/${attempts}); retrying…`),
|
|
64
|
+
});
|
|
65
|
+
log.info(`[fastagent] ${kind}: event Request URL registered → ${requestUrl}`);
|
|
66
|
+
// Field-tested: a URL change applies immediately, but the MODE flip (the template's long
|
|
67
|
+
// connection → webhook) only takes effect when a version is published — the dispatcher serves
|
|
68
|
+
// the published snapshot, and version publishing has no open API. One console click, once.
|
|
69
|
+
log.info(`[fastagent] ${kind}: if messages do not arrive, publish a version (one click, prompted) — the switch to webhook mode takes effect on publish: ${versionUrl}`);
|
|
70
|
+
return "registered";
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// A 404 on the config route is the CLOUD lagging, not this app's configuration: the v7 API is
|
|
74
|
+
// live on open.feishu.cn but not yet on open.larksuite.com. Name that — "check your scopes"
|
|
75
|
+
// would send the operator hunting for a problem they cannot fix. It stays WARN: on that cloud the
|
|
76
|
+
// manual path is the known norm, not an exceptional failure.
|
|
77
|
+
if (isFeishuConfigApiMissing(e)) {
|
|
78
|
+
log.warn(`[fastagent] ${kind}: this cloud (${apiBase}) returned HTTP 404 for the app-config API — ` +
|
|
79
|
+
`manual registration is required`);
|
|
80
|
+
manualRegistration();
|
|
81
|
+
return "manual"; // that cloud has no config API — the manual path is the norm there
|
|
98
82
|
}
|
|
83
|
+
// Exhausted retries end in the same state as a permanent config error (event URL not registered,
|
|
84
|
+
// manual action required) — report at the same level.
|
|
85
|
+
log.error(isTransientFeishuRegistrationError(e)
|
|
86
|
+
? `[fastagent] ${kind}: the platform could not verify ${requestUrl} after retries — manual registration is required`
|
|
87
|
+
: `[fastagent] ${kind}: could not register the event URL (${String(e)}). ` +
|
|
88
|
+
`The app may lack the "application:application:patch" scope (console → Permissions) or be under review; manual registration is available below.`);
|
|
89
|
+
manualRegistration();
|
|
90
|
+
return "failed";
|
|
99
91
|
}
|
|
100
|
-
// Exhausted retries end in the same state as a permanent error (event URL not registered, manual
|
|
101
|
-
// action required) — report at the same level. The cloud-lag 404 above stays warn: on that cloud the
|
|
102
|
-
// manual path is the known norm, not an exceptional failure.
|
|
103
|
-
log.error(`[fastagent] ${kind}: registration still failing after retries — manual registration is required`);
|
|
104
|
-
manualRegistration();
|
|
105
|
-
return "failed";
|
|
106
92
|
}
|
|
@@ -1,58 +1,8 @@
|
|
|
1
1
|
import { defineTool, z } from "@fastagent-sh/fastagent";
|
|
2
|
+
import { feishuTransport } from "@fastagent-sh/fastagent/feishu";
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// wake-up, whose plain reply is not delivered anywhere. The chatId comes from the [feishu: chat …]
|
|
6
|
-
// context line in a chat turn; a scheduled turn has no such line, so the schedule's prompt must name
|
|
7
|
-
// the target chat id. tools/ is auto-discovered.
|
|
8
|
-
|
|
9
|
-
// Embedded copy of the channel transport's discipline: a timeout so a wedged connection can't hang
|
|
10
|
-
// the tool call (and the turn), named errors, and success gated on the body's own code===0.
|
|
11
|
-
// Deliberately NO rate-limit retry — a tool error goes back to the agent, which can decide to retry;
|
|
12
|
-
// fail-fast beats a silently sleeping tool.
|
|
13
|
-
const BASE = "https://open.feishu.cn";
|
|
14
|
-
|
|
15
|
-
async function callApi(path: string, body: unknown, token?: string): Promise<Record<string, unknown>> {
|
|
16
|
-
let res: Response;
|
|
17
|
-
let raw: string;
|
|
18
|
-
try {
|
|
19
|
-
res = await fetch(`${BASE}${path}`, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: {
|
|
22
|
-
"content-type": "application/json; charset=utf-8",
|
|
23
|
-
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
24
|
-
},
|
|
25
|
-
body: JSON.stringify(body),
|
|
26
|
-
signal: AbortSignal.timeout(30_000),
|
|
27
|
-
});
|
|
28
|
-
raw = await res.text();
|
|
29
|
-
} catch (e) {
|
|
30
|
-
throw new Error(`feishu ${path}: ${String(e)}`, { cause: e });
|
|
31
|
-
}
|
|
32
|
-
let data: { code?: number; msg?: string; [k: string]: unknown };
|
|
33
|
-
try {
|
|
34
|
-
data = JSON.parse(raw) as typeof data;
|
|
35
|
-
} catch {
|
|
36
|
-
data = {};
|
|
37
|
-
}
|
|
38
|
-
if (!res.ok || data.code !== 0) {
|
|
39
|
-
throw new Error(`feishu ${path} failed: ${res.status} ${data.msg ?? "response was not the expected JSON"}`);
|
|
40
|
-
}
|
|
41
|
-
return data;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function tenantToken(): Promise<string> {
|
|
45
|
-
const appId = process.env.FEISHU_APP_ID;
|
|
46
|
-
const appSecret = process.env.FEISHU_APP_SECRET;
|
|
47
|
-
if (!appId || !appSecret) throw new Error("FEISHU_APP_ID / FEISHU_APP_SECRET are not set");
|
|
48
|
-
const data = await callApi("/open-apis/auth/v3/tenant_access_token/internal", {
|
|
49
|
-
app_id: appId,
|
|
50
|
-
app_secret: appSecret,
|
|
51
|
-
});
|
|
52
|
-
const token = data.tenant_access_token;
|
|
53
|
-
if (typeof token !== "string") throw new Error("feishu tenant_access_token: response carried no token");
|
|
54
|
-
return token;
|
|
55
|
-
}
|
|
4
|
+
// Proactive delivery uses the mounted channel's credentials, gateway and token cache. With no
|
|
5
|
+
// channel mounted (fire/invoke/tool), it reads FEISHU_APP_ID / FEISHU_APP_SECRET from the environment.
|
|
56
6
|
|
|
57
7
|
export default defineTool({
|
|
58
8
|
description:
|
|
@@ -70,23 +20,18 @@ export default defineTool({
|
|
|
70
20
|
text: z.string().optional().describe("plain text message to send"),
|
|
71
21
|
markdown: z.string().optional().describe("markdown to send as a card"),
|
|
72
22
|
}),
|
|
73
|
-
async execute({ chatId, text, markdown }) {
|
|
23
|
+
async execute({ chatId, text, markdown }, ctx) {
|
|
74
24
|
if ((text === undefined) === (markdown === undefined)) {
|
|
75
25
|
throw new Error("pass exactly one of `text` (plain) or `markdown` (a card)");
|
|
76
26
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
schema: "2.0",
|
|
86
|
-
body: { elements: [{ tag: "markdown", content: markdown }] },
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
await callApi(`/open-apis/im/v1/messages?receive_id_type=chat_id`, { receive_id: chatId, ...msg }, token);
|
|
27
|
+
const api = feishuTransport(ctx.cwd);
|
|
28
|
+
if (text !== undefined) await api.sendText({ chatId }, text);
|
|
29
|
+
else {
|
|
30
|
+
await api.sendMessage(chatId, "interactive", JSON.stringify({
|
|
31
|
+
schema: "2.0",
|
|
32
|
+
body: { elements: [{ tag: "markdown", content: markdown }] },
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
90
35
|
return `sent ${text !== undefined ? "message" : "card"} to chat ${chatId}`;
|
|
91
36
|
},
|
|
92
37
|
});
|
|
@@ -4,6 +4,36 @@ export type FeishuSubscriptionMode = "webhook" | "websocket";
|
|
|
4
4
|
* runtime remains capability-driven because the platform, not channel source, decides which events
|
|
5
5
|
* are delivered. */
|
|
6
6
|
export type FeishuGroupBehavior = "context" | "mentions";
|
|
7
|
+
/**
|
|
8
|
+
* Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and
|
|
9
|
+
* adding a scope to the app draft during context-aware group setup.
|
|
10
|
+
*
|
|
11
|
+
* Requested at creation for BOTH ingress modes, though only webhook uses it on day one. A WebSocket
|
|
12
|
+
* app that later wants webhook delivery cannot acquire it in passing: changing mode is a migration
|
|
13
|
+
* the CLI deliberately refuses to perform (`resolveIngress`), so the app would have to be recreated
|
|
14
|
+
* or the scope added by hand in the console after a failed PATCH. It is not a data scope — it reads
|
|
15
|
+
* and writes this app's own configuration — so carrying it costs an app nothing and keeps the one
|
|
16
|
+
* door open that the alternative nails shut.
|
|
17
|
+
*/
|
|
18
|
+
export declare const FEISHU_APP_CONFIG_SCOPE = "application:application:patch";
|
|
19
|
+
/** The one event an agent app cannot serve without: an inbound message. */
|
|
20
|
+
export declare const FEISHU_MESSAGE_RECEIVE_EVENT = "im.message.receive_v1";
|
|
21
|
+
/**
|
|
22
|
+
* What a created app carries on top of the platform's agent template, for EITHER ingress — the
|
|
23
|
+
* `addons` merged onto the confirm page. Here rather than inline at the call site because it is a
|
|
24
|
+
* policy decision (which capabilities an app is born with), and the CLI layer that requests it is
|
|
25
|
+
* terminal wiring with no seam to test through.
|
|
26
|
+
*/
|
|
27
|
+
export declare function feishuAppAddons(): {
|
|
28
|
+
scopes: {
|
|
29
|
+
tenant: string[];
|
|
30
|
+
};
|
|
31
|
+
events: {
|
|
32
|
+
items: {
|
|
33
|
+
tenant: string[];
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
7
37
|
/** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
|
|
8
38
|
export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
9
39
|
/** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and
|
|
3
|
+
* adding a scope to the app draft during context-aware group setup.
|
|
4
|
+
*
|
|
5
|
+
* Requested at creation for BOTH ingress modes, though only webhook uses it on day one. A WebSocket
|
|
6
|
+
* app that later wants webhook delivery cannot acquire it in passing: changing mode is a migration
|
|
7
|
+
* the CLI deliberately refuses to perform (`resolveIngress`), so the app would have to be recreated
|
|
8
|
+
* or the scope added by hand in the console after a failed PATCH. It is not a data scope — it reads
|
|
9
|
+
* and writes this app's own configuration — so carrying it costs an app nothing and keeps the one
|
|
10
|
+
* door open that the alternative nails shut.
|
|
11
|
+
*/
|
|
12
|
+
export const FEISHU_APP_CONFIG_SCOPE = "application:application:patch";
|
|
13
|
+
/** The one event an agent app cannot serve without: an inbound message. */
|
|
14
|
+
export const FEISHU_MESSAGE_RECEIVE_EVENT = "im.message.receive_v1";
|
|
15
|
+
/**
|
|
16
|
+
* What a created app carries on top of the platform's agent template, for EITHER ingress — the
|
|
17
|
+
* `addons` merged onto the confirm page. Here rather than inline at the call site because it is a
|
|
18
|
+
* policy decision (which capabilities an app is born with), and the CLI layer that requests it is
|
|
19
|
+
* terminal wiring with no seam to test through.
|
|
20
|
+
*/
|
|
21
|
+
export function feishuAppAddons() {
|
|
22
|
+
return {
|
|
23
|
+
scopes: { tenant: [FEISHU_APP_CONFIG_SCOPE] },
|
|
24
|
+
events: { items: { tenant: [FEISHU_MESSAGE_RECEIVE_EVENT] } },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
1
27
|
/** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
|
|
2
28
|
export const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
3
29
|
/** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FeishuCloudKind } from "./cloud.ts";
|
|
2
|
+
import { type FeishuApi } from "./feishu-api.ts";
|
|
3
|
+
/** The delivery methods a proactive sender needs. */
|
|
4
|
+
export type FeishuTransport = Pick<FeishuApi, "sendText" | "sendMessage">;
|
|
5
|
+
/** A remount replaces the transport; separate clouds can share one agent state root. */
|
|
6
|
+
export declare function registerFeishuApi(stateRoot: string, kind: FeishuCloudKind, api: FeishuApi): void;
|
|
7
|
+
/** Resolve the Feishu transport for a tool's workspace (`ctx.cwd`). */
|
|
8
|
+
export declare function feishuTransport(cwd: string): FeishuTransport;
|
|
9
|
+
/** Resolve the Lark transport for a tool's workspace (`ctx.cwd`). */
|
|
10
|
+
export declare function larkTransport(cwd: string): FeishuTransport;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The mounted channel and proactive tools share credentials, token cache, gateway and retry policy. */
|
|
2
|
+
import { findAgentDir, resolveStateRoot } from "../../paths.js";
|
|
3
|
+
import { cloudFor } from "./cloud.js";
|
|
4
|
+
import { createFeishuApi } from "./feishu-api.js";
|
|
5
|
+
const byKind = {
|
|
6
|
+
feishu: new Map(),
|
|
7
|
+
lark: new Map(),
|
|
8
|
+
};
|
|
9
|
+
/** A remount replaces the transport; separate clouds can share one agent state root. */
|
|
10
|
+
export function registerFeishuApi(stateRoot, kind, api) {
|
|
11
|
+
byKind[kind].set(stateRoot, api);
|
|
12
|
+
}
|
|
13
|
+
function cloudTransport(cwd, kind) {
|
|
14
|
+
// Embedded senders may use a bare definition directory or an independent workspace.
|
|
15
|
+
const stateRoot = resolveStateRoot(findAgentDir(cwd) ?? cwd);
|
|
16
|
+
const transports = byKind[kind];
|
|
17
|
+
let api = transports.get(stateRoot);
|
|
18
|
+
if (!api) {
|
|
19
|
+
// fire/invoke/tool do not mount channels, so their credentials come from the agent's environment.
|
|
20
|
+
const { apiBase, envPrefix } = cloudFor(kind);
|
|
21
|
+
const appId = process.env[`${envPrefix}_APP_ID`];
|
|
22
|
+
const appSecret = process.env[`${envPrefix}_APP_SECRET`];
|
|
23
|
+
if (!appId || !appSecret) {
|
|
24
|
+
throw new Error(`${envPrefix}_APP_ID / ${envPrefix}_APP_SECRET are not set and no ${kind} channel is mounted`);
|
|
25
|
+
}
|
|
26
|
+
api = createFeishuApi({ kind, baseUrl: apiBase, appId, appSecret });
|
|
27
|
+
transports.set(stateRoot, api);
|
|
28
|
+
}
|
|
29
|
+
return api;
|
|
30
|
+
}
|
|
31
|
+
/** Resolve the Feishu transport for a tool's workspace (`ctx.cwd`). */
|
|
32
|
+
export function feishuTransport(cwd) {
|
|
33
|
+
return cloudTransport(cwd, "feishu");
|
|
34
|
+
}
|
|
35
|
+
/** Resolve the Lark transport for a tool's workspace (`ctx.cwd`). */
|
|
36
|
+
export function larkTransport(cwd) {
|
|
37
|
+
return cloudTransport(cwd, "lark");
|
|
38
|
+
}
|
package/dist/channels/http.d.ts
CHANGED
|
@@ -13,14 +13,6 @@ import type { Agent } from "../agent.ts";
|
|
|
13
13
|
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
14
14
|
* dispatch imports it), so the two caps cannot drift apart. */
|
|
15
15
|
export declare const MAX_BODY_BYTES: number;
|
|
16
|
-
/** SSE comment-heartbeat interval, shared by every SSE surface (the control events route imports
|
|
17
|
-
* it, and the remote client sizes its dead-connection watchdog as a multiple of it). */
|
|
18
|
-
export declare const SSE_HEARTBEAT_MS = 30000;
|
|
19
|
-
/** The emitting half of the heartbeat contract (the client watchdog is the other): starts the
|
|
20
|
-
* `: ping` comment interval on an SSE stream controller and returns its stop function — ONE
|
|
21
|
-
* implementation for every SSE surface, so the emission side cannot regress on one route while
|
|
22
|
-
* the shared client watchdog keeps assuming it. Self-stops if the controller is already closed. */
|
|
23
|
-
export declare function sseHeartbeat(controller: ReadableStreamDefaultController<Uint8Array>): () => void;
|
|
24
16
|
/** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
|
|
25
17
|
* satisfy, so the CLI's "try it" hint can't drift from the protocol. */
|
|
26
18
|
export declare const INVOKE_EXAMPLE_BODY = "{\"session\":\"dev\",\"text\":\"hello\"}";
|
package/dist/channels/http.js
CHANGED
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
import { readBodyCapped } from "./body.js";
|
|
2
2
|
import { text } from "./respond.js";
|
|
3
|
+
import { sseResponse } from "./sse.js";
|
|
3
4
|
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
4
5
|
* dispatch imports it), so the two caps cannot drift apart. */
|
|
5
6
|
export const MAX_BODY_BYTES = 1 << 20;
|
|
6
|
-
const encoder = new TextEncoder();
|
|
7
|
-
/** SSE comment-heartbeat interval, shared by every SSE surface (the control events route imports
|
|
8
|
-
* it, and the remote client sizes its dead-connection watchdog as a multiple of it). */
|
|
9
|
-
export const SSE_HEARTBEAT_MS = 30_000;
|
|
10
|
-
/** The emitting half of the heartbeat contract (the client watchdog is the other): starts the
|
|
11
|
-
* `: ping` comment interval on an SSE stream controller and returns its stop function — ONE
|
|
12
|
-
* implementation for every SSE surface, so the emission side cannot regress on one route while
|
|
13
|
-
* the shared client watchdog keeps assuming it. Self-stops if the controller is already closed. */
|
|
14
|
-
export function sseHeartbeat(controller) {
|
|
15
|
-
const encoder = new TextEncoder();
|
|
16
|
-
const timer = setInterval(() => {
|
|
17
|
-
try {
|
|
18
|
-
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
clearInterval(timer);
|
|
22
|
-
}
|
|
23
|
-
}, SSE_HEARTBEAT_MS);
|
|
24
|
-
return () => clearInterval(timer);
|
|
25
|
-
}
|
|
26
7
|
/** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
|
|
27
8
|
* satisfy, so the CLI's "try it" hint can't drift from the protocol. */
|
|
28
9
|
export const INVOKE_EXAMPLE_BODY = '{"session":"dev","text":"hello"}';
|
|
@@ -48,7 +29,7 @@ export function createInvokeHandler(agent) {
|
|
|
48
29
|
if (typeof session !== "string" || typeof promptText !== "string") {
|
|
49
30
|
return text('need { "session": string, "text": string }\n', 400);
|
|
50
31
|
}
|
|
51
|
-
//
|
|
32
|
+
// INVOKE_EXAMPLE_BODY must keep satisfying this request shape.
|
|
52
33
|
// The OPTIONAL lineage extension (Scope): malformed values are a 400, not a silent drop — a
|
|
53
34
|
// caller that sent them meant them.
|
|
54
35
|
if (parentSession !== undefined && typeof parentSession !== "string") {
|
|
@@ -57,43 +38,10 @@ export function createInvokeHandler(agent) {
|
|
|
57
38
|
if (branchHints !== undefined && !(Array.isArray(branchHints) && branchHints.every((h) => typeof h === "string"))) {
|
|
58
39
|
return text('"branchHints" must be an array of strings\n', 400);
|
|
59
40
|
}
|
|
60
|
-
|
|
61
|
-
// run invoke's cancellation cleanup (SPEC MUST 3). pull = backpressure: the next event is produced on demand.
|
|
62
|
-
const iterator = agent
|
|
63
|
-
.invoke({
|
|
41
|
+
return sseResponse(agent.invoke({
|
|
64
42
|
session,
|
|
65
43
|
...(parentSession !== undefined ? { parentSession } : {}),
|
|
66
44
|
...(branchHints !== undefined ? { branchHints } : {}),
|
|
67
|
-
}, { text: promptText })
|
|
68
|
-
// Heartbeats: a QUIET stream (a long tool call, no events) is normal here — remote consumers
|
|
69
|
-
// distinguish "quiet but alive" from a dead connection by byte arrival, so silence must not
|
|
70
|
-
// look identical to a black hole (SSE comments are ignored by spec-conforming parsers).
|
|
71
|
-
let stopHeartbeat = () => { };
|
|
72
|
-
const stream = new ReadableStream({
|
|
73
|
-
start(controller) {
|
|
74
|
-
stopHeartbeat = sseHeartbeat(controller);
|
|
75
|
-
},
|
|
76
|
-
async pull(controller) {
|
|
77
|
-
const { value, done } = await iterator.next();
|
|
78
|
-
if (done) {
|
|
79
|
-
stopHeartbeat();
|
|
80
|
-
controller.close();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}\n\n`));
|
|
84
|
-
},
|
|
85
|
-
async cancel() {
|
|
86
|
-
stopHeartbeat();
|
|
87
|
-
await iterator.return?.();
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
return new Response(stream, {
|
|
91
|
-
status: 200,
|
|
92
|
-
headers: {
|
|
93
|
-
"content-type": "text/event-stream",
|
|
94
|
-
"cache-control": "no-cache",
|
|
95
|
-
connection: "keep-alive",
|
|
96
|
-
},
|
|
97
|
-
});
|
|
45
|
+
}, { text: promptText }));
|
|
98
46
|
};
|
|
99
47
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment destination for one conversation. Never rejects an id — but the path it returns is not
|
|
3
|
+
* promised to be one the filesystem accepts: percent-encoding spends up to 9 bytes per character, so
|
|
4
|
+
* an id of ~29 CJK characters exceeds a 255-byte `NAME_MAX` and the caller's `mkdir` fails with
|
|
5
|
+
* `ENAMETOOLONG`. Left to fail there rather than truncated here: a digest suffix would buy a length
|
|
6
|
+
* no platform id approaches, at the cost of the readability this encoding exists to keep.
|
|
7
|
+
*/
|
|
8
|
+
export declare function attachmentPath(filesDir: string, conversationId: string | number, fileName: string): {
|
|
9
|
+
dir: string;
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an inbound attachment lands: `<filesDir>/<conversation>/<file>`.
|
|
3
|
+
*
|
|
4
|
+
* Both halves come from outside the channel, and they are NOT the same question:
|
|
5
|
+
*
|
|
6
|
+
* - The conversation is an id, and turning an arbitrary caller id into a storage name is a job this
|
|
7
|
+
* repo already answers by ENCODING it, never by rejecting it (`piSessionId` in
|
|
8
|
+
* engines/pi/session-store.ts, same shape: a character whitelist under a fixed prefix). An id is
|
|
9
|
+
* the caller's — a route returning `../..` gets an odd directory, not a failed turn, because the
|
|
10
|
+
* place to notice that id is broken is the reply it cannot deliver, not the disk.
|
|
11
|
+
* - The file name is not an id: it is read by a human and by the model, in a path this puts into the
|
|
12
|
+
* prompt. Encoding it would cost that, so it is reduced instead — losing the odd character rather
|
|
13
|
+
* than the readability. A leading dot survives: `.gitignore` is a name, not an escape, and nothing
|
|
14
|
+
* lists this directory, so hiding is not a property anyone here depends on.
|
|
15
|
+
*
|
|
16
|
+
* The encoding makes containment structural rather than checked: `encodeURIComponent` emits no path
|
|
17
|
+
* separator (`/` → `%2F`, `\` → `%5C`, `:` → `%3A`), and the prefix leaves no way to spell `.`, `..`
|
|
18
|
+
* or the empty string.
|
|
19
|
+
*
|
|
20
|
+
* It is lossless for a well-formed id, so `a/b` and `a_b` are different places. Two exceptions, both
|
|
21
|
+
* outside what a platform hands over: ids differing only in malformed UTF-16 (see the substitution
|
|
22
|
+
* below), and ids differing only in case on a case-insensitive filesystem.
|
|
23
|
+
*/
|
|
24
|
+
import { resolve, sep } from "node:path";
|
|
25
|
+
/**
|
|
26
|
+
* Attachment destination for one conversation. Never rejects an id — but the path it returns is not
|
|
27
|
+
* promised to be one the filesystem accepts: percent-encoding spends up to 9 bytes per character, so
|
|
28
|
+
* an id of ~29 CJK characters exceeds a 255-byte `NAME_MAX` and the caller's `mkdir` fails with
|
|
29
|
+
* `ENAMETOOLONG`. Left to fail there rather than truncated here: a digest suffix would buy a length
|
|
30
|
+
* no platform id approaches, at the cost of the readability this encoding exists to keep.
|
|
31
|
+
*/
|
|
32
|
+
export function attachmentPath(filesDir, conversationId, fileName) {
|
|
33
|
+
// A lone surrogate makes `encodeURIComponent` throw `URIError`, and this function rejecting an id
|
|
34
|
+
// is the one thing it must not do — a permanent bad id would surface as a retryable turn failure.
|
|
35
|
+
// The `u` flag matches by code point, so a valid pair (an emoji in a route's id) is left alone.
|
|
36
|
+
const id = String(conversationId).replace(/\p{Surrogate}/gu, "\uFFFD");
|
|
37
|
+
const dir = resolve(filesDir, `c-${encodeURIComponent(id)}`);
|
|
38
|
+
// The name keeps its own check because it is NOT encoded: separators go, and `resolve` settles
|
|
39
|
+
// whether what is left (`..`, a Windows `D:foo`) still lands in `dir`.
|
|
40
|
+
const cleaned = fileName.replace(/[/\\]/g, "_");
|
|
41
|
+
const name = resolve(dir, cleaned).startsWith(dir + sep) ? cleaned : "file";
|
|
42
|
+
return { dir, name, path: resolve(dir, name) };
|
|
43
|
+
}
|
|
@@ -9,6 +9,15 @@ export declare const BUFFER_LINE_MAX_CHARS = 280;
|
|
|
9
9
|
* note, so the model never sees an attachment reference it silently cannot open. Shared policy:
|
|
10
10
|
* each channel's attachment collector caps against this. */
|
|
11
11
|
export declare const BUFFER_ATTACH_MAX = 3;
|
|
12
|
+
/**
|
|
13
|
+
* The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is
|
|
14
|
+
* empty. One renderer for every channel: what the agent is told about un-summoned discussion should
|
|
15
|
+
* not depend on which chat platform delivered it, and three copies of the literal is how that drifts.
|
|
16
|
+
*
|
|
17
|
+
* A channel that folds a SECOND source (feishu's originating room) labels that one itself — it is a
|
|
18
|
+
* different claim about a different place, not this block with another name.
|
|
19
|
+
*/
|
|
20
|
+
export declare function discussionBlock(text: string): string;
|
|
12
21
|
export interface ContextBuffer<E> {
|
|
13
22
|
/** Record an un-summoned message. Persists BEFORE returning (pre-ACK; see the module header). */
|
|
14
23
|
push(placeKey: string, entry: E): void;
|
|
@@ -39,6 +39,17 @@ export const BUFFER_LINE_MAX_CHARS = 280;
|
|
|
39
39
|
* note, so the model never sees an attachment reference it silently cannot open. Shared policy:
|
|
40
40
|
* each channel's attachment collector caps against this. */
|
|
41
41
|
export const BUFFER_ATTACH_MAX = 3;
|
|
42
|
+
/**
|
|
43
|
+
* The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is
|
|
44
|
+
* empty. One renderer for every channel: what the agent is told about un-summoned discussion should
|
|
45
|
+
* not depend on which chat platform delivered it, and three copies of the literal is how that drifts.
|
|
46
|
+
*
|
|
47
|
+
* A channel that folds a SECOND source (feishu's originating room) labels that one itself — it is a
|
|
48
|
+
* different claim about a different place, not this block with another name.
|
|
49
|
+
*/
|
|
50
|
+
export function discussionBlock(text) {
|
|
51
|
+
return text ? `[recent group discussion:\n${text}\n]\n\n` : "";
|
|
52
|
+
}
|
|
42
53
|
export function createContextBuffer(options) {
|
|
43
54
|
const { path, label, isEntry, line } = options;
|
|
44
55
|
const load = () => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replay window every signed webhook ingress needs. A signature covers its timestamp but proves
|
|
3
|
+
* nothing about it, so without a window a captured body plus its signed headers replays forever.
|
|
4
|
+
* The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
|
|
8
|
+
* ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
|
|
9
|
+
* signed material, so a value the signature commits to but this cannot read is a reason to refuse,
|
|
10
|
+
* not to wave through.
|
|
11
|
+
*/
|
|
12
|
+
export declare function signatureIsFresh(timestamp: string, maxAgeS: number, nowMs?: number): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replay window every signed webhook ingress needs. A signature covers its timestamp but proves
|
|
3
|
+
* nothing about it, so without a window a captured body plus its signed headers replays forever.
|
|
4
|
+
* The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
|
|
8
|
+
* ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
|
|
9
|
+
* signed material, so a value the signature commits to but this cannot read is a reason to refuse,
|
|
10
|
+
* not to wave through.
|
|
11
|
+
*/
|
|
12
|
+
export function signatureIsFresh(timestamp, maxAgeS, nowMs = Date.now()) {
|
|
13
|
+
if (!/^\d+$/.test(timestamp))
|
|
14
|
+
return false;
|
|
15
|
+
const seconds = Number(timestamp);
|
|
16
|
+
return Number.isSafeInteger(seconds) && Math.abs(Math.floor(nowMs / 1000) - seconds) <= maxAgeS;
|
|
17
|
+
}
|
|
@@ -21,16 +21,16 @@ export async function dispatchStop(control, session, label) {
|
|
|
21
21
|
if (!control)
|
|
22
22
|
return STOP_UNAVAILABLE_NOTICE;
|
|
23
23
|
try {
|
|
24
|
-
const result = await control.
|
|
24
|
+
const result = await control.sessions.get(session).abort();
|
|
25
25
|
if (result.ok)
|
|
26
26
|
return STOPPED_NOTICE;
|
|
27
27
|
if (result.error.code === NO_ACTIVE_RUN_CODE)
|
|
28
28
|
return NOTHING_RUNNING_NOTICE;
|
|
29
|
-
log.warn(`${label} stop
|
|
29
|
+
log.warn(`${label} stop rejected for ${session}: ${result.error.code} — ${result.error.message}`);
|
|
30
30
|
return `⚠️ Could not stop (${result.error.code}).`;
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
|
-
log.warn(`${label} stop
|
|
33
|
+
log.warn(`${label} stop failed for ${session}: ${String(error)}`);
|
|
34
34
|
return "⚠️ Could not stop — see the server logs.";
|
|
35
35
|
}
|
|
36
36
|
}
|