@fastagent-sh/fastagent 0.14.0 → 0.15.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 +3 -3
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -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 +8 -11
- package/dist/channels/feishu/context-buffer.js +6 -67
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +20 -2
- package/dist/channels/feishu/feishu.d.ts +28 -16
- package/dist/channels/feishu/feishu.js +210 -188
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +10 -62
- package/dist/channels/feishu/model.d.ts +2 -1
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/owned-threads.d.ts +2 -6
- package/dist/channels/feishu/parse.d.ts +1 -1
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +24 -140
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -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 +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- 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/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -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/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -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 +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -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/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -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 +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -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 +152 -20
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +6 -1
- package/dist/cli/commands/deploy.d.ts +2 -0
- package/dist/cli/commands/deploy.js +78 -22
- package/dist/cli/commands/dev.js +11 -5
- package/dist/cli/commands/fire.js +3 -3
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/invoke.js +2 -3
- package/dist/cli/commands/login.js +14 -25
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/start.js +15 -8
- package/dist/cli/commands/tool.js +2 -1
- package/dist/cli/fail.d.ts +0 -2
- package/dist/cli/fail.js +1 -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 +79 -21
- package/dist/cli/serve.d.ts +38 -18
- package/dist/cli/serve.js +197 -39
- package/dist/cli/shared.d.ts +26 -6
- package/dist/cli/shared.js +163 -33
- 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/docker/plan.d.ts +2 -2
- package/dist/deploy/docker/plan.js +8 -6
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +53 -9
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +2 -1
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +14 -9
- package/dist/engines/pi/config.js +33 -18
- package/dist/engines/pi/create.d.ts +22 -2
- package/dist/engines/pi/create.js +20 -14
- 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 +11 -0
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- 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 +6 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +52 -0
- package/dist/engines/pi/workspace.js +72 -15
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- 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/observe.js +3 -0
- package/dist/pi.d.ts +5 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +126 -17
- package/dist/scaffold/templates/fastagent.config.mjs +4 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -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 +21 -10
- package/package.json +19 -7
- 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-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -1,22 +1,15 @@
|
|
|
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
13
|
/** Direct-message context + delivery policy. `threaded` (default) gives every top-level p2p message
|
|
21
14
|
* its own session, creates a platform thread for the answer, and routes later thread messages back
|
|
22
15
|
* by root message id. `continuous` keeps one session per p2p chat and sends ordinary unquoted replies. */
|
|
@@ -33,14 +26,33 @@ export interface FeishuChannelOptions {
|
|
|
33
26
|
* log). Return a string to send it, or undefined/"" to stay silent. Default: a neutral message keyed
|
|
34
27
|
* on `retryable`. A developer's own bot can surface the raw details, e.g. `(f) => `⚠️ ${f.details}``. */
|
|
35
28
|
onError?: (failed: FeishuFailure) => string | undefined;
|
|
36
|
-
/** API origin override (tests / self-hosted gateways).
|
|
37
|
-
* `
|
|
29
|
+
/** API origin override (tests / self-hosted gateways). Feishu factories default to
|
|
30
|
+
* `https://open.feishu.cn`; Lark factories default to `https://open.larksuite.com`. Named to match
|
|
31
|
+
* the other channels (telegram/slack). */
|
|
32
|
+
apiBaseUrl?: string;
|
|
33
|
+
/** @deprecated Alias of {@link apiBaseUrl}; kept for existing feishu/lark channel files. */
|
|
38
34
|
baseUrl?: string;
|
|
39
35
|
/** How long (ms) a turn waits before its reply-quoted "⏳ Queued" card mounts. Defaults to 0
|
|
40
36
|
* (immediate); the same card is later taken over by the live preview/final answer. */
|
|
41
37
|
queueNoticeDelayMs?: number;
|
|
42
38
|
}
|
|
43
|
-
|
|
39
|
+
export interface FeishuChannelOptions extends FeishuChannelBaseOptions {
|
|
40
|
+
/** Verification Token for Request-URL authentication. */
|
|
41
|
+
verificationToken: string;
|
|
42
|
+
/** Optional webhook Encrypt Key. When set, plaintext events are rejected. */
|
|
43
|
+
encryptKey?: string;
|
|
44
|
+
}
|
|
45
|
+
export type FeishuWebSocketChannelOptions = FeishuChannelBaseOptions & {
|
|
46
|
+
verificationToken?: never;
|
|
47
|
+
encryptKey?: never;
|
|
48
|
+
};
|
|
49
|
+
/** Build the canonical Feishu Request-URL webhook channel. */
|
|
44
50
|
export declare function feishuChannel(opts: FeishuChannelOptions): ChannelModule;
|
|
45
|
-
/**
|
|
46
|
-
export declare function
|
|
51
|
+
/** Build the canonical Feishu WebSocket long-connection channel. */
|
|
52
|
+
export declare function feishuWebSocketChannel(opts: FeishuWebSocketChannelOptions): LongConnectionChannelModule;
|
|
53
|
+
/** Internal compatibility seams: protocol behavior comes from Feishu; the profile binds cloud edges. */
|
|
54
|
+
interface FeishuWebSocketChannelDeps {
|
|
55
|
+
connectWs?: typeof connectFeishuWs;
|
|
56
|
+
}
|
|
57
|
+
export declare function buildFeishuChannel(profile: FeishuCloudProfile, opts: FeishuChannelOptions, factoryName: string): ChannelModule;
|
|
58
|
+
export declare function buildFeishuWebSocketChannel(profile: FeishuCloudProfile, opts: FeishuWebSocketChannelOptions, factoryName: string, deps?: FeishuWebSocketChannelDeps): LongConnectionChannelModule;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical Feishu bot-channel engine:
|
|
3
|
-
*
|
|
2
|
+
* Canonical Feishu bot-channel engine: verified webhook or official-SDK WebSocket → dedup → route →
|
|
3
|
+
* persist → enqueue → stream a live card. Feishu (open.feishu.cn) is the reference cloud. Lark
|
|
4
4
|
* international binds this engine through an explicit compatibility profile because its control plane
|
|
5
5
|
* trails Feishu; protocol reuse does not make Lark the design center.
|
|
6
6
|
*
|
|
7
7
|
* The channel kind remains the unit of route, env namespace, state home, logs, and onboarding, so one
|
|
8
|
-
* workspace may
|
|
9
|
-
*
|
|
8
|
+
* workspace may run both without sharing state. Webhook returns the existing route factory; WebSocket
|
|
9
|
+
* returns an explicit long-connection module. Both feed the same acceptance/turn engine. See docs/feishu.md.
|
|
10
10
|
*/
|
|
11
11
|
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";
|
|
15
17
|
import { ensureStateHome } from "../state.js";
|
|
18
|
+
import { dispatchStop, isStopText } from "../stop-command.js";
|
|
16
19
|
import { createTurnQueue } from "../turn-queue.js";
|
|
17
20
|
import { createTurnStore } from "../turn-store.js";
|
|
18
21
|
import { FEISHU_CLOUD } from "./cloud.js";
|
|
@@ -22,9 +25,10 @@ import { invokeFeishuTurn } from "./invoke-turn.js";
|
|
|
22
25
|
import { createFeishuApi } from "./feishu-api.js";
|
|
23
26
|
import { normalizeFeishuMessage } from "./normalize.js";
|
|
24
27
|
import { createOwnedFeishuThreads } from "./owned-threads.js";
|
|
28
|
+
import { FEISHU_GROUP_CONTEXT_SCOPE } from "./setup-mode.js";
|
|
25
29
|
import { cloudEnvelope, defaultFeishuRoute, feishuEnvelope, placeKey, senderLabel, } from "./parse.js";
|
|
26
30
|
import { defaultErrorMessage, mountFeishuPreview, settleFeishuPreview, streamFeishuReply, } from "./preview.js";
|
|
27
|
-
import {
|
|
31
|
+
import { connectFeishuWs } from "./ws-ingress.js";
|
|
28
32
|
// Canonical public surface; the Lark subpath aliases these types/functions at its compatibility boundary.
|
|
29
33
|
export { defaultFeishuRoute, feishuEnvelope };
|
|
30
34
|
/** Execution ceiling: a turn that has STARTED running this many times without finishing is dropped
|
|
@@ -58,30 +62,33 @@ function isStoredFeishuTurn(t) {
|
|
|
58
62
|
refs(r.files) &&
|
|
59
63
|
typeof r.attempts === "number");
|
|
60
64
|
}
|
|
61
|
-
/** Build the canonical Feishu
|
|
65
|
+
/** Build the canonical Feishu Request-URL webhook channel. */
|
|
62
66
|
export function feishuChannel(opts) {
|
|
63
67
|
return buildFeishuChannel(FEISHU_CLOUD, opts, feishuChannel.name);
|
|
64
68
|
}
|
|
65
|
-
/**
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// silently at the first event.
|
|
72
|
-
if (!appId || !appSecret) {
|
|
73
|
-
throw new Error(`${factoryName} requires appId + appSecret (developer console → Credentials & Basic Info)`);
|
|
74
|
-
}
|
|
75
|
-
if (!verificationToken) {
|
|
76
|
-
throw new Error(`${factoryName} requires a non-empty verificationToken (console → Events & Callbacks; an unset one accepts forged events)`);
|
|
77
|
-
}
|
|
78
|
-
if (directMessageSession !== "continuous" && directMessageSession !== "threaded") {
|
|
69
|
+
/** Build the canonical Feishu WebSocket long-connection channel. */
|
|
70
|
+
export function feishuWebSocketChannel(opts) {
|
|
71
|
+
return buildFeishuWebSocketChannel(FEISHU_CLOUD, opts, feishuWebSocketChannel.name);
|
|
72
|
+
}
|
|
73
|
+
function validateSessionOptions(opts, factoryName) {
|
|
74
|
+
if (opts.directMessageSession !== undefined && !["continuous", "threaded"].includes(opts.directMessageSession)) {
|
|
79
75
|
throw new Error(`${factoryName} directMessageSession must be "continuous" or "threaded"`);
|
|
80
76
|
}
|
|
81
|
-
if (groupMessageSession !== "continuous"
|
|
77
|
+
if (opts.groupMessageSession !== undefined && !["continuous", "threaded"].includes(opts.groupMessageSession)) {
|
|
82
78
|
throw new Error(`${factoryName} groupMessageSession must be "continuous" or "threaded"`);
|
|
83
79
|
}
|
|
84
|
-
|
|
80
|
+
}
|
|
81
|
+
function createFeishuRuntimeFactory(profile, opts, factoryName) {
|
|
82
|
+
const { appId, appSecret, directMessageSession = "threaded", groupMessageSession = "threaded", route, onError, queueNoticeDelayMs = QUEUE_NOTICE_DELAY_MS, } = opts;
|
|
83
|
+
const baseUrl = opts.apiBaseUrl ?? opts.baseUrl ?? profile.apiBase;
|
|
84
|
+
const { kind } = profile;
|
|
85
|
+
const label = `[${kind}]`;
|
|
86
|
+
return ({ agent, stateRoot, control }) => {
|
|
87
|
+
// Credential checks run when serving starts, not while the authored module is imported: deployment
|
|
88
|
+
// can inspect the module shape before secrets exist, while serving still fails before ready.
|
|
89
|
+
if (!appId || !appSecret) {
|
|
90
|
+
throw new Error(`${factoryName} requires appId + appSecret (developer console → Credentials & Basic Info)`);
|
|
91
|
+
}
|
|
85
92
|
const formatError = onError ?? defaultErrorMessage;
|
|
86
93
|
const api = createFeishuApi({ kind, baseUrl, appId, appSecret });
|
|
87
94
|
// One bot/v3/info at startup: the bot's open_id drives the default route's group @mention summon.
|
|
@@ -92,6 +99,17 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
92
99
|
if (!botOpenId)
|
|
93
100
|
log.warn(`${label} bot/v3/info returned no open_id — group @mention summon stays off`);
|
|
94
101
|
}, (e) => log.warn(`${label} bot/v3/info failed; group @mention summon stays off until restart: ${String(e)}`));
|
|
102
|
+
void api.listAppScopes().then((scopes) => {
|
|
103
|
+
const contextAware = scopes.some((scope) => scope.name === FEISHU_GROUP_CONTEXT_SCOPE &&
|
|
104
|
+
scope.grantStatus === 1 &&
|
|
105
|
+
(scope.type === undefined || scope.type === "tenant"));
|
|
106
|
+
if (contextAware) {
|
|
107
|
+
log.info(`${label} group visibility: context-aware — bare managed-thread replies + buffered discussion enabled`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
log.warn(`${label} group visibility: @mentions only — ${FEISHU_GROUP_CONTEXT_SCOPE} is not granted; bare managed-thread replies + group context buffering are unavailable`);
|
|
111
|
+
}
|
|
112
|
+
}, (error) => log.warn(`${label} could not inspect group visibility: ${String(error)}`));
|
|
95
113
|
const decide = route ?? ((event) => defaultFeishuRoute(event, { botOpenId }));
|
|
96
114
|
// The channel-state convention: this channel's durable home is `<stateRoot>/channels/<kind>`
|
|
97
115
|
// (engine state at the root, channel state under `channels/<kind>/`) — derived, not an option, so
|
|
@@ -109,6 +127,8 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
109
127
|
order: (a, b) => a.seq - b.seq,
|
|
110
128
|
});
|
|
111
129
|
const seen = createSeenRing(join(stateHome, "seen.json"), label);
|
|
130
|
+
// Side tasks (stop feedback) run off the ingress path but drain in turnsIdle.
|
|
131
|
+
const sideTasks = createTaskTracker();
|
|
112
132
|
const toStored = (r) => {
|
|
113
133
|
const { preview: _live, ...intent } = r; // drop the live-only field; TS enforces the rest is complete
|
|
114
134
|
return { ...intent, attempts: 0 };
|
|
@@ -226,7 +246,7 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
226
246
|
// when the platform redelivers. Recovery re-enqueues a crash survivor without re-persisting it.
|
|
227
247
|
const submit = (rec, persist) => {
|
|
228
248
|
if (persist) {
|
|
229
|
-
store.add(toStored(rec)); // failed write →
|
|
249
|
+
store.add(toStored(rec)); // failed write → HTTP/WS 500 → platform re-push
|
|
230
250
|
seen.add(rec.id); // post-persist, best-effort protection from documented duplicate pushes
|
|
231
251
|
}
|
|
232
252
|
queue.accept(rec);
|
|
@@ -250,104 +270,20 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
250
270
|
const bufferKey = intent.bufferKey ?? `${intent.chatId}:legacy-turn:${intent.id}`;
|
|
251
271
|
submit({ ...intent, bufferKey, preview: undefined }, false);
|
|
252
272
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const body = await readBodyCapped(req, MAX_EVENT_BYTES);
|
|
257
|
-
if ("tooLarge" in body)
|
|
258
|
-
return text("payload too large\n", 413);
|
|
259
|
-
let outer;
|
|
260
|
-
try {
|
|
261
|
-
outer = JSON.parse(body.text);
|
|
262
|
-
if (typeof outer !== "object" || outer === null)
|
|
263
|
-
throw new Error("not an object");
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return text("invalid json\n", 400);
|
|
267
|
-
}
|
|
268
|
-
// ── Verification. Two modes, decided by the CONSOLE's Encrypt Key setting, mirrored here. ──────
|
|
269
|
-
let envelope;
|
|
270
|
-
if (typeof outer.encrypt === "string") {
|
|
271
|
-
if (!encryptKey) {
|
|
272
|
-
log.error(`${label} received an ENCRYPTED event but no encryptKey is configured — set ${envPrefix}_ENCRYPT_KEY`);
|
|
273
|
-
return text("encrypt key not configured\n", 400);
|
|
274
|
-
}
|
|
275
|
-
const sig = {
|
|
276
|
-
timestamp: req.headers.get("x-lark-request-timestamp") ?? "",
|
|
277
|
-
nonce: req.headers.get("x-lark-request-nonce") ?? "",
|
|
278
|
-
signature: req.headers.get("x-lark-signature") ?? "",
|
|
279
|
-
};
|
|
280
|
-
// Ordinary encrypted events MUST verify the signature over the raw body before decryption.
|
|
281
|
-
// Feishu's documented exception is Request URL verification: its encrypted challenge carries
|
|
282
|
-
// no event-signature headers, so it is decrypted first and admitted ONLY when its type is
|
|
283
|
-
// url_verification; the common constant-time Token check below then authenticates it.
|
|
284
|
-
if (sig.signature && !verifySignature(encryptKey, sig, body.text)) {
|
|
285
|
-
log.warn(`${label} rejected an event: invalid X-Lark-Signature (encrypt key mismatch, or a forgery)`);
|
|
286
|
-
return text("invalid signature\n", 401);
|
|
287
|
-
}
|
|
288
|
-
try {
|
|
289
|
-
envelope = JSON.parse(decryptEvent(encryptKey, outer.encrypt));
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
if (!sig.signature) {
|
|
293
|
-
log.warn(`${label} rejected an unsigned encrypted request that could not be decrypted`);
|
|
294
|
-
return text("invalid encrypted payload\n", 401);
|
|
295
|
-
}
|
|
296
|
-
return text("invalid encrypted payload\n", 400);
|
|
297
|
-
}
|
|
298
|
-
if (!sig.signature && envelope.type !== "url_verification") {
|
|
299
|
-
log.warn(`${label} rejected an encrypted event: missing X-Lark-Signature`);
|
|
300
|
-
return text("invalid signature\n", 401);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
if (encryptKey) {
|
|
305
|
-
// With an Encrypt Key configured, a PLAINTEXT event can only be a forgery (or a console
|
|
306
|
-
// mismatch — surfaced in the log): accepting it would let a sender skip the signature.
|
|
307
|
-
log.warn(`${label} rejected a plaintext event while encryptKey is set (console mismatch, or a forgery)`);
|
|
308
|
-
return text("plaintext events not accepted\n", 401);
|
|
309
|
-
}
|
|
310
|
-
envelope = outer;
|
|
311
|
-
}
|
|
312
|
-
// The Verification Token authenticates plaintext mode and the platform-documented unsigned,
|
|
313
|
-
// encrypted URL challenge; on signed encrypted events it is defense in depth. V2 events carry it
|
|
314
|
-
// in header.token, while url_verification carries it at the top level. Fail closed when absent.
|
|
315
|
-
const token = (typeof envelope.token === "string" ? envelope.token : undefined) ??
|
|
316
|
-
(typeof envelope.header?.token === "string"
|
|
317
|
-
? envelope.header.token
|
|
318
|
-
: undefined);
|
|
319
|
-
if (!token || !timingSafeEqualStr(token, verificationToken)) {
|
|
320
|
-
// Loud on purpose: the send side gets an opaque 401 and the platform just retries — this line is
|
|
321
|
-
// the operator's ONLY signal that LARK_VERIFICATION_TOKEN does not match the console.
|
|
322
|
-
log.warn(`${label} rejected an event: verification token mismatch (check ${envPrefix}_VERIFICATION_TOKEN against the console)`);
|
|
323
|
-
return text("invalid token\n", 401);
|
|
324
|
-
}
|
|
325
|
-
// ── The console's URL-verification challenge (fires when the operator saves the Request URL). ──
|
|
326
|
-
if (envelope.type === "url_verification" && typeof envelope.challenge === "string") {
|
|
327
|
-
// The console fires this when the operator saves the Request URL; without this line a PASSING
|
|
328
|
-
// handshake is invisible and "did the challenge even arrive?" becomes guesswork.
|
|
329
|
-
log.info(`${label} answered the console's url_verification challenge`);
|
|
330
|
-
return Response.json({ challenge: envelope.challenge });
|
|
331
|
-
}
|
|
332
|
-
// ── Events. Only im.message.receive_v1 is consumed; everything else is ACKed and dropped
|
|
333
|
-
// (a non-2xx would just make the platform retry an event this channel will never act on). ──────
|
|
334
|
-
const header = envelope.header;
|
|
335
|
-
if (header?.event_type !== "im.message.receive_v1") {
|
|
336
|
-
log.debug(`${label} ignoring event type ${header?.event_type ?? "(none)"}`);
|
|
337
|
-
return new Response(null, { status: 200 });
|
|
338
|
-
}
|
|
339
|
-
const event = (envelope.event ?? {});
|
|
273
|
+
// Transport-neutral acceptance boundary. It performs only the fast pre-ACK work: normalize,
|
|
274
|
+
// route, persist intent/context, and enqueue. The minutes-long Agent turn remains fire-and-forget.
|
|
275
|
+
const acceptEvent = (event) => {
|
|
340
276
|
const m = event.message;
|
|
341
277
|
if (!m?.message_id || !m.chat_id)
|
|
342
|
-
return
|
|
278
|
+
return;
|
|
343
279
|
if (seen.has(m.message_id)) {
|
|
344
280
|
log.debug(`${label} duplicate push for message ${m.message_id} — already persisted, skipping`);
|
|
345
|
-
return
|
|
281
|
+
return;
|
|
346
282
|
}
|
|
347
283
|
let r = decide(event);
|
|
348
284
|
const normalized = normalizeFeishuMessage(event);
|
|
349
285
|
if (!normalized)
|
|
350
|
-
return
|
|
286
|
+
return;
|
|
351
287
|
const bufferKey = feishuBufferPlaceKey(normalized.conversation);
|
|
352
288
|
const isHumanGroup = event.sender?.sender_type === "user" && m.chat_type === "group";
|
|
353
289
|
const managedThread = groupMessageSession === "threaded" &&
|
|
@@ -355,9 +291,6 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
355
291
|
m.thread_id !== undefined &&
|
|
356
292
|
m.root_id !== undefined &&
|
|
357
293
|
ownedThreads.has(m.chat_id, m.root_id);
|
|
358
|
-
// In an Agent-created thread, a bare user continuation still summons. Any explicit mention changes
|
|
359
|
-
// that intent: only defaultFeishuRoute's structural @THIS-bot match summons; @other-only discussion
|
|
360
|
-
// is buffered like unsummoned group context. A custom route remains fully authoritative.
|
|
361
294
|
if (!r && route === undefined && managedThread && !normalized.content.hasMentions)
|
|
362
295
|
r = {};
|
|
363
296
|
if (!r) {
|
|
@@ -375,7 +308,8 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
375
308
|
key: resource.key,
|
|
376
309
|
name: resource.name,
|
|
377
310
|
}));
|
|
378
|
-
//
|
|
311
|
+
// A write failure escapes this boundary. HTTP turns it into a 500 response; the official WS
|
|
312
|
+
// SDK turns it into a 500 ACK frame. Both transports therefore ask the platform to re-push.
|
|
379
313
|
buffer.push(bufferKey, {
|
|
380
314
|
sender: senderLabel(event.sender) ?? "someone",
|
|
381
315
|
body: bodyText,
|
|
@@ -384,7 +318,7 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
384
318
|
files: files.length ? files : undefined,
|
|
385
319
|
images: images.length ? images : undefined,
|
|
386
320
|
});
|
|
387
|
-
seen.add(m.message_id);
|
|
321
|
+
seen.add(m.message_id);
|
|
388
322
|
log.debug(`${label} buffered unsummoned group message ${m.message_id} (place ${bufferKey})`);
|
|
389
323
|
}
|
|
390
324
|
else {
|
|
@@ -394,79 +328,167 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
394
328
|
else {
|
|
395
329
|
log.debug(`${label} not summoned — ignoring message ${m.message_id} (chat ${m.chat_id}, ${m.chat_type})`);
|
|
396
330
|
}
|
|
397
|
-
return
|
|
331
|
+
return;
|
|
398
332
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
333
|
+
const threadedP2p = directMessageSession === "threaded" && m.chat_type === "p2p";
|
|
334
|
+
const threadedGroup = groupMessageSession === "threaded" && m.chat_type === "group";
|
|
335
|
+
const threadedConversation = threadedP2p || threadedGroup;
|
|
336
|
+
if (threadedConversation && m.thread_id !== undefined && m.root_id === undefined) {
|
|
337
|
+
log.warn(`${label} threaded ${m.chat_type} message ${m.message_id} has thread_id ${m.thread_id} but no root_id — session continuity cannot be guaranteed`);
|
|
338
|
+
}
|
|
339
|
+
const defaultSession = threadedConversation
|
|
340
|
+
? `${kind}:${m.thread_id === undefined ? m.message_id : (m.root_id ?? `missing-root:${m.thread_id}`)}`
|
|
341
|
+
: placeKey(m);
|
|
342
|
+
const session = r.session ?? defaultSession;
|
|
343
|
+
const chatId = r.chatId ?? m.chat_id;
|
|
344
|
+
const sameTarget = chatId === m.chat_id;
|
|
345
|
+
const replyTo = sameTarget && (m.chat_type === "group" || threadedP2p) ? m.message_id : undefined;
|
|
346
|
+
const replyInThread = replyTo !== undefined && (threadedConversation || m.thread_id !== undefined) ? true : undefined;
|
|
347
|
+
const queueReplyTo = sameTarget ? m.message_id : undefined;
|
|
348
|
+
// Explicit user stop: a control action, never a turn — it must not queue behind the run it
|
|
349
|
+
// stops. Mentions arrive as @name tokens; strip them before matching the bare word. Record the
|
|
350
|
+
// message id so a platform re-push doesn't double-abort or double-notify.
|
|
351
|
+
if (isStopText(normalized.content.text.replace(/@\S+/g, " "))) {
|
|
352
|
+
seen.add(m.message_id);
|
|
353
|
+
sideTasks.track(dispatchStop(control, session, label)
|
|
354
|
+
.then((feedback) => api.sendText({ chatId, replyTo, replyInThread }, feedback).then(() => undefined))
|
|
355
|
+
.catch((error) => log.warn(`${label} stop feedback failed: ${String(error)}`)));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const resources = normalized.content.resources;
|
|
359
|
+
const images = resources
|
|
360
|
+
.filter((resource) => resource.kind === "image")
|
|
361
|
+
.map((resource) => ({ msg: resource.messageId, key: resource.key }));
|
|
362
|
+
const files = resources
|
|
363
|
+
.filter((resource) => resource.kind === "file" || resource.kind === "audio" || resource.kind === "video")
|
|
364
|
+
.map((resource) => ({ msg: resource.messageId, key: resource.key, name: resource.name }));
|
|
365
|
+
const baseText = r.text ?? cloudEnvelope(event, kind);
|
|
366
|
+
if (baseText.trim() === "" && images.length === 0 && files.length === 0)
|
|
367
|
+
return;
|
|
368
|
+
if (route === undefined && threadedGroup && m.thread_id === undefined && sameTarget && replyInThread === true) {
|
|
369
|
+
ownedThreads.add(m.chat_id, m.message_id);
|
|
370
|
+
}
|
|
371
|
+
submit({
|
|
372
|
+
id: m.message_id,
|
|
373
|
+
seq: ++seqCounter,
|
|
374
|
+
session,
|
|
375
|
+
baseText,
|
|
376
|
+
bufferKey,
|
|
377
|
+
chatId,
|
|
378
|
+
replyTo,
|
|
379
|
+
queueReplyTo,
|
|
380
|
+
replyInThread,
|
|
381
|
+
parentId: threadedConversation && m.thread_id !== undefined ? undefined : m.parent_id,
|
|
382
|
+
images,
|
|
383
|
+
files,
|
|
384
|
+
}, true);
|
|
385
|
+
};
|
|
386
|
+
return { acceptEvent, turnsIdle: () => Promise.all([queue.idle(), sideTasks.drain()]).then(() => undefined) };
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function createFeishuWebhookRoutes(profile, opts, runtime) {
|
|
390
|
+
const { verificationToken, encryptKey } = opts;
|
|
391
|
+
const { kind, envPrefix } = profile;
|
|
392
|
+
const label = `[${kind}]`;
|
|
393
|
+
const handler = async (req) => {
|
|
394
|
+
if (req.method !== "POST")
|
|
395
|
+
return text("POST only\n", 405);
|
|
396
|
+
const body = await readBodyCapped(req, MAX_EVENT_BYTES);
|
|
397
|
+
if ("tooLarge" in body)
|
|
398
|
+
return text("payload too large\n", 413);
|
|
399
|
+
let outer;
|
|
400
|
+
try {
|
|
401
|
+
outer = JSON.parse(body.text);
|
|
402
|
+
if (typeof outer !== "object" || outer === null)
|
|
403
|
+
throw new Error("not an object");
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return text("invalid json\n", 400);
|
|
407
|
+
}
|
|
408
|
+
let envelope;
|
|
409
|
+
if (typeof outer.encrypt === "string") {
|
|
410
|
+
if (!encryptKey) {
|
|
411
|
+
log.error(`${label} received an ENCRYPTED event but no encryptKey is configured — set ${envPrefix}_ENCRYPT_KEY`);
|
|
412
|
+
return text("encrypt key not configured\n", 400);
|
|
413
|
+
}
|
|
414
|
+
const sig = {
|
|
415
|
+
timestamp: req.headers.get("x-lark-request-timestamp") ?? "",
|
|
416
|
+
nonce: req.headers.get("x-lark-request-nonce") ?? "",
|
|
417
|
+
signature: req.headers.get("x-lark-signature") ?? "",
|
|
418
|
+
};
|
|
419
|
+
if (sig.signature && !verifySignature(encryptKey, sig, body.text)) {
|
|
420
|
+
log.warn(`${label} rejected an event: invalid X-Lark-Signature (encrypt key mismatch, or a forgery)`);
|
|
421
|
+
return text("invalid signature\n", 401);
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
envelope = JSON.parse(decryptEvent(encryptKey, outer.encrypt));
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
if (!sig.signature) {
|
|
428
|
+
log.warn(`${label} rejected an unsigned encrypted request that could not be decrypted`);
|
|
429
|
+
return text("invalid encrypted payload\n", 401);
|
|
462
430
|
}
|
|
431
|
+
return text("invalid encrypted payload\n", 400);
|
|
432
|
+
}
|
|
433
|
+
if (!sig.signature && envelope.type !== "url_verification") {
|
|
434
|
+
log.warn(`${label} rejected an encrypted event: missing X-Lark-Signature`);
|
|
435
|
+
return text("invalid signature\n", 401);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
if (encryptKey) {
|
|
440
|
+
log.warn(`${label} rejected a plaintext event while encryptKey is set (console mismatch, or a forgery)`);
|
|
441
|
+
return text("plaintext events not accepted\n", 401);
|
|
463
442
|
}
|
|
464
|
-
|
|
465
|
-
|
|
443
|
+
envelope = outer;
|
|
444
|
+
}
|
|
445
|
+
const token = (typeof envelope.token === "string" ? envelope.token : undefined) ??
|
|
446
|
+
(typeof envelope.header?.token === "string"
|
|
447
|
+
? envelope.header.token
|
|
448
|
+
: undefined);
|
|
449
|
+
if (!token || !timingSafeEqualStr(token, verificationToken)) {
|
|
450
|
+
log.warn(`${label} rejected an event: verification token mismatch (check ${envPrefix}_VERIFICATION_TOKEN against the console)`);
|
|
451
|
+
return text("invalid token\n", 401);
|
|
452
|
+
}
|
|
453
|
+
if (envelope.type === "url_verification" && typeof envelope.challenge === "string") {
|
|
454
|
+
log.info(`${label} answered the console's url_verification challenge`);
|
|
455
|
+
return Response.json({ challenge: envelope.challenge });
|
|
456
|
+
}
|
|
457
|
+
const header = envelope.header;
|
|
458
|
+
if (header?.event_type !== "im.message.receive_v1") {
|
|
459
|
+
log.debug(`${label} ignoring event type ${header?.event_type ?? "(none)"}`);
|
|
466
460
|
return new Response(null, { status: 200 });
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
461
|
+
}
|
|
462
|
+
runtime.acceptEvent((envelope.event ?? {}));
|
|
463
|
+
return new Response(null, { status: 200 });
|
|
464
|
+
};
|
|
465
|
+
handler.turnsIdle = runtime.turnsIdle;
|
|
466
|
+
return { [`POST /${kind}`]: handler };
|
|
467
|
+
}
|
|
468
|
+
export function buildFeishuChannel(profile, opts, factoryName) {
|
|
469
|
+
validateSessionOptions(opts, factoryName);
|
|
470
|
+
const createRuntime = createFeishuRuntimeFactory(profile, opts, factoryName);
|
|
471
|
+
return (ctx) => {
|
|
472
|
+
if (!opts.verificationToken) {
|
|
473
|
+
throw new Error(`${factoryName} requires a non-empty verificationToken (console → Events & Callbacks)`);
|
|
474
|
+
}
|
|
475
|
+
return createFeishuWebhookRoutes(profile, opts, createRuntime(ctx));
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
export function buildFeishuWebSocketChannel(profile, opts, factoryName, deps = {}) {
|
|
479
|
+
validateSessionOptions(opts, factoryName);
|
|
480
|
+
const createRuntime = createFeishuRuntimeFactory(profile, opts, factoryName);
|
|
481
|
+
return {
|
|
482
|
+
name: `${profile.kind} websocket`,
|
|
483
|
+
connect(ctx, signal) {
|
|
484
|
+
const runtime = createRuntime(ctx);
|
|
485
|
+
return (deps.connectWs ?? connectFeishuWs)({
|
|
486
|
+
kind: profile.kind,
|
|
487
|
+
appId: opts.appId,
|
|
488
|
+
appSecret: opts.appSecret,
|
|
489
|
+
domain: opts.apiBaseUrl ?? opts.baseUrl ?? profile.apiBase,
|
|
490
|
+
onEvent: runtime.acceptEvent,
|
|
491
|
+
}, signal);
|
|
492
|
+
},
|
|
471
493
|
};
|
|
472
494
|
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* BUFFERED resources come from earlier un-summoned thread/group discussion and degrade per attachment:
|
|
12
12
|
* one expired background file must not block the current ask or hide its still-readable siblings.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
15
|
+
import { type BusyRetry } from "../invoke-turn-kit.ts";
|
|
15
16
|
import type { FeishuBufferedRef } from "./context-buffer.ts";
|
|
16
17
|
import type { FeishuApi } from "./feishu-api.ts";
|
|
17
18
|
/** Everything the transport needs to fetch a turn's attachments. */
|
|
@@ -23,7 +24,7 @@ export interface FeishuTurnTransport {
|
|
|
23
24
|
}
|
|
24
25
|
/** An attachment reference: the resource key inside its CARRYING message (the resource API addresses
|
|
25
26
|
* bytes by message_id + key, so the pair travels together through the turn record). */
|
|
26
|
-
|
|
27
|
+
interface FeishuAttachmentInput {
|
|
27
28
|
msg: string;
|
|
28
29
|
key: string;
|
|
29
30
|
name?: string;
|
|
@@ -42,24 +43,10 @@ export interface FeishuTurnAttachments {
|
|
|
42
43
|
skipped: number;
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
|
-
/** How the busy-wait paces: retry the invoke every `delayMs` while the session's lease is held by an
|
|
46
|
-
* EXTERNAL turn (a self-scheduled wake, a concurrent embedder invoke), up to `maxWaitMs` total. The
|
|
47
|
-
* channel's own turns never collide (the turn-queue serializes per session), so a busy reject here is
|
|
48
|
-
* always an outside holder — wait for it like a queued turn, instead of erroring at the user. */
|
|
49
|
-
export interface BusyRetry {
|
|
50
|
-
delayMs: number;
|
|
51
|
-
maxWaitMs: number;
|
|
52
|
-
}
|
|
53
46
|
/**
|
|
54
|
-
* Run one turn: resolve its inputs, then stream agent.invoke
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* moment the turn provably lives in the session.
|
|
58
|
-
*
|
|
59
|
-
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
60
|
-
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user sees
|
|
61
|
-
* the "Thinking…" preview while waiting, and only an exhausted wait surfaces the busy failure. Only a
|
|
62
|
-
* FIRST-event busy retries — inputs are already resolved, and a fail-fast reject is the only shape the
|
|
63
|
-
* engine emits it in, so nothing that started is ever re-run.
|
|
47
|
+
* Run one turn: resolve its inputs, then stream agent.invoke with the shared busy-wait
|
|
48
|
+
* (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
|
|
49
|
+
* primary-input failure surfaces as a `failed` event (never a silent drop).
|
|
64
50
|
*/
|
|
65
51
|
export declare function invokeFeishuTurn(agent: Agent, session: string, text: string, transport: FeishuTurnTransport, attachments: FeishuTurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
|
|
52
|
+
export {};
|