@fastagent-sh/fastagent 0.13.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 +5 -5
- 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 +43 -0
- package/dist/channels/feishu/context-buffer.js +72 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +15 -7
- package/dist/channels/feishu/feishu-api.js +22 -4
- package/dist/channels/feishu/feishu.d.ts +38 -16
- package/dist/channels/feishu/feishu.js +286 -151
- package/dist/channels/feishu/invoke-turn.d.ts +24 -31
- package/dist/channels/feishu/invoke-turn.js +61 -62
- package/dist/channels/feishu/model.d.ts +98 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +23 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +7 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +21 -103
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +26 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- 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/lark/scaffold/channel.ts +10 -3
- 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 +13 -0
- package/dist/cli/commands/add.js +274 -0
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.js +21 -0
- package/dist/cli/commands/deploy.d.ts +15 -0
- package/dist/cli/commands/deploy.js +394 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +82 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +28 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +52 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +97 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +38 -0
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +32 -0
- 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 +90 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +479 -0
- package/dist/cli/serve.d.ts +48 -0
- package/dist/cli/serve.js +248 -0
- package/dist/cli/shared.d.ts +44 -0
- package/dist/cli/shared.js +246 -0
- package/dist/cli.js +8 -1329
- 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 +45 -0
- package/dist/deploy/docker/plan.js +141 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- 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 +57 -12
- 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 +6 -2
- package/dist/deploy/runner.js +1 -0
- 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/auth.js +160 -46
- 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 -188
- package/dist/engines/pi/config.d.ts +23 -9
- package/dist/engines/pi/config.js +35 -5
- package/dist/engines/pi/create.d.ts +36 -7
- package/dist/engines/pi/create.js +63 -22
- package/dist/engines/pi/harness.d.ts +65 -1
- package/dist/engines/pi/harness.js +166 -2
- package/dist/engines/pi/invoke.d.ts +56 -3
- package/dist/engines/pi/invoke.js +340 -20
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +17 -5
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +46 -11
- package/dist/engines/pi/tool-context.js +11 -9
- package/dist/engines/pi/tool.d.ts +35 -6
- package/dist/engines/pi/tool.js +47 -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 +56 -1
- package/dist/engines/pi/workspace.js +75 -16
- 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 +6 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +127 -16
- package/dist/scaffold/templates/fastagent.config.mjs +5 -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 +22 -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,27 +1,34 @@
|
|
|
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
|
+
import { collectFeishuBufferedAttachments, createFeishuContextBuffer, feishuBufferPlaceKey, feishuBufferText, } from "./context-buffer.js";
|
|
19
23
|
import { decryptEvent, timingSafeEqualStr, verifySignature } from "./crypto.js";
|
|
20
24
|
import { invokeFeishuTurn } from "./invoke-turn.js";
|
|
21
25
|
import { createFeishuApi } from "./feishu-api.js";
|
|
22
|
-
import {
|
|
26
|
+
import { normalizeFeishuMessage } from "./normalize.js";
|
|
27
|
+
import { createOwnedFeishuThreads } from "./owned-threads.js";
|
|
28
|
+
import { FEISHU_GROUP_CONTEXT_SCOPE } from "./setup-mode.js";
|
|
29
|
+
import { cloudEnvelope, defaultFeishuRoute, feishuEnvelope, placeKey, senderLabel, } from "./parse.js";
|
|
23
30
|
import { defaultErrorMessage, mountFeishuPreview, settleFeishuPreview, streamFeishuReply, } from "./preview.js";
|
|
24
|
-
import {
|
|
31
|
+
import { connectFeishuWs } from "./ws-ingress.js";
|
|
25
32
|
// Canonical public surface; the Lark subpath aliases these types/functions at its compatibility boundary.
|
|
26
33
|
export { defaultFeishuRoute, feishuEnvelope };
|
|
27
34
|
/** Execution ceiling: a turn that has STARTED running this many times without finishing is dropped
|
|
@@ -45,6 +52,7 @@ function isStoredFeishuTurn(t) {
|
|
|
45
52
|
typeof r.seq === "number" &&
|
|
46
53
|
typeof r.session === "string" &&
|
|
47
54
|
typeof r.baseText === "string" &&
|
|
55
|
+
(r.bufferKey === undefined || typeof r.bufferKey === "string") &&
|
|
48
56
|
typeof r.chatId === "string" &&
|
|
49
57
|
(r.replyTo === undefined || typeof r.replyTo === "string") &&
|
|
50
58
|
(r.queueReplyTo === undefined || typeof r.queueReplyTo === "string") &&
|
|
@@ -54,24 +62,33 @@ function isStoredFeishuTurn(t) {
|
|
|
54
62
|
refs(r.files) &&
|
|
55
63
|
typeof r.attempts === "number");
|
|
56
64
|
}
|
|
57
|
-
/** Build the canonical Feishu
|
|
65
|
+
/** Build the canonical Feishu Request-URL webhook channel. */
|
|
58
66
|
export function feishuChannel(opts) {
|
|
59
67
|
return buildFeishuChannel(FEISHU_CLOUD, opts, feishuChannel.name);
|
|
60
68
|
}
|
|
61
|
-
/**
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!appId || !appSecret) {
|
|
69
|
-
throw new Error(`${factoryName} requires appId + appSecret (developer console → Credentials & Basic Info)`);
|
|
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)) {
|
|
75
|
+
throw new Error(`${factoryName} directMessageSession must be "continuous" or "threaded"`);
|
|
70
76
|
}
|
|
71
|
-
if (!
|
|
72
|
-
throw new Error(`${factoryName}
|
|
77
|
+
if (opts.groupMessageSession !== undefined && !["continuous", "threaded"].includes(opts.groupMessageSession)) {
|
|
78
|
+
throw new Error(`${factoryName} groupMessageSession must be "continuous" or "threaded"`);
|
|
73
79
|
}
|
|
74
|
-
|
|
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
|
+
}
|
|
75
92
|
const formatError = onError ?? defaultErrorMessage;
|
|
76
93
|
const api = createFeishuApi({ kind, baseUrl, appId, appSecret });
|
|
77
94
|
// One bot/v3/info at startup: the bot's open_id drives the default route's group @mention summon.
|
|
@@ -82,6 +99,17 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
82
99
|
if (!botOpenId)
|
|
83
100
|
log.warn(`${label} bot/v3/info returned no open_id — group @mention summon stays off`);
|
|
84
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)}`));
|
|
85
113
|
const decide = route ?? ((event) => defaultFeishuRoute(event, { botOpenId }));
|
|
86
114
|
// The channel-state convention: this channel's durable home is `<stateRoot>/channels/<kind>`
|
|
87
115
|
// (engine state at the root, channel state under `channels/<kind>/`) — derived, not an option, so
|
|
@@ -90,13 +118,17 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
90
118
|
throw new Error(`${factoryName} requires an absolute ctx.stateRoot, got "${stateRoot}"`);
|
|
91
119
|
}
|
|
92
120
|
const stateHome = join(stateRoot, "channels", kind);
|
|
93
|
-
ensureStateHome(stateHome); // create + self-ignore —
|
|
121
|
+
ensureStateHome(stateHome); // create + self-ignore — buffers/files may carry chat content
|
|
122
|
+
const ownedThreads = createOwnedFeishuThreads(join(stateHome, "owned-threads.json"), label);
|
|
123
|
+
const buffer = createFeishuContextBuffer(join(stateHome, "buffers.json"), label);
|
|
94
124
|
const store = createTurnStore(join(stateHome, "turns.json"), {
|
|
95
125
|
label,
|
|
96
126
|
isRecord: isStoredFeishuTurn,
|
|
97
127
|
order: (a, b) => a.seq - b.seq,
|
|
98
128
|
});
|
|
99
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();
|
|
100
132
|
const toStored = (r) => {
|
|
101
133
|
const { preview: _live, ...intent } = r; // drop the live-only field; TS enforces the rest is complete
|
|
102
134
|
return { ...intent, attempts: 0 };
|
|
@@ -177,10 +209,24 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
177
209
|
}
|
|
178
210
|
const startedAt = Date.now();
|
|
179
211
|
log.info(`${label} turn start: turn=${rec.id} session=${rec.session} chat=${rec.chatId}`);
|
|
212
|
+
// Snapshot background discussion at dequeue. Commit only this snapshot on `completed`, so a
|
|
213
|
+
// message arriving while the turn runs remains buffered for the next answered turn.
|
|
214
|
+
// ponytail: independent threaded roots in one main chat dequeue concurrently and may both fold
|
|
215
|
+
// this snapshot before either commits it. That fan-out loses nothing; claiming by buffer key
|
|
216
|
+
// would instead couple otherwise-independent root sessions and require failure rollback.
|
|
217
|
+
const { text: recent, consumed } = buffer.peek(rec.bufferKey);
|
|
218
|
+
const prompt = recent ? `[recent group discussion:\n${recent}\n]\n\n${rec.baseText}` : rec.baseText;
|
|
219
|
+
const buffered = collectFeishuBufferedAttachments(consumed, {
|
|
220
|
+
images: rec.images.map((ref) => ({ messageId: ref.msg, key: ref.key })),
|
|
221
|
+
files: rec.files.map((ref) => ({ messageId: ref.msg, key: ref.key, name: ref.name })),
|
|
222
|
+
});
|
|
180
223
|
try {
|
|
181
|
-
await streamFeishuReply(invokeFeishuTurn(agent, rec.session,
|
|
182
|
-
|
|
183
|
-
|
|
224
|
+
await streamFeishuReply(invokeFeishuTurn(agent, rec.session, prompt, { api, chatId: rec.chatId, filesDir: join(stateHome, "files"), label }, { primary: { images: rec.images, files: rec.files, parentId: rec.parentId }, buffered }, () => {
|
|
225
|
+
// Drop intent first: a crash between these writes may re-fold answered context later,
|
|
226
|
+
// but can never replay this turn after its context was removed.
|
|
227
|
+
store.remove(rec.id);
|
|
228
|
+
buffer.commit(rec.bufferKey, consumed);
|
|
229
|
+
}), api, targetOf(rec), formatError, rec.preview, label);
|
|
184
230
|
log.info(`${label} turn done: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms)`);
|
|
185
231
|
}
|
|
186
232
|
catch (error) {
|
|
@@ -195,13 +241,13 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
195
241
|
}
|
|
196
242
|
},
|
|
197
243
|
});
|
|
198
|
-
// Accept a turn: persist its intent
|
|
199
|
-
//
|
|
200
|
-
// re-enqueues a crash
|
|
244
|
+
// Accept a turn: persist its intent before the ACK, then record the platform delivery id and enqueue
|
|
245
|
+
// it. The ordering is deliberate: recording first could turn a failed intent write into silent loss
|
|
246
|
+
// when the platform redelivers. Recovery re-enqueues a crash survivor without re-persisting it.
|
|
201
247
|
const submit = (rec, persist) => {
|
|
202
248
|
if (persist) {
|
|
203
|
-
store.add(toStored(rec));
|
|
204
|
-
seen.add(rec.id);
|
|
249
|
+
store.add(toStored(rec)); // failed write → HTTP/WS 500 → platform re-push
|
|
250
|
+
seen.add(rec.id); // post-persist, best-effort protection from documented duplicate pushes
|
|
205
251
|
}
|
|
206
252
|
queue.accept(rec);
|
|
207
253
|
};
|
|
@@ -218,142 +264,231 @@ export function buildFeishuChannel(profile, { appId, appSecret, verificationToke
|
|
|
218
264
|
if (recovered.length > 0)
|
|
219
265
|
log.info(`${label} recovering ${recovered.length} unfinished turn(s) from a prior run`);
|
|
220
266
|
let seqCounter = recovered.reduce((max, r) => Math.max(max, r.seq), 0);
|
|
221
|
-
for (const { attempts: _a, ...intent } of recovered)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return text("invalid json\n", 400);
|
|
267
|
+
for (const { attempts: _a, ...intent } of recovered) {
|
|
268
|
+
// A pre-buffer-version record has no trustworthy place identity. Give it an empty private bucket
|
|
269
|
+
// rather than risk consuming new main-chat context that arrived after this restart.
|
|
270
|
+
const bufferKey = intent.bufferKey ?? `${intent.chatId}:legacy-turn:${intent.id}`;
|
|
271
|
+
submit({ ...intent, bufferKey, preview: undefined }, false);
|
|
272
|
+
}
|
|
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) => {
|
|
276
|
+
const m = event.message;
|
|
277
|
+
if (!m?.message_id || !m.chat_id)
|
|
278
|
+
return;
|
|
279
|
+
if (seen.has(m.message_id)) {
|
|
280
|
+
log.debug(`${label} duplicate push for message ${m.message_id} — already persisted, skipping`);
|
|
281
|
+
return;
|
|
237
282
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
283
|
+
let r = decide(event);
|
|
284
|
+
const normalized = normalizeFeishuMessage(event);
|
|
285
|
+
if (!normalized)
|
|
286
|
+
return;
|
|
287
|
+
const bufferKey = feishuBufferPlaceKey(normalized.conversation);
|
|
288
|
+
const isHumanGroup = event.sender?.sender_type === "user" && m.chat_type === "group";
|
|
289
|
+
const managedThread = groupMessageSession === "threaded" &&
|
|
290
|
+
isHumanGroup &&
|
|
291
|
+
m.thread_id !== undefined &&
|
|
292
|
+
m.root_id !== undefined &&
|
|
293
|
+
ownedThreads.has(m.chat_id, m.root_id);
|
|
294
|
+
if (!r && route === undefined && managedThread && !normalized.content.hasMentions)
|
|
295
|
+
r = {};
|
|
296
|
+
if (!r) {
|
|
297
|
+
if (route === undefined && isHumanGroup) {
|
|
298
|
+
const bodyText = feishuBufferText(normalized.content.text);
|
|
299
|
+
if (bodyText) {
|
|
300
|
+
const resources = normalized.content.resources;
|
|
301
|
+
const images = resources
|
|
302
|
+
.filter((resource) => resource.kind === "image")
|
|
303
|
+
.map((resource) => ({ messageId: resource.messageId, key: resource.key }));
|
|
304
|
+
const files = resources
|
|
305
|
+
.filter((resource) => resource.kind === "file" || resource.kind === "audio" || resource.kind === "video")
|
|
306
|
+
.map((resource) => ({
|
|
307
|
+
messageId: resource.messageId,
|
|
308
|
+
key: resource.key,
|
|
309
|
+
name: resource.name,
|
|
310
|
+
}));
|
|
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.
|
|
313
|
+
buffer.push(bufferKey, {
|
|
314
|
+
sender: senderLabel(event.sender) ?? "someone",
|
|
315
|
+
body: bodyText,
|
|
316
|
+
messageId: m.message_id,
|
|
317
|
+
replyTo: m.parent_id,
|
|
318
|
+
files: files.length ? files : undefined,
|
|
319
|
+
images: images.length ? images : undefined,
|
|
320
|
+
});
|
|
321
|
+
seen.add(m.message_id);
|
|
322
|
+
log.debug(`${label} buffered unsummoned group message ${m.message_id} (place ${bufferKey})`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
log.debug(`${label} not summoned — ignoring empty message ${m.message_id} (chat ${m.chat_id})`);
|
|
265
326
|
}
|
|
266
|
-
return text("invalid encrypted payload\n", 400);
|
|
267
327
|
}
|
|
268
|
-
|
|
269
|
-
log.
|
|
270
|
-
return text("invalid signature\n", 401);
|
|
328
|
+
else {
|
|
329
|
+
log.debug(`${label} not summoned — ignoring message ${m.message_id} (chat ${m.chat_id}, ${m.chat_type})`);
|
|
271
330
|
}
|
|
331
|
+
return;
|
|
272
332
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return text("plaintext events not accepted\n", 401);
|
|
279
|
-
}
|
|
280
|
-
envelope = outer;
|
|
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`);
|
|
281
338
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
294
357
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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);
|
|
301
370
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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);
|
|
308
413
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
422
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
log.debug(`${label} not summoned — ignoring message ${m.message_id} (chat ${m.chat_id}, ${m.chat_type})`);
|
|
320
|
-
return new Response(null, { status: 200 });
|
|
423
|
+
try {
|
|
424
|
+
envelope = JSON.parse(decryptEvent(encryptKey, outer.encrypt));
|
|
321
425
|
}
|
|
322
|
-
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// topic); a 1:1 p2p chat needs no reply-quote. Only when the RESOLVED target is the message's own
|
|
327
|
-
// chat: a route that redirects elsewhere must not quote a same-id message in the wrong place.
|
|
328
|
-
const sameTarget = chatId === m.chat_id;
|
|
329
|
-
const replyTo = m.chat_type !== "p2p" && sameTarget ? m.message_id : undefined;
|
|
330
|
-
// Queue feedback always identifies the exact ask, including p2p. Ordinary p2p answers remain
|
|
331
|
-
// unquoted unless the turn actually waited long enough for its queue preview to mount.
|
|
332
|
-
const queueReplyTo = sameTarget ? m.message_id : undefined;
|
|
333
|
-
const content = parseContent(m);
|
|
334
|
-
const baseText = r.text ?? cloudEnvelope(event, kind);
|
|
335
|
-
if (baseText.trim() !== "" || content.imageKeys.length > 0 || content.fileRefs.length > 0) {
|
|
336
|
-
submit({
|
|
337
|
-
id: m.message_id,
|
|
338
|
-
seq: ++seqCounter,
|
|
339
|
-
session,
|
|
340
|
-
baseText,
|
|
341
|
-
chatId,
|
|
342
|
-
replyTo,
|
|
343
|
-
queueReplyTo,
|
|
344
|
-
replyInThread: replyTo !== undefined && m.thread_id !== undefined ? true : undefined,
|
|
345
|
-
parentId: m.parent_id,
|
|
346
|
-
images: content.imageKeys.map((key) => ({ msg: m.message_id, key })),
|
|
347
|
-
files: content.fileRefs.map((f) => ({ msg: m.message_id, key: f.key, name: f.name })),
|
|
348
|
-
}, true);
|
|
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);
|
|
349
430
|
}
|
|
431
|
+
return text("invalid encrypted payload\n", 400);
|
|
350
432
|
}
|
|
351
|
-
|
|
352
|
-
|
|
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);
|
|
442
|
+
}
|
|
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)"}`);
|
|
353
460
|
return new Response(null, { status: 200 });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
},
|
|
358
493
|
};
|
|
359
494
|
}
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* half touches the Open API + disk; split from feishu.ts so the factory keeps only wiring and the
|
|
7
7
|
* per-turn lifecycle.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* with the buffer itself, which needs the sensitive all-group-messages scope.)
|
|
9
|
+
* Inputs have two tiers. PRIMARY is the summoning message plus the message it explicitly replied to;
|
|
10
|
+
* any load failure there aborts visibly so the Agent never runs without an input the user pointed at.
|
|
11
|
+
* BUFFERED resources come from earlier un-summoned thread/group discussion and degrade per attachment:
|
|
12
|
+
* one expired background file must not block the current ask or hide its still-readable siblings.
|
|
14
13
|
*/
|
|
15
|
-
import {
|
|
14
|
+
import type { Agent, AgentEvent } from "../../agent.ts";
|
|
15
|
+
import { type BusyRetry } from "../invoke-turn-kit.ts";
|
|
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. */
|
|
18
19
|
export interface FeishuTurnTransport {
|
|
@@ -23,37 +24,29 @@ 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;
|
|
30
31
|
}
|
|
31
|
-
/** A turn's
|
|
32
|
-
* (resolved here — content and resources both). */
|
|
32
|
+
/** A turn's primary resources plus background resources folded from the context buffer. */
|
|
33
33
|
export interface FeishuTurnAttachments {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
maxWaitMs: number;
|
|
34
|
+
primary: {
|
|
35
|
+
images: FeishuAttachmentInput[];
|
|
36
|
+
files: FeishuAttachmentInput[];
|
|
37
|
+
/** The replied-to message's id, when the summon is a reply. */
|
|
38
|
+
parentId?: string;
|
|
39
|
+
};
|
|
40
|
+
buffered: {
|
|
41
|
+
files: FeishuBufferedRef[];
|
|
42
|
+
images: FeishuBufferedRef[];
|
|
43
|
+
skipped: number;
|
|
44
|
+
};
|
|
46
45
|
}
|
|
47
46
|
/**
|
|
48
|
-
* Run one turn: resolve its inputs, then stream agent.invoke
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* moment the turn provably lives in the session.
|
|
52
|
-
*
|
|
53
|
-
* BUSY-WAIT: a `failed{code: session_busy}` FIRST event means an external turn holds this session's
|
|
54
|
-
* lease and OUR turn never started — replay-safe. Retry (bounded) instead of yielding it: the user sees
|
|
55
|
-
* the "Thinking…" preview while waiting, and only an exhausted wait surfaces the busy failure. Only a
|
|
56
|
-
* FIRST-event busy retries — inputs are already resolved, and a fail-fast reject is the only shape the
|
|
57
|
-
* 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).
|
|
58
50
|
*/
|
|
59
51
|
export declare function invokeFeishuTurn(agent: Agent, session: string, text: string, transport: FeishuTurnTransport, attachments: FeishuTurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
|
|
52
|
+
export {};
|