@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.
Files changed (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. 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 "./text.js";
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
- /** Main-chat discussion stays in the chat bucket; a topic uses its stable root id (thread id fallback). */
20
- export declare function feishuBufferPlaceKey(conversation: Pick<NormalizedFeishuMessage["conversation"], "chatId" | "rootId" | "threadId">): string;
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 interface FeishuContextBuffer {
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
- * Durable context buffer for human group messages that do NOT currently summon the Feishu/Lark Agent.
3
- * Entries are bucketed by conversation place (main chat, or one concrete thread root) and folded into
4
- * the next answered turn in that place. The consume protocol mirrors Telegram: peek without clearing,
5
- * then commit exactly that snapshot only after the Agent emits `completed`.
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
- const BUFFER_MAX_CHARS = 4000;
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
- /** Main-chat discussion stays in the chat bucket; a topic uses its stable root id (thread id fallback). */
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
- const topic = conversation.rootId ?? conversation.threadId;
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().slice(0, 280);
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
- const load = () => {
75
- const raw = loadStateFile(path);
76
- if (raw === undefined)
77
- return new Map();
78
- if (typeof raw === "object" &&
79
- raw !== null &&
80
- !Array.isArray(raw) &&
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 "./text.js";
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
- export const FEISHU_MAX_TEXT_BYTES = 100 * 1024;
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", `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`);
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
- export interface FeishuChannelOptions {
8
+ interface FeishuChannelBaseOptions {
8
9
  /** App ID (developer console → Credentials & Basic Info). */
9
10
  appId: string;
10
- /** App Secret (same page) — drives the tenant_access_token the replies ride on. */
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). The kind fixes the default
37
- * `feishuChannel` → `https://open.feishu.cn`, `larkChannel` `https://open.larksuite.com`. */
38
- baseUrl?: string;
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
- /** Build the canonical Feishu channel. Lark calls the internal profile-bound builder below. */
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
- /** Internal compatibility seam: protocol behavior comes from Feishu; the profile binds cloud edges. */
46
- export declare function buildFeishuChannel(profile: FeishuCloudProfile, { appId, appSecret, verificationToken, encryptKey, directMessageSession, groupMessageSession, route, onError, baseUrl, queueNoticeDelayMs, }: FeishuChannelOptions, factoryName: string): ChannelModule;
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;