@fastagent-sh/fastagent 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +7 -7
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +8 -5
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -8
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/dev.js +25 -13
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -4
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +63 -94
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +9 -3
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -43
- package/dist/engines/pi/create.js +141 -71
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +84 -88
- package/dist/engines/pi/session-control.d.ts +10 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +10 -0
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type ModuleLoadFailure } from "
|
|
1
|
+
import type { ChannelContext, LongConnection, Routes } from "../channel.ts";
|
|
2
|
+
import { type ModuleLoadFailure } from "../loader.ts";
|
|
3
3
|
/** A dropped route: two channels claim the same key. Surfaced, never silent. */
|
|
4
4
|
export interface ChannelCollision {
|
|
5
5
|
route: string;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Channel discovery (the N axis, filesystem form). A channel file default-exports either the existing
|
|
3
3
|
* route factory `(ctx) => Routes`, or an explicit long-connection module `{ name, connect(ctx, signal) }`.
|
|
4
|
+
*
|
|
5
|
+
* Engine-neutral, and living here rather than under `engines/` because of it: reading `channels/*.ts`
|
|
6
|
+
* is the Channel contract plus a directory, with no engine in sight.
|
|
4
7
|
*/
|
|
5
8
|
import { readdir } from "node:fs/promises";
|
|
6
9
|
import { isAbsolute, join } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import { isModuleFile, loadModuleDir } from "
|
|
9
|
-
import { assertInsideAgentDir } from "
|
|
10
|
+
import { assertRouteKey, routeKeysConflict } from "./serve.js";
|
|
11
|
+
import { isModuleFile, loadModuleDir } from "../loader.js";
|
|
12
|
+
import { assertInsideAgentDir } from "../paths.js";
|
|
10
13
|
function longConnectionModule(value) {
|
|
11
14
|
return value !== null && typeof value === "object" && typeof value.connect === "function";
|
|
12
15
|
}
|
|
@@ -79,9 +82,7 @@ function validateRoutes(value, label) {
|
|
|
79
82
|
if (typeof handler !== "function") {
|
|
80
83
|
throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
|
|
84
|
-
}
|
|
85
|
+
assertRouteKey(route, (problem) => `${label}: route "${route}" is not a valid route key — ${problem}`);
|
|
85
86
|
}
|
|
86
87
|
return routes;
|
|
87
88
|
}
|
|
@@ -121,12 +122,7 @@ export async function loadChannels(dir, ctx) {
|
|
|
121
122
|
}
|
|
122
123
|
const declaredRoutes = validateRoutes(declared, label);
|
|
123
124
|
for (const [route, handler] of declaredRoutes) {
|
|
124
|
-
const
|
|
125
|
-
const clash = Object.keys(routes).some((key) => {
|
|
126
|
-
const existing = parseRouteKey(key);
|
|
127
|
-
return (existing.path === parsed.path &&
|
|
128
|
-
(existing.method === undefined || parsed.method === undefined || existing.method === parsed.method));
|
|
129
|
-
});
|
|
125
|
+
const clash = Object.keys(routes).some((key) => routeKeysConflict(key, route));
|
|
130
126
|
if (clash) {
|
|
131
127
|
collisions.push({ route, source: label });
|
|
132
128
|
continue;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Budget: a card entity is capped at 30 KB, so the final answer's card chunk (and the live view) stay
|
|
18
18
|
* well under it; longer answers overflow into follow-up messages (preview.ts owns that policy).
|
|
19
19
|
*/
|
|
20
|
-
import { truncateCodePointPrefix } from "../text.js";
|
|
20
|
+
import { truncateCodePointPrefix } from "../kit/text.js";
|
|
21
21
|
/** The append-only answer element's id — shared by create (card.ts) and update (preview.ts). */
|
|
22
22
|
export const ANSWER_ELEMENT_ID = "answer";
|
|
23
23
|
/** The volatile process element's id (thinking tail + tool lines + retry notice; live-only). */
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
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
|
|
3
|
+
* ../kit/context-buffer.ts): the entry shape, its fold-line rendering, place-key derivation, and
|
|
4
4
|
* buffered-resource selection. Entries are bucketed by conversation place (main chat, or one
|
|
5
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";
|
|
9
|
-
import { loadStateFile, saveStateFile } from "../state.js";
|
|
10
|
-
import { truncateCodePointPrefix } from "../text.js";
|
|
8
|
+
import { BUFFER_ATTACH_MAX, BUFFER_LINE_MAX_CHARS, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
|
|
9
|
+
import { loadStateFile, saveStateFile } from "../kit/state.js";
|
|
10
|
+
import { truncateCodePointPrefix } from "../kit/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)
|
|
@@ -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 "../kit/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. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelModule, LongConnectionChannelModule } from "../../
|
|
1
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../../channel.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";
|
|
@@ -12,19 +12,19 @@ import { isAbsolute, join } from "node:path";
|
|
|
12
12
|
import { log } from "../../log.js";
|
|
13
13
|
import { readBodyCapped } from "../body.js";
|
|
14
14
|
import { text } from "../respond.js";
|
|
15
|
-
import { createSeenRing } from "../seen.js";
|
|
16
|
-
import { createTaskTracker } from "../tasks.js";
|
|
17
|
-
import { ensureStateHome, loadStateFile, saveStateFile } from "../state.js";
|
|
18
|
-
import { dispatchStop, isStopText } from "../stop-command.js";
|
|
19
|
-
import { createTurnQueue } from "../turn-queue.js";
|
|
20
|
-
import { createTurnStore } from "../turn-store.js";
|
|
15
|
+
import { createSeenRing } from "../kit/seen.js";
|
|
16
|
+
import { createTaskTracker } from "../kit/tasks.js";
|
|
17
|
+
import { ensureStateHome, loadStateFile, saveStateFile } from "../kit/state.js";
|
|
18
|
+
import { dispatchStop, isStopText } from "../kit/stop-command.js";
|
|
19
|
+
import { createTurnQueue } from "../kit/turn-queue.js";
|
|
20
|
+
import { createTurnStore } from "../kit/turn-store.js";
|
|
21
21
|
import { FEISHU_CLOUD } from "./cloud.js";
|
|
22
22
|
import { collectFeishuBufferedAttachments, createFeishuContextBuffer, feishuBufferPlaceKey, feishuBufferText, } from "./context-buffer.js";
|
|
23
23
|
import { decryptEvent, timingSafeEqualStr, verifySignature } from "./crypto.js";
|
|
24
24
|
import { invokeFeishuTurn } from "./invoke-turn.js";
|
|
25
25
|
import { createFeishuApi } from "./feishu-api.js";
|
|
26
26
|
import { normalizeFeishuMessage } from "./normalize.js";
|
|
27
|
-
import { createThreadParticipants } from "../thread-participants.js";
|
|
27
|
+
import { createThreadParticipants } from "../kit/thread-participants.js";
|
|
28
28
|
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_MESSAGE_READ_REQUEST, FEISHU_MESSAGE_READ_SCOPE, scopeSatisfied, } from "./setup-mode.js";
|
|
29
29
|
import { cloudEnvelope, defaultFeishuRoute, feishuEnvelope, placeKey, senderId, senderLabel, } from "./parse.js";
|
|
30
30
|
import { defaultErrorMessage, mountFeishuPreview, settleFeishuPreview, streamFeishuReply, } from "./preview.js";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* or hide its still-readable siblings.
|
|
14
14
|
*/
|
|
15
15
|
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
16
|
-
import { type BusyRetry } from "../invoke-turn-kit.ts";
|
|
16
|
+
import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
|
|
17
17
|
import type { FeishuBufferedRef } from "./context-buffer.ts";
|
|
18
18
|
import type { FeishuApi } from "./feishu-api.ts";
|
|
19
19
|
/** Everything the transport needs to fetch a turn's attachments. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { log } from "../../log.js";
|
|
2
|
-
import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../invoke-turn-kit.js";
|
|
3
|
-
import { BUFFER_ATTACH_MAX } from "../context-buffer.js";
|
|
2
|
+
import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../kit/invoke-turn-kit.js";
|
|
3
|
+
import { BUFFER_ATTACH_MAX } from "../kit/context-buffer.js";
|
|
4
4
|
import { parseContent } from "./parse.js";
|
|
5
|
-
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../text.js";
|
|
5
|
+
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../kit/text.js";
|
|
6
6
|
/** The per-turn REPLY CONTRACT, appended to the prompt (not the system prompt). Two halves, one
|
|
7
7
|
* concept — what happens to the reply: its FORMAT (rendered in a card whose markdown element is the
|
|
8
8
|
* natural fit for LLM output — steer away from HTML/plain) and its DELIVERY OWNERSHIP (the channel
|
|
@@ -169,7 +169,6 @@ async function resolveTurnInputs(t, attachments) {
|
|
|
169
169
|
referentBlock += chain.block;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
// Primary first and fail-fast: these are resources the current user explicitly pointed at.
|
|
173
172
|
const imageRefs = [];
|
|
174
173
|
for (const ref of images)
|
|
175
174
|
imageRefs.push(await t.api.fetchImage(ref.msg, ref.key));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentEvent } from "../../agent.ts";
|
|
2
2
|
import { type FeishuApi, type FeishuTarget } from "./feishu-api.ts";
|
|
3
|
-
import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
|
|
3
|
+
import { type ChannelFailure, defaultErrorMessage } from "../kit/preview-kit.ts";
|
|
4
4
|
/** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
|
|
5
5
|
export type FeishuFailure = ChannelFailure;
|
|
6
6
|
export { defaultErrorMessage };
|
|
@@ -25,8 +25,8 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
25
25
|
import { log } from "../../log.js";
|
|
26
26
|
import { ANSWER_ELEMENT_ID, CARD_MARKDOWN_MAX_BYTES, PROCESS_ELEMENT_ID, cardEntityContent, finalCardJson, streamingCardJson, } from "./card.js";
|
|
27
27
|
import { chunkFeishuText, isCardStreamingClosed } from "./feishu-api.js";
|
|
28
|
-
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../preview-kit.js";
|
|
29
|
-
import { truncateCodePointPrefix, truncateUtf8 } from "../text.js";
|
|
28
|
+
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
|
|
29
|
+
import { truncateCodePointPrefix, truncateUtf8 } from "../kit/text.js";
|
|
30
30
|
export { defaultErrorMessage };
|
|
31
31
|
/** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. Cardkit
|
|
32
32
|
* allows 10 QPS per card entity (50 per app), but one snapshot a second reads smoothly (the client
|
|
@@ -286,7 +286,11 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
286
286
|
// we throw below regardless.
|
|
287
287
|
finalized = true;
|
|
288
288
|
{
|
|
289
|
-
const msg = formatError({
|
|
289
|
+
const msg = formatError({
|
|
290
|
+
details: e.details,
|
|
291
|
+
retryable: e.retryable,
|
|
292
|
+
...(e.code !== undefined ? { code: e.code } : {}),
|
|
293
|
+
}) ?? "";
|
|
290
294
|
try {
|
|
291
295
|
await settle(msg);
|
|
292
296
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Schema } from "@octokit/webhooks-types";
|
|
2
|
-
import type { ChannelModule } from "../../
|
|
2
|
+
import type { ChannelModule } from "../../channel.ts";
|
|
3
3
|
/** A verified GitHub webhook event. Header fields plus the official typed payload. */
|
|
4
4
|
export interface GithubEvent {
|
|
5
5
|
/** `X-GitHub-Event` (e.g. "pull_request", "issue_comment"). */
|
package/dist/channels/http.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* HTTP/SSE channel: fan one invoke stream out to Server-Sent Events.
|
|
3
|
+
*
|
|
4
|
+
* The handler is Fetch-shaped (`(Request) => Promise<Response>`) — the cross-runtime form every
|
|
5
|
+
* embedding host speaks, so it mounts inside an existing app's own route. It is path-agnostic. The
|
|
6
|
+
* web stream primitives give cancellation (consumer disconnect → cancel() → iterator.return() →
|
|
7
|
+
* invoke cancellation, SPEC MUST 3), backpressure (pull-based), and the body cap natively.
|
|
8
|
+
*
|
|
9
|
+
* Serving it is somebody else's job: the node:http bridge and the route table belong to the HOST
|
|
10
|
+
* (`./serve.ts`). This file knows only the Agent contract and the wire shape of one stream.
|
|
11
|
+
*/
|
|
2
12
|
import type { Agent } from "../agent.ts";
|
|
3
13
|
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
4
14
|
* dispatch imports it), so the two caps cannot drift apart. */
|
|
@@ -19,9 +29,3 @@ export declare const INVOKE_EXAMPLE_BODY = "{\"session\":\"dev\",\"text\":\"hell
|
|
|
19
29
|
* Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
|
|
20
30
|
*/
|
|
21
31
|
export declare function createInvokeHandler(agent: Agent): (req: Request) => Promise<Response>;
|
|
22
|
-
/**
|
|
23
|
-
* node:http adapter for a Fetch handler. Bridges IncomingMessage → Request and pumps the
|
|
24
|
-
* Response body back to ServerResponse with backpressure; a client disconnect (`res` close)
|
|
25
|
-
* cancels both the request signal and the response stream (→ invoke cancellation).
|
|
26
|
-
*/
|
|
27
|
-
export declare function nodeListener(handler: (req: Request) => Promise<Response>): (req: IncomingMessage, res: ServerResponse) => void;
|
package/dist/channels/http.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP/SSE channel: fan one invoke stream out to Server-Sent Events.
|
|
3
|
-
*
|
|
4
|
-
* The handler is Fetch-shaped (`(Request) => Promise<Response>`) — the cross-runtime form every
|
|
5
|
-
* embedding host speaks, so it mounts inside an existing app's own route. It is path-agnostic. The
|
|
6
|
-
* web stream primitives give cancellation (consumer disconnect → cancel() → iterator.return() →
|
|
7
|
-
* invoke cancellation, SPEC MUST 3), backpressure (pull-based), and the body cap natively.
|
|
8
|
-
*
|
|
9
|
-
* `nodeListener` is the thin node:http adapter for the embedded `fastagent dev/start` server.
|
|
10
|
-
*/
|
|
11
|
-
import { Readable } from "node:stream";
|
|
12
|
-
import { log } from "../log.js";
|
|
13
1
|
import { readBodyCapped } from "./body.js";
|
|
14
|
-
import { text
|
|
2
|
+
import { text } from "./respond.js";
|
|
15
3
|
/** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
|
|
16
4
|
* dispatch imports it), so the two caps cannot drift apart. */
|
|
17
5
|
export const MAX_BODY_BYTES = 1 << 20;
|
|
@@ -109,99 +97,3 @@ export function createInvokeHandler(agent) {
|
|
|
109
97
|
});
|
|
110
98
|
};
|
|
111
99
|
}
|
|
112
|
-
/**
|
|
113
|
-
* node:http adapter for a Fetch handler. Bridges IncomingMessage → Request and pumps the
|
|
114
|
-
* Response body back to ServerResponse with backpressure; a client disconnect (`res` close)
|
|
115
|
-
* cancels both the request signal and the response stream (→ invoke cancellation).
|
|
116
|
-
*/
|
|
117
|
-
export function nodeListener(handler) {
|
|
118
|
-
return (req, res) => {
|
|
119
|
-
void pump(handler, req, res); // safe: pump is TOTAL (never rejects) — see its contract below
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Consume ONE request and drive its response to a terminal state. pump is TOTAL: a SINGLE try/catch wraps
|
|
124
|
-
* the whole request→response→stream path, so EVERY failure — a handler throw, a non-Response return
|
|
125
|
-
* (`response.headers` undefined), a header Node rejects, `getReader`, or a body stream that errors
|
|
126
|
-
* mid-flight — ends the response and the returned promise NEVER rejects, which is what lets the
|
|
127
|
-
* `void pump(...)` above be safe. Before any byte goes out (headers not sent) it is a clean 500; once the
|
|
128
|
-
* response is streaming, the only honest signal left is to destroy the socket (truncated stream, not a
|
|
129
|
-
* hang). The process installs no `unhandledRejection` handler by design: robustness against a background
|
|
130
|
-
* throw is each fire-and-forget's OWN contract (fail into a terminal HTTP response here), not a global net
|
|
131
|
-
* that would blanket-swallow.
|
|
132
|
-
*/
|
|
133
|
-
async function pump(handler, req, res) {
|
|
134
|
-
const controller = new AbortController();
|
|
135
|
-
res.on("close", () => controller.abort());
|
|
136
|
-
try {
|
|
137
|
-
const method = req.method ?? "GET";
|
|
138
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
139
|
-
const headers = new Headers();
|
|
140
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
141
|
-
if (Array.isArray(v))
|
|
142
|
-
for (const vv of v)
|
|
143
|
-
headers.append(k, vv);
|
|
144
|
-
else if (v != null)
|
|
145
|
-
headers.set(k, v);
|
|
146
|
-
}
|
|
147
|
-
const request = new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
148
|
-
method,
|
|
149
|
-
headers,
|
|
150
|
-
body: hasBody ? Readable.toWeb(req) : undefined,
|
|
151
|
-
duplex: "half",
|
|
152
|
-
signal: controller.signal,
|
|
153
|
-
});
|
|
154
|
-
const response = await handler(request);
|
|
155
|
-
const outHeaders = {};
|
|
156
|
-
response.headers.forEach((value, key) => {
|
|
157
|
-
outHeaders[key] = value;
|
|
158
|
-
});
|
|
159
|
-
res.writeHead(response.status, outHeaders);
|
|
160
|
-
if (!response.body) {
|
|
161
|
-
res.end();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const reader = response.body.getReader();
|
|
165
|
-
res.on("close", () => void reader.cancel());
|
|
166
|
-
for (;;) {
|
|
167
|
-
const { done, value } = await reader.read();
|
|
168
|
-
if (done || res.destroyed)
|
|
169
|
-
break;
|
|
170
|
-
// Backpressure: wait for drain, but ALSO resolve on close. A client disconnect after write()
|
|
171
|
-
// returned false never emits 'drain' on the closed socket, so waiting on 'drain' alone would
|
|
172
|
-
// suspend pump() forever (leaking the request/stream).
|
|
173
|
-
if (!res.write(value)) {
|
|
174
|
-
await new Promise((resolve) => {
|
|
175
|
-
const settle = () => {
|
|
176
|
-
res.off("drain", settle);
|
|
177
|
-
res.off("close", settle);
|
|
178
|
-
resolve();
|
|
179
|
-
};
|
|
180
|
-
res.once("drain", settle);
|
|
181
|
-
res.once("close", settle);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!res.destroyed)
|
|
186
|
-
res.end(); // normal completion
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
// The ONE totality boundary: every failure above lands here, so pump never rejects (see the header
|
|
190
|
-
// doc) — which REQUIRES the catch itself not to throw. Don't leak the internal message to the client.
|
|
191
|
-
log.error(`[host] request failed: ${String(error)}`);
|
|
192
|
-
// Never touch an already-terminal res: a client that disconnects during the handler await destroys res
|
|
193
|
-
// (headers not yet sent), and writeHead/end on a dead socket can throw ERR_STREAM_DESTROYED here — which
|
|
194
|
-
// WOULD be the unhandled rejection this boundary exists to kill. One named gate states the invariant;
|
|
195
|
-
// with it the catch is provably non-throwing (writeHead only when !headersSent && !destroyed, destroy is
|
|
196
|
-
// idempotent).
|
|
197
|
-
if (res.destroyed)
|
|
198
|
-
return;
|
|
199
|
-
if (res.headersSent) {
|
|
200
|
-
res.destroy(error instanceof Error ? error : undefined); // streaming → truncate (not a hang)
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
res.writeHead(500, textHeaders); // pre-header → a clean 500
|
|
204
|
-
res.end("internal error\n");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* un-summoned messages per conversation "place", kept under a char budget and folded into the next
|
|
5
5
|
* answered turn in that place, so a summoned agent has the discussion it didn't see turn-by-turn.
|
|
6
6
|
*
|
|
7
|
-
* Channel-neutral and generic over the entry shape (like ../turn-store.ts): the channel supplies its
|
|
7
|
+
* Channel-neutral and generic over the entry shape (like ../kit/turn-store.ts): the channel supplies its
|
|
8
8
|
* entry type, the shape validator (state files are an IO boundary — valid JSON of the WRONG shape
|
|
9
9
|
* must degrade exactly like a corrupt file: warn + empty, never flow in as trusted data), the
|
|
10
10
|
* fold-line renderer, and its log label. What stays per channel: the entry type itself, place-key
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* message that arrives while the turn runs survives for the next answered turn (a whole-bucket
|
|
22
22
|
* delete would lose it).
|
|
23
23
|
*/
|
|
24
|
-
import { log } from "
|
|
24
|
+
import { log } from "../../log.js";
|
|
25
25
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
26
26
|
/** Char budget for the per-place buffer — bounds the cost of folding it into a prompt; when exceeded
|
|
27
27
|
* the OLDEST un-summoned messages are dropped (not a time window: a quiet group keeps its
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* - the prompt-suffix wording: {@link attachedFilesManifest}, {@link backgroundImagesManifest},
|
|
8
8
|
* {@link missingAttachmentsNote}, {@link attributedFileName}.
|
|
9
9
|
*
|
|
10
|
+
* None of it asks what the agent can DO with an attachment. A channel resolves platform resources and
|
|
11
|
+
* states what it found; deciding whether to open a file is the agent's, and one assembled without a
|
|
12
|
+
* file tool answers that it cannot — visibly, at the moment it is asked.
|
|
13
|
+
*
|
|
10
14
|
* Attachment RESOLUTION stays per channel — the platform resource models (Bot API file_ids,
|
|
11
15
|
* message-scoped Feishu keys, Slack file objects) are real differences.
|
|
12
16
|
*/
|
|
13
|
-
import { type Agent, type AgentEvent, type Prompt, type Scope } from "
|
|
17
|
+
import { type Agent, type AgentEvent, type Prompt, type Scope } from "../../agent.ts";
|
|
14
18
|
/** How the busy-wait paces: retry the invoke every `delayMs` while the session's lease is held by an
|
|
15
19
|
* EXTERNAL turn (a self-scheduled wake, a concurrent embedder invoke), up to `maxWaitMs` total. The
|
|
16
20
|
* channel's own turns never collide (the turn-queue serializes per session), so a busy reject here is
|
|
@@ -49,8 +53,15 @@ export interface ManifestFile {
|
|
|
49
53
|
size: number;
|
|
50
54
|
path: string;
|
|
51
55
|
}
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
56
|
+
/**
|
|
57
|
+
* The downloaded-file manifest appended to the prompt: name, size, path. Empty input renders nothing.
|
|
58
|
+
*
|
|
59
|
+
* It STATES, it does not instruct. The earlier wording ("read them with your tools") was an
|
|
60
|
+
* assumption about the reader, and an assumption has to be verified — which is where a capability
|
|
61
|
+
* flag threaded through eight files came from. An agent with a file tool decides for itself whether
|
|
62
|
+
* to open one, and how much of it; an agent without one says so. Neither needs this line to have
|
|
63
|
+
* guessed first.
|
|
64
|
+
*/
|
|
54
65
|
export declare function attachedFilesManifest(files: readonly ManifestFile[]): string;
|
|
55
66
|
/** Decorate a background file's display name with its attribution ("the file Bob sent" resolves),
|
|
56
67
|
* the way the fold attributes text lines. */
|
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
* - the prompt-suffix wording: {@link attachedFilesManifest}, {@link backgroundImagesManifest},
|
|
8
8
|
* {@link missingAttachmentsNote}, {@link attributedFileName}.
|
|
9
9
|
*
|
|
10
|
+
* None of it asks what the agent can DO with an attachment. A channel resolves platform resources and
|
|
11
|
+
* states what it found; deciding whether to open a file is the agent's, and one assembled without a
|
|
12
|
+
* file tool answers that it cannot — visibly, at the moment it is asked.
|
|
13
|
+
*
|
|
10
14
|
* Attachment RESOLUTION stays per channel — the platform resource models (Bot API file_ids,
|
|
11
15
|
* message-scoped Feishu keys, Slack file objects) are real differences.
|
|
12
16
|
*/
|
|
13
|
-
import { SESSION_BUSY_CODE } from "
|
|
14
|
-
import { log } from "
|
|
15
|
-
// Each retry is a lease-check-level reject (tryAcquire runs before
|
|
17
|
+
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
18
|
+
import { log } from "../../log.js";
|
|
19
|
+
// Each retry is a lease-check-level reject (tryAcquire runs before the session is bound) — waiting is nearly
|
|
16
20
|
// free, and the loop exits within one delay of the holder finishing. So the cap is sized to outlast a
|
|
17
21
|
// real tool-using wake turn (minutes), not to be short: 10 min. CEILING: a holder that runs longer than
|
|
18
22
|
// this still surfaces the busy error to the user — the bound exists so a stuck lease can't hang a chat
|
|
@@ -59,11 +63,18 @@ scope, prompt, options) {
|
|
|
59
63
|
await new Promise((r) => setTimeout(r, busyRetry.delayMs));
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
66
|
+
/**
|
|
67
|
+
* The downloaded-file manifest appended to the prompt: name, size, path. Empty input renders nothing.
|
|
68
|
+
*
|
|
69
|
+
* It STATES, it does not instruct. The earlier wording ("read them with your tools") was an
|
|
70
|
+
* assumption about the reader, and an assumption has to be verified — which is where a capability
|
|
71
|
+
* flag threaded through eight files came from. An agent with a file tool decides for itself whether
|
|
72
|
+
* to open one, and how much of it; an agent without one says so. Neither needs this line to have
|
|
73
|
+
* guessed first.
|
|
74
|
+
*/
|
|
64
75
|
export function attachedFilesManifest(files) {
|
|
65
76
|
return files.length
|
|
66
|
-
? `\n\n[attached files
|
|
77
|
+
? `\n\n[attached files:\n${files.map((f) => `- ${f.name} (${f.size} bytes) → ${f.path}`).join("\n")}\n]`
|
|
67
78
|
: "";
|
|
68
79
|
}
|
|
69
80
|
/** Decorate a background file's display name with its attribution ("the file Bob sent" resolves),
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* everything platform-independent lives here, so a new event type or a wording change lands in ONE
|
|
9
9
|
* place instead of one hunk per channel.
|
|
10
10
|
*/
|
|
11
|
-
import type { AgentEvent, Json } from "
|
|
11
|
+
import type { AgentEvent, Json } from "../../agent.ts";
|
|
12
12
|
/** A terminal failure, as a channel hands it to its `onError`. */
|
|
13
13
|
export interface ChannelFailure {
|
|
14
14
|
details: string;
|
|
15
15
|
retryable: boolean;
|
|
16
|
+
/** The engine's failure code, when it set one. */
|
|
17
|
+
code?: string;
|
|
16
18
|
}
|
|
17
19
|
/** The customer-facing default: neutral, no leaked internals. Differentiate on retryability and always
|
|
18
20
|
* offer a next step (Slack agent-design: never leave the user with a dead-end "something went wrong").
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Best-effort bounded durable dedup ring, recorded only after the caller's pre-ACK side effect is durable. */
|
|
2
|
-
import { log } from "
|
|
2
|
+
import { log } from "../../log.js";
|
|
3
3
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
4
4
|
export function createSeenRing(path, label, cap = 2000) {
|
|
5
5
|
const raw = loadStateFile(path);
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* is an ENVIRONMENT error the operator must fix: it throws, and construction fails loudly — booting
|
|
13
13
|
* with silently-empty state would hide real data behind a config mistake.
|
|
14
14
|
*/
|
|
15
|
-
import { mkdirSync, readFileSync
|
|
16
|
-
import {
|
|
17
|
-
import { log } from "
|
|
15
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { writeFileAtomic } from "../../atomic-write.js";
|
|
17
|
+
import { log } from "../../log.js";
|
|
18
18
|
/** Create the channel's state home — the one shared spelling of it, so no channel invents its own. */
|
|
19
19
|
export function ensureStateHome(dir) {
|
|
20
20
|
mkdirSync(dir, { recursive: true });
|
|
@@ -43,8 +43,5 @@ export function loadStateFile(path) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
export function saveStateFile(path, value) {
|
|
46
|
-
|
|
47
|
-
const tmp = `${path}.tmp`;
|
|
48
|
-
writeFileSync(tmp, JSON.stringify(value));
|
|
49
|
-
renameSync(tmp, path);
|
|
46
|
+
writeFileAtomic(path, JSON.stringify(value));
|
|
50
47
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SessionControl } from "
|
|
1
|
+
import { type SessionControl } from "../../session.ts";
|
|
2
2
|
/** Bare stop word for summon-body matching (Slack/Feishu); Telegram uses its native `/stop` command. */
|
|
3
3
|
export declare function isStopText(text: string): boolean;
|
|
4
4
|
/** Dispatch `abort` for the session and map the outcome to the customer-facing line. Never throws;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* at-least-once floor; and the hub stays gated by `config.sessionControl`, so without it the command
|
|
7
7
|
* degrades to a visible "not enabled" notice, never a silent ignore.
|
|
8
8
|
*/
|
|
9
|
-
import { log } from "
|
|
10
|
-
import { NO_ACTIVE_RUN_CODE } from "
|
|
9
|
+
import { log } from "../../log.js";
|
|
10
|
+
import { NO_ACTIVE_RUN_CODE } from "../../session.js";
|
|
11
11
|
/** Bare stop word for summon-body matching (Slack/Feishu); Telegram uses its native `/stop` command. */
|
|
12
12
|
export function isStopText(text) {
|
|
13
13
|
return /^(stop|cancel)[.!]?$/i.test(text.trim());
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`turnsIdle`) — otherwise a reply in flight when the process exits is silently dropped. Error
|
|
5
5
|
* handling stays with the caller: track() only guarantees the drain sees the task settle.
|
|
6
6
|
*/
|
|
7
|
-
import { beginWork } from "
|
|
7
|
+
import { beginWork } from "../busy.js";
|
|
8
8
|
export function createTaskTracker() {
|
|
9
9
|
const tasks = new Set();
|
|
10
10
|
return {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - Keyed by `thread_id`, never a reply-chain root: Feishu's `root_id` moves with the chain, so it
|
|
20
20
|
* cannot identify a side conversation at all.
|
|
21
21
|
*/
|
|
22
|
-
import { log } from "
|
|
22
|
+
import { log } from "../../log.js";
|
|
23
23
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
24
24
|
/** Cap on remembered threads. Losing one costs a mention to re-enter that thread, so an unbounded file
|
|
25
25
|
* buys little — and a merge that carries new information rewrites the whole map synchronously, so the
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* process-crash recovery, at-least-once). Exactly-once / deterministic step-replay (L2) is the K-axis
|
|
11
11
|
* backend — an external queue with distributed locking (SPEC §11) — not this in-memory queue.
|
|
12
12
|
*/
|
|
13
|
-
import { log } from "
|
|
14
|
-
import { beginWork } from "
|
|
13
|
+
import { log } from "../../log.js";
|
|
14
|
+
import { beginWork } from "../busy.js";
|
|
15
15
|
export function createTurnQueue(opts) {
|
|
16
16
|
const { label, run, onQueuedBehind } = opts;
|
|
17
17
|
// Per-session serial chains: a second turn for the same session waits its turn (FIFO) instead of
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* Single-process, single-writer: same durability model as state.ts (crash-safe via atomic rename;
|
|
50
50
|
* power-loss is best-effort — no fsync, consistent with the rest of the channel's state).
|
|
51
51
|
*/
|
|
52
|
-
import { log } from "
|
|
52
|
+
import { log } from "../../log.js";
|
|
53
53
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
54
54
|
export function createTurnStore(path, opts) {
|
|
55
55
|
const { label, isRecord, order } = opts;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* this module binds it to Lark's cloud profile and exposes natural Lark-branded public names. Lark's
|
|
4
4
|
* weaker control-plane capabilities live in onboarding/registration, not in a fork of the turn engine.
|
|
5
5
|
*/
|
|
6
|
-
import type { ChannelModule, LongConnectionChannelModule } from "../../
|
|
6
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
|
|
7
7
|
import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, type FeishuWebSocketChannelOptions, defaultFeishuRoute } from "../feishu/feishu.ts";
|
|
8
8
|
export type LarkChannelOptions = FeishuChannelOptions;
|
|
9
9
|
export type LarkWebSocketChannelOptions = FeishuWebSocketChannelOptions;
|