@fastagent-sh/fastagent 0.14.0 → 0.16.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { INVALID_COMMAND_CODE } from "../session.js";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { readBodyCapped } from "./body.js";
|
|
4
|
+
import { MAX_BODY_BYTES, createInvokeHandler, sseHeartbeat } from "./http.js";
|
|
5
|
+
import { text } from "./respond.js";
|
|
6
|
+
const json = (value, status = 200) => new Response(`${JSON.stringify(value)}\n`, { status, headers: { "content-type": "application/json" } });
|
|
7
|
+
// ONE constant for every Prompt-bearing wire surface (imported from the invoke channel — the two
|
|
8
|
+
// caps cannot drift apart): commands carry Prompts, which may ride base64 images.
|
|
9
|
+
const DISPATCH_BODY_LIMIT = MAX_BODY_BYTES;
|
|
10
|
+
/**
|
|
11
|
+
* Parse-don't-validate at the wire: a remote client can send any JSON, and the hub's inner layers
|
|
12
|
+
* trust command shapes (a malformed `steer` would surface as an ENGINE failure misclassified as
|
|
13
|
+
* `run_command_failed`). Returns the typed command, or undefined for anything malformed — which
|
|
14
|
+
* answers protocol-level `invalid_command`, same responsibility as the hub's unknown-type default.
|
|
15
|
+
*/
|
|
16
|
+
function parseWireCommand(raw) {
|
|
17
|
+
// COMPILE-TIME drift guard, variant level: this switch hand-mirrors the SessionCommand union,
|
|
18
|
+
// and a new variant added in session.ts would otherwise compile clean while the wire answers it
|
|
19
|
+
// `invalid_command` — silently breaking local/remote isomorphism. A new variant must break THIS
|
|
20
|
+
// line first, forcing the decision of how the wire carries it.
|
|
21
|
+
const _commandDriftGuard = {
|
|
22
|
+
steer: true,
|
|
23
|
+
follow_up: true,
|
|
24
|
+
abort: true,
|
|
25
|
+
compact: true,
|
|
26
|
+
set_model: true,
|
|
27
|
+
set_thinking: true,
|
|
28
|
+
};
|
|
29
|
+
void _commandDriftGuard;
|
|
30
|
+
if (typeof raw !== "object" || raw === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
const c = raw;
|
|
33
|
+
const imageOk = (i) => typeof i === "object" &&
|
|
34
|
+
i !== null &&
|
|
35
|
+
typeof i.data === "string" &&
|
|
36
|
+
typeof i.mimeType === "string";
|
|
37
|
+
const promptOk = (p) => {
|
|
38
|
+
if (typeof p !== "object" || p === null)
|
|
39
|
+
return false;
|
|
40
|
+
if (typeof p.text !== "string")
|
|
41
|
+
return false;
|
|
42
|
+
const images = p.images;
|
|
43
|
+
// Element-level: `images: [42]` reaching the engine would resurface exactly the misclassified
|
|
44
|
+
// failure this parser exists to prevent (ImageRef shape from src/session.ts's Prompt).
|
|
45
|
+
return images === undefined || (Array.isArray(images) && images.every(imageOk));
|
|
46
|
+
};
|
|
47
|
+
// REBUILD, never pass raw through: "typed command out" must be construction, not assertion — a
|
|
48
|
+
// passed-through object would carry arbitrary extra keys into the engine.
|
|
49
|
+
const rebuildPrompt = (p) => {
|
|
50
|
+
const images = p.images;
|
|
51
|
+
return {
|
|
52
|
+
text: p.text,
|
|
53
|
+
...(images ? { images: images.map((i) => ({ data: i.data, mimeType: i.mimeType })) } : {}),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
// COMPILE-TIME drift guard: whitelist reconstruction silently strips any field it does not know.
|
|
57
|
+
// A new Prompt field must break THIS line (non-empty Exclude → {} unassignable), not vanish on
|
|
58
|
+
// the wire while the client believes it was sent.
|
|
59
|
+
const _promptDriftGuard = {};
|
|
60
|
+
void _promptDriftGuard;
|
|
61
|
+
// Same guard one level down: the image whitelist ({data, mimeType}) must break when ImageRef
|
|
62
|
+
// grows a field — top-level coverage alone would let element fields vanish silently.
|
|
63
|
+
const _imageDriftGuard = {};
|
|
64
|
+
void _imageDriftGuard;
|
|
65
|
+
switch (c.type) {
|
|
66
|
+
case "steer":
|
|
67
|
+
case "follow_up":
|
|
68
|
+
return promptOk(c.prompt) ? { type: c.type, prompt: rebuildPrompt(c.prompt) } : undefined;
|
|
69
|
+
case "abort":
|
|
70
|
+
return { type: "abort" };
|
|
71
|
+
case "compact":
|
|
72
|
+
return c.instructions === undefined || typeof c.instructions === "string"
|
|
73
|
+
? { type: "compact", instructions: c.instructions }
|
|
74
|
+
: undefined;
|
|
75
|
+
case "set_model":
|
|
76
|
+
return typeof c.model === "string" ? { type: "set_model", model: c.model } : undefined;
|
|
77
|
+
case "set_thinking":
|
|
78
|
+
return typeof c.level === "string" ? { type: "set_thinking", level: c.level } : undefined;
|
|
79
|
+
default:
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mount the control plane: `GET /control/capabilities|state|entries|events` + `POST
|
|
85
|
+
* /control/dispatch`, all bearer-authenticated. `events` streams SSE (`data: <WireEvent>` lines).
|
|
86
|
+
*/
|
|
87
|
+
export function controlRoutes(control, options) {
|
|
88
|
+
const { token } = options;
|
|
89
|
+
if (!token)
|
|
90
|
+
throw new Error("controlRoutes: a bearer token is required (empty tokens are not a mode)");
|
|
91
|
+
const epoch = crypto.randomUUID();
|
|
92
|
+
// Timing-safe: the bearer token is this surface's ONLY auth (and the --tunnel warning names it
|
|
93
|
+
// as the sole protection on a public URL) — a plain === would leak byte-by-byte via timing.
|
|
94
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
95
|
+
const authed = (req) => {
|
|
96
|
+
const header = Buffer.from(req.headers.get("authorization") ?? "");
|
|
97
|
+
return header.length === expected.length && timingSafeEqual(header, expected);
|
|
98
|
+
};
|
|
99
|
+
const invokeHandler = options.agent ? createInvokeHandler(options.agent) : undefined;
|
|
100
|
+
/** Wrap a handler with auth + the session query param most routes need. */
|
|
101
|
+
const guard = (handler) => (req) => {
|
|
102
|
+
if (!authed(req))
|
|
103
|
+
return text("unauthorized\n", 401);
|
|
104
|
+
return handler(req, new URL(req.url));
|
|
105
|
+
};
|
|
106
|
+
// Extraction only — each route still answers its own 400 (the name must not imply enforcement).
|
|
107
|
+
const sessionParam = (url) => url.searchParams.get("session") ?? undefined;
|
|
108
|
+
return {
|
|
109
|
+
...(invokeHandler ? { "POST /control/invoke": guard((req) => invokeHandler(req)) } : {}),
|
|
110
|
+
"GET /control/capabilities": guard(() => json(control.capabilities())),
|
|
111
|
+
"GET /control/state": guard(async (_req, url) => {
|
|
112
|
+
const session = sessionParam(url);
|
|
113
|
+
if (!session)
|
|
114
|
+
return text("missing ?session\n", 400);
|
|
115
|
+
return json(await control.state(session));
|
|
116
|
+
}),
|
|
117
|
+
"GET /control/entries": guard(async (_req, url) => {
|
|
118
|
+
const session = sessionParam(url);
|
|
119
|
+
if (!session)
|
|
120
|
+
return text("missing ?session\n", 400);
|
|
121
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
122
|
+
return json(await control.entries(session, since !== undefined ? { since } : undefined));
|
|
123
|
+
}),
|
|
124
|
+
"POST /control/dispatch": guard(async (req) => {
|
|
125
|
+
const body = await readBodyCapped(req, DISPATCH_BODY_LIMIT);
|
|
126
|
+
// The 413 names the ceiling: the docs promise images on this plane, and an unexplained
|
|
127
|
+
// rejection would send a client author hunting everywhere but the cap.
|
|
128
|
+
if ("tooLarge" in body) {
|
|
129
|
+
// Derived from the constant — a hardcoded "1 MiB" would lie the day the cap changes.
|
|
130
|
+
return text(`body too large (limit ${MAX_BODY_BYTES >> 20} MiB — images count base64-inflated)\n`, 413);
|
|
131
|
+
}
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(body.text);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return text("invalid JSON\n", 400);
|
|
138
|
+
}
|
|
139
|
+
if (typeof parsed.session !== "string") {
|
|
140
|
+
return text("expected { session: string, command: SessionCommand }\n", 400);
|
|
141
|
+
}
|
|
142
|
+
const command = parseWireCommand(parsed.command);
|
|
143
|
+
if (!command) {
|
|
144
|
+
// Malformed shape = a protocol-level answer, mirrored from the hub's unknown-type default.
|
|
145
|
+
return json({
|
|
146
|
+
ok: false,
|
|
147
|
+
error: { code: INVALID_COMMAND_CODE, message: "malformed command", retryable: false },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// The result rides HTTP 200 either way: `ok: false` is a protocol-level answer (rejected
|
|
151
|
+
// before acceptance), not a transport failure.
|
|
152
|
+
return json(await control.dispatch(parsed.session, command));
|
|
153
|
+
}),
|
|
154
|
+
"GET /control/events": guard((_req, url) => {
|
|
155
|
+
const session = sessionParam(url);
|
|
156
|
+
if (!session)
|
|
157
|
+
return text("missing ?session\n", 400);
|
|
158
|
+
const iterator = control.events(session)[Symbol.asyncIterator]();
|
|
159
|
+
// EAGER registration: issue the first pull NOW, before the Response (and thus the client's
|
|
160
|
+
// fetch resolution) exists — hub subscription is registered synchronously inside next(), so
|
|
161
|
+
// "the client saw response headers" implies "events from that moment on will be delivered".
|
|
162
|
+
// Shrinks the subscribe/backfill race to network reordering instead of a full pull cycle.
|
|
163
|
+
let pending = iterator.next();
|
|
164
|
+
// Observed here so a client that disconnects BEFORE the first pull cannot turn a rejecting
|
|
165
|
+
// events iterator (this is the neutral contract face — any implementation may reject) into a
|
|
166
|
+
// process-killing unhandledRejection; awaiting `pending` at pull still surfaces the error.
|
|
167
|
+
pending.catch(() => { });
|
|
168
|
+
let seq = 0;
|
|
169
|
+
let stopHeartbeat = () => { };
|
|
170
|
+
const encoder = new TextEncoder();
|
|
171
|
+
const stream = new ReadableStream({
|
|
172
|
+
start(controller) {
|
|
173
|
+
stopHeartbeat = sseHeartbeat(controller);
|
|
174
|
+
},
|
|
175
|
+
async pull(controller) {
|
|
176
|
+
let next;
|
|
177
|
+
try {
|
|
178
|
+
next = await (pending ?? iterator.next());
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
// A rejecting implementation (the neutral contract permits it) must not leak its
|
|
182
|
+
// subscription: an errored stream never gets cancel(), so the unsubscribe and the
|
|
183
|
+
// heartbeat teardown happen HERE.
|
|
184
|
+
stopHeartbeat();
|
|
185
|
+
void iterator.return?.(undefined)?.catch?.(() => { });
|
|
186
|
+
controller.error(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
pending = undefined;
|
|
190
|
+
if (next.done) {
|
|
191
|
+
stopHeartbeat();
|
|
192
|
+
controller.close();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const wire = { sessionId: session, epoch, seq: seq++, event: next.value };
|
|
196
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(wire)}\n\n`));
|
|
197
|
+
},
|
|
198
|
+
cancel() {
|
|
199
|
+
stopHeartbeat();
|
|
200
|
+
// Same neutral-contract defense as the pull error path: a rejecting return() on client
|
|
201
|
+
// disconnect must not become a process-level unhandledRejection.
|
|
202
|
+
void iterator.return?.(undefined)?.catch?.(() => { });
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
return new Response(stream, {
|
|
206
|
+
headers: {
|
|
207
|
+
"content-type": "text/event-stream",
|
|
208
|
+
"cache-control": "no-cache",
|
|
209
|
+
connection: "keep-alive",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Budget: a card entity is capped at 30 KB, so the final answer's card chunk (and the live view) stay
|
|
12
12
|
* well under it; longer answers overflow into follow-up messages (preview.ts owns that policy).
|
|
13
13
|
*/
|
|
14
|
-
import { truncateCodePointPrefix } from "
|
|
14
|
+
import { truncateCodePointPrefix } from "../text.js";
|
|
15
15
|
/** The one streamed element's id — shared by create (card.ts) and update (preview.ts). */
|
|
16
16
|
export const ANSWER_ELEMENT_ID = "answer";
|
|
17
17
|
/** Byte budget for markdown carried by ONE card (entity cap 30 KB minus JSON envelope + escaping room). */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ContextBuffer } from "../context-buffer.ts";
|
|
1
2
|
import type { NormalizedFeishuMessage } from "./model.ts";
|
|
2
3
|
export interface FeishuBufferedResource {
|
|
3
4
|
messageId: string;
|
|
@@ -16,8 +17,19 @@ export interface FeishuBufferEntry {
|
|
|
16
17
|
export interface FeishuBufferedRef extends FeishuBufferedResource {
|
|
17
18
|
from: string;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The place a message belongs to: the main chat, or a thread within it. Keyed by `thread_id`, the
|
|
22
|
+
* platform's own identity for a side conversation — NOT `root_id`, which tracks the reply chain and
|
|
23
|
+
* can differ between messages of one thread (which would split a thread's context across buckets).
|
|
24
|
+
* A quoted reply outside a thread carries a root but is main-chat discussion, so it buckets there.
|
|
25
|
+
*
|
|
26
|
+
* Its own namespace, deliberately: this names a BUCKET of undelivered text, while `parse.ts`'s
|
|
27
|
+
* `placeKey` names a SESSION. Nothing here claims anything about that session — unlike thread
|
|
28
|
+
* participation, which asserts "the agent answered into this memory" and is therefore keyed by the
|
|
29
|
+
* session itself. Re-keying either one leaves the other correct, and converging them now would strand
|
|
30
|
+
* live buckets for no gain (Slack keeps its own shape for the same reason).
|
|
31
|
+
*/
|
|
32
|
+
export declare function feishuBufferPlaceKey(conversation: Pick<NormalizedFeishuMessage["conversation"], "chatId" | "threadId">): string;
|
|
21
33
|
/** One-line, bounded background text. Resource-only messages already carry a visible decoder marker. */
|
|
22
34
|
export declare function feishuBufferText(text: string): string;
|
|
23
35
|
/**
|
|
@@ -32,15 +44,5 @@ export declare function collectFeishuBufferedAttachments(consumed: FeishuBufferE
|
|
|
32
44
|
images: FeishuBufferedRef[];
|
|
33
45
|
skipped: number;
|
|
34
46
|
};
|
|
35
|
-
export
|
|
36
|
-
/** Persist before webhook ACK. A failed write throws and rolls memory back for safe redelivery. */
|
|
37
|
-
push(placeKey: string, entry: FeishuBufferEntry): void;
|
|
38
|
-
/** Render and snapshot without clearing. */
|
|
39
|
-
peek(placeKey: string): {
|
|
40
|
-
text: string;
|
|
41
|
-
consumed: FeishuBufferEntry[];
|
|
42
|
-
};
|
|
43
|
-
/** Remove only the consumed snapshot after `completed`; a post-ACK write failure is logged. */
|
|
44
|
-
commit(placeKey: string, consumed: FeishuBufferEntry[]): void;
|
|
45
|
-
}
|
|
47
|
+
export type FeishuContextBuffer = ContextBuffer<FeishuBufferEntry>;
|
|
46
48
|
export declare function createFeishuContextBuffer(path: string, label: string): FeishuContextBuffer;
|
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Feishu/Lark's half of the shared context buffer (mechanics + consume protocol:
|
|
3
|
+
* ../context-buffer.ts): the entry shape, its fold-line rendering, place-key derivation, and
|
|
4
|
+
* buffered-resource selection. Entries are bucketed by conversation place (main chat, or one
|
|
5
|
+
* concrete thread root) and folded into the next answered turn in that place.
|
|
6
6
|
*/
|
|
7
7
|
import { log } from "../../log.js";
|
|
8
|
+
import { BUFFER_ATTACH_MAX, BUFFER_LINE_MAX_CHARS, createContextBuffer as createGenericContextBuffer, } from "../context-buffer.js";
|
|
8
9
|
import { loadStateFile, saveStateFile } from "../state.js";
|
|
9
|
-
|
|
10
|
-
const BUFFER_ATTACH_MAX = 3;
|
|
10
|
+
import { truncateCodePointPrefix } from "../text.js";
|
|
11
11
|
function bufferLine(entry) {
|
|
12
12
|
const meta = [`msg ${entry.messageId}`, entry.replyTo ? `reply to msg ${entry.replyTo}` : undefined]
|
|
13
13
|
.filter(Boolean)
|
|
14
14
|
.join(", ");
|
|
15
15
|
return `${entry.sender} (${meta}): ${entry.body}`;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The place a message belongs to: the main chat, or a thread within it. Keyed by `thread_id`, the
|
|
19
|
+
* platform's own identity for a side conversation — NOT `root_id`, which tracks the reply chain and
|
|
20
|
+
* can differ between messages of one thread (which would split a thread's context across buckets).
|
|
21
|
+
* A quoted reply outside a thread carries a root but is main-chat discussion, so it buckets there.
|
|
22
|
+
*
|
|
23
|
+
* Its own namespace, deliberately: this names a BUCKET of undelivered text, while `parse.ts`'s
|
|
24
|
+
* `placeKey` names a SESSION. Nothing here claims anything about that session — unlike thread
|
|
25
|
+
* participation, which asserts "the agent answered into this memory" and is therefore keyed by the
|
|
26
|
+
* session itself. Re-keying either one leaves the other correct, and converging them now would strand
|
|
27
|
+
* live buckets for no gain (Slack keeps its own shape for the same reason).
|
|
28
|
+
*/
|
|
18
29
|
export function feishuBufferPlaceKey(conversation) {
|
|
19
|
-
|
|
20
|
-
return topic ? `${conversation.chatId}:root:${topic}` : conversation.chatId;
|
|
30
|
+
return conversation.threadId ? `${conversation.chatId}:thread:${conversation.threadId}` : conversation.chatId;
|
|
21
31
|
}
|
|
22
32
|
/** One-line, bounded background text. Resource-only messages already carry a visible decoder marker. */
|
|
23
33
|
export function feishuBufferText(text) {
|
|
24
|
-
return text.replace(/\s+/g, " ").trim()
|
|
34
|
+
return truncateCodePointPrefix(text.replace(/\s+/g, " ").trim(), BUFFER_LINE_MAX_CHARS);
|
|
25
35
|
}
|
|
26
36
|
function resourceIdentity(resource) {
|
|
27
37
|
return `${resource.messageId}\u0000${resource.key}`;
|
|
@@ -70,64 +80,47 @@ function isEntry(value) {
|
|
|
70
80
|
resources(entry.files) &&
|
|
71
81
|
resources(entry.images));
|
|
72
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Buckets from the pre-participant-model keying (`<chat>:root:<root_id>`) can never be produced again —
|
|
85
|
+
* a place is `<chat>` or `<chat>:thread:<thread_id>` — so nothing could ever fold or clear them, and
|
|
86
|
+
* they would hold chat content on disk forever. Dropped here, before the buffer loads, so the shared
|
|
87
|
+
* kernel never learns about a key shape one channel retired.
|
|
88
|
+
*
|
|
89
|
+
* TWO one-time losses, both accepted and both logged by count. (1) The retired shape covered every
|
|
90
|
+
* thread bucket and every main-chat quoted-reply bucket, so buffered discussion in threads does not
|
|
91
|
+
* survive the upgrade — it becomes unreachable BECAUSE of the re-keying, not before it. (2)
|
|
92
|
+
* `turns.json` persists each in-flight turn's `bufferKey` verbatim and this runs before turn recovery,
|
|
93
|
+
* so a turn spanning the upgrade finds its bucket already gone. Sparing referenced keys would couple
|
|
94
|
+
* the buffer to the turn store to protect a single upgrade, and would not help (1) at all.
|
|
95
|
+
*
|
|
96
|
+
* PERMANENT, unlike the `owned-threads.json` cleanup it otherwise resembles. That one leaves an inert
|
|
97
|
+
* orphan file, so deleting it a release later is free; this one is what stops user chat content
|
|
98
|
+
* lingering, and a deployment that skips from before the model to well after it would never run an
|
|
99
|
+
* expired version of this code. The standing cost is one key scan at load, and nothing when no retired
|
|
100
|
+
* key is present.
|
|
101
|
+
*/
|
|
102
|
+
function dropRetiredBuckets(path, label) {
|
|
103
|
+
const raw = loadStateFile(path);
|
|
104
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
105
|
+
return;
|
|
106
|
+
const live = Object.entries(raw).filter(([placeKey]) => !placeKey.includes(":root:"));
|
|
107
|
+
const dropped = Object.keys(raw).length - live.length;
|
|
108
|
+
if (dropped === 0)
|
|
109
|
+
return;
|
|
110
|
+
log.info(`${label} dropped ${dropped} context bucket(s) with a retired key shape`);
|
|
111
|
+
try {
|
|
112
|
+
saveStateFile(path, Object.fromEntries(live));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
log.warn(`${label} could not rewrite ${path} after dropping retired buckets: ${String(error)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
73
118
|
export function createFeishuContextBuffer(path, label) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Object.values(raw).every((entries) => Array.isArray(entries) && entries.every(isEntry))) {
|
|
82
|
-
return new Map(Object.entries(raw));
|
|
83
|
-
}
|
|
84
|
-
log.warn(`${label} unexpected shape in ${path} — starting with an empty context buffer`);
|
|
85
|
-
return new Map();
|
|
86
|
-
};
|
|
87
|
-
const buffers = load();
|
|
88
|
-
const persist = () => saveStateFile(path, Object.fromEntries(buffers));
|
|
89
|
-
return {
|
|
90
|
-
push(placeKey, entry) {
|
|
91
|
-
const previous = buffers.get(placeKey);
|
|
92
|
-
const entries = previous ? [...previous] : [];
|
|
93
|
-
entries.push(entry);
|
|
94
|
-
let total = entries.reduce((sum, candidate) => sum + bufferLine(candidate).length + 1, 0);
|
|
95
|
-
while (entries.length > 1 && total > BUFFER_MAX_CHARS) {
|
|
96
|
-
const dropped = entries.shift();
|
|
97
|
-
if (dropped)
|
|
98
|
-
total -= bufferLine(dropped).length + 1;
|
|
99
|
-
}
|
|
100
|
-
buffers.set(placeKey, entries);
|
|
101
|
-
try {
|
|
102
|
-
persist();
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (previous)
|
|
106
|
-
buffers.set(placeKey, previous);
|
|
107
|
-
else
|
|
108
|
-
buffers.delete(placeKey);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
peek(placeKey) {
|
|
113
|
-
const entries = buffers.get(placeKey) ?? [];
|
|
114
|
-
return { text: entries.map(bufferLine).join("\n"), consumed: [...entries] };
|
|
115
|
-
},
|
|
116
|
-
commit(placeKey, consumed) {
|
|
117
|
-
const entries = buffers.get(placeKey);
|
|
118
|
-
if (!entries)
|
|
119
|
-
return;
|
|
120
|
-
const remaining = entries.filter((entry) => !consumed.includes(entry));
|
|
121
|
-
if (remaining.length === 0)
|
|
122
|
-
buffers.delete(placeKey);
|
|
123
|
-
else
|
|
124
|
-
buffers.set(placeKey, remaining);
|
|
125
|
-
try {
|
|
126
|
-
persist();
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
log.error(`${label} context-buffer write failed post-ACK (a restart may re-fold answered discussion): ${String(error)}`);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
};
|
|
119
|
+
dropRetiredBuckets(path, label);
|
|
120
|
+
return createGenericContextBuffer({
|
|
121
|
+
path,
|
|
122
|
+
label,
|
|
123
|
+
isEntry,
|
|
124
|
+
line: bufferLine,
|
|
125
|
+
});
|
|
133
126
|
}
|
|
@@ -34,6 +34,11 @@ export interface FeishuApiOptions {
|
|
|
34
34
|
appId: string;
|
|
35
35
|
appSecret: string;
|
|
36
36
|
}
|
|
37
|
+
export interface FeishuAppScope {
|
|
38
|
+
name: string;
|
|
39
|
+
grantStatus: number;
|
|
40
|
+
type?: "user" | "tenant";
|
|
41
|
+
}
|
|
37
42
|
/**
|
|
38
43
|
* The Feishu Open API client: one instance per channel, holding the token cache. Every method rides the
|
|
39
44
|
* single pipeline (module header). Throws {@link FeishuApiError} on any failure.
|
|
@@ -85,6 +90,12 @@ export interface FeishuApi {
|
|
|
85
90
|
verificationToken?: string;
|
|
86
91
|
encryptionKey?: string;
|
|
87
92
|
}>;
|
|
93
|
+
/** List the app's scopes and grant state. Used by onboarding/runtime to make group visibility
|
|
94
|
+
* explicit instead of silently assuming unmentioned group events are delivered. */
|
|
95
|
+
listAppScopes(): Promise<FeishuAppScope[]>;
|
|
96
|
+
/** Add tenant scopes to the app draft through application-v7 config. Approval + version publishing
|
|
97
|
+
* remain console actions; this method only removes the error-prone manual draft edit. */
|
|
98
|
+
addAppScopes(appId: string, scopeNames: string[]): Promise<void>;
|
|
88
99
|
/** Update the app's own event subscription (application-v7 config PATCH — tenant token can only
|
|
89
100
|
* operate on itself; the request-URL change takes effect immediately, no version publish). The
|
|
90
101
|
* platform VERIFIES `requestUrl` with a url_verification challenge during this call, so the server
|
|
@@ -100,9 +111,6 @@ export interface FeishuApi {
|
|
|
100
111
|
/** Replace a card entity's content (the settle write; also flips streaming_mode off via the JSON). */
|
|
101
112
|
updateCard(cardId: string, cardJson: string, sequence: number): Promise<void>;
|
|
102
113
|
}
|
|
103
|
-
/** The platform caps a text-message request body at 150 KB; stay well under it (the content is a JSON
|
|
104
|
-
* envelope around the text, and multi-byte characters inflate the byte count). */
|
|
105
|
-
export declare const FEISHU_MAX_TEXT_BYTES: number;
|
|
106
114
|
/** Split text into chunks whose UTF-8 size fits the message cap, preferring a newline boundary. */
|
|
107
115
|
export declare function chunkFeishuText(text: string, maxBytes?: number): string[];
|
|
108
116
|
export declare function createFeishuApi(opts: FeishuApiOptions): FeishuApi;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import { utf8Prefix } from "
|
|
24
|
+
import { utf8Prefix } from "../text.js";
|
|
25
25
|
/** Per-attempt timeout for a JSON API call — small JSON round-trips, so 30s is generous. */
|
|
26
26
|
const API_TIMEOUT_MS = 30_000;
|
|
27
27
|
/** Timeout for downloading resource bytes — sized for a slow link, not a JSON call. */
|
|
@@ -79,7 +79,7 @@ export function isTransientFeishuRegistrationError(e) {
|
|
|
79
79
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
80
80
|
/** The platform caps a text-message request body at 150 KB; stay well under it (the content is a JSON
|
|
81
81
|
* envelope around the text, and multi-byte characters inflate the byte count). */
|
|
82
|
-
|
|
82
|
+
const FEISHU_MAX_TEXT_BYTES = 100 * 1024;
|
|
83
83
|
/** Split text into chunks whose UTF-8 size fits the message cap, preferring a newline boundary. */
|
|
84
84
|
export function chunkFeishuText(text, maxBytes = FEISHU_MAX_TEXT_BYTES) {
|
|
85
85
|
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
@@ -236,7 +236,10 @@ export function createFeishuApi(opts) {
|
|
|
236
236
|
await call("deleteMessage", "DELETE", `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`);
|
|
237
237
|
},
|
|
238
238
|
async getMessage(messageId) {
|
|
239
|
-
const data = await call("getMessage", "GET",
|
|
239
|
+
const data = await call("getMessage", "GET",
|
|
240
|
+
// Pin the id type: callers match mentions/sender against open_ids, so the response's id shape
|
|
241
|
+
// must not depend on the platform's default staying open_id.
|
|
242
|
+
`/open-apis/im/v1/messages/${encodeURIComponent(messageId)}?user_id_type=open_id`);
|
|
240
243
|
return data.data?.items?.[0];
|
|
241
244
|
},
|
|
242
245
|
async downloadResource(messageId, fileKey, type) {
|
|
@@ -296,6 +299,24 @@ export function createFeishuApi(opts) {
|
|
|
296
299
|
encryptionKey: data.data?.app?.encryption?.encryption_key,
|
|
297
300
|
};
|
|
298
301
|
},
|
|
302
|
+
async listAppScopes() {
|
|
303
|
+
const data = await call("listAppScopes", "GET", "/open-apis/application/v6/scopes");
|
|
304
|
+
return (data.data?.scopes ?? []).flatMap((scope) => {
|
|
305
|
+
if (typeof scope.scope_name !== "string" || typeof scope.grant_status !== "number")
|
|
306
|
+
return [];
|
|
307
|
+
const type = scope.scope_type === "user" || scope.scope_type === "tenant" ? scope.scope_type : undefined;
|
|
308
|
+
return [{ name: scope.scope_name, grantStatus: scope.grant_status, type }];
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
async addAppScopes(appId, scopeNames) {
|
|
312
|
+
if (scopeNames.length === 0)
|
|
313
|
+
return;
|
|
314
|
+
await call("addAppScopes", "PATCH", `/open-apis/application/v7/applications/${encodeURIComponent(appId)}/config`, {
|
|
315
|
+
scope: {
|
|
316
|
+
add_scopes: scopeNames.map((scopeName) => ({ scope_name: scopeName, token_type: "tenant" })),
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
},
|
|
299
320
|
async updateEventSubscription(appId, cfg) {
|
|
300
321
|
await call("updateEventSubscription", "PATCH", `/open-apis/application/v7/applications/${encodeURIComponent(appId)}/config`, {
|
|
301
322
|
event: { subscription_type: cfg.subscriptionType, request_url: cfg.requestUrl },
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import type { ChannelModule } from "../../host/node.ts";
|
|
1
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../../host/node.ts";
|
|
2
2
|
import { type FeishuCloudProfile } from "./cloud.ts";
|
|
3
3
|
import { type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, defaultFeishuRoute, feishuEnvelope } from "./parse.ts";
|
|
4
4
|
import { type FeishuFailure } from "./preview.ts";
|
|
5
|
+
import { connectFeishuWs } from "./ws-ingress.ts";
|
|
5
6
|
export { defaultFeishuRoute, feishuEnvelope };
|
|
6
7
|
export type { FeishuFailure, FeishuMessage, FeishuMessageEvent, FeishuRoute };
|
|
7
|
-
|
|
8
|
+
interface FeishuChannelBaseOptions {
|
|
8
9
|
/** App ID (developer console → Credentials & Basic Info). */
|
|
9
10
|
appId: string;
|
|
10
|
-
/** App Secret (same page) — drives
|
|
11
|
+
/** App Secret (same page) — drives both ingress authentication and outbound API calls. */
|
|
11
12
|
appSecret: string;
|
|
12
|
-
/** Verification Token (console → Events & Callbacks) — authenticates PLAINTEXT events. */
|
|
13
|
-
verificationToken: string;
|
|
14
|
-
/** Encrypt Key (same page, optional there — recommended): when set, ordinary events arrive encrypted
|
|
15
|
-
* and signed; this channel then REFUSES plaintext events (fail closed — accepting both would let a
|
|
16
|
-
* forger skip the stronger check). Feishu explicitly excludes the encrypted `url_verification`
|
|
17
|
-
* handshake from event signature verification; that narrow path is authenticated after decryption
|
|
18
|
-
* by the Verification Token. Must match the console exactly. */
|
|
19
|
-
encryptKey?: string;
|
|
20
|
-
/** Direct-message context + delivery policy. `threaded` (default) gives every top-level p2p message
|
|
21
|
-
* its own session, creates a platform thread for the answer, and routes later thread messages back
|
|
22
|
-
* by root message id. `continuous` keeps one session per p2p chat and sends ordinary unquoted replies. */
|
|
23
|
-
directMessageSession?: "continuous" | "threaded";
|
|
24
|
-
/** Group-message context + delivery policy. `threaded` (default) gives every top-level summoned
|
|
25
|
-
* message its own session and platform thread; later bare user messages in that managed thread answer
|
|
26
|
-
* in the same root session, while @other-only discussion buffers. `continuous` preserves the legacy
|
|
27
|
-
* chat/topic sessions (`chat_id` / `chat_id:thread_id`). Buffering and bare continuations require
|
|
28
|
-
* `im:message.group_msg`. */
|
|
29
|
-
groupMessageSession?: "continuous" | "threaded";
|
|
30
13
|
/** Policy: whether/where to answer an event (return null to ignore). Defaults to {@link defaultFeishuRoute}. */
|
|
31
14
|
route?: (event: FeishuMessageEvent) => FeishuRoute | null;
|
|
32
15
|
/** Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator
|
|
33
16
|
* log). Return a string to send it, or undefined/"" to stay silent. Default: a neutral message keyed
|
|
34
17
|
* on `retryable`. A developer's own bot can surface the raw details, e.g. `(f) => `⚠️ ${f.details}``. */
|
|
35
18
|
onError?: (failed: FeishuFailure) => string | undefined;
|
|
36
|
-
/** API origin override (tests / self-hosted gateways).
|
|
37
|
-
* `
|
|
38
|
-
|
|
19
|
+
/** API origin override (tests / self-hosted gateways). Feishu factories default to
|
|
20
|
+
* `https://open.feishu.cn`; Lark factories default to `https://open.larksuite.com`. Named to match
|
|
21
|
+
* the other channels (telegram/slack). */
|
|
22
|
+
apiBaseUrl?: string;
|
|
39
23
|
/** How long (ms) a turn waits before its reply-quoted "⏳ Queued" card mounts. Defaults to 0
|
|
40
24
|
* (immediate); the same card is later taken over by the live preview/final answer. */
|
|
41
25
|
queueNoticeDelayMs?: number;
|
|
42
26
|
}
|
|
43
|
-
|
|
27
|
+
export interface FeishuChannelOptions extends FeishuChannelBaseOptions {
|
|
28
|
+
/** Verification Token for Request-URL authentication. */
|
|
29
|
+
verificationToken: string;
|
|
30
|
+
/** Optional webhook Encrypt Key. When set, plaintext events are rejected. */
|
|
31
|
+
encryptKey?: string;
|
|
32
|
+
}
|
|
33
|
+
export type FeishuWebSocketChannelOptions = FeishuChannelBaseOptions & {
|
|
34
|
+
verificationToken?: never;
|
|
35
|
+
encryptKey?: never;
|
|
36
|
+
};
|
|
37
|
+
/** Build the canonical Feishu Request-URL webhook channel. */
|
|
44
38
|
export declare function feishuChannel(opts: FeishuChannelOptions): ChannelModule;
|
|
45
|
-
/**
|
|
46
|
-
export declare function
|
|
39
|
+
/** Build the canonical Feishu WebSocket long-connection channel. */
|
|
40
|
+
export declare function feishuWebSocketChannel(opts: FeishuWebSocketChannelOptions): LongConnectionChannelModule;
|
|
41
|
+
/** Internal compatibility seams: protocol behavior comes from Feishu; the profile binds cloud edges. */
|
|
42
|
+
interface FeishuWebSocketChannelDeps {
|
|
43
|
+
connectWs?: typeof connectFeishuWs;
|
|
44
|
+
}
|
|
45
|
+
export declare function buildFeishuChannel(profile: FeishuCloudProfile, opts: FeishuChannelOptions, factoryName: string): ChannelModule;
|
|
46
|
+
export declare function buildFeishuWebSocketChannel(profile: FeishuCloudProfile, opts: FeishuWebSocketChannelOptions, factoryName: string, deps?: FeishuWebSocketChannelDeps): LongConnectionChannelModule;
|