@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,175 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
function restoreMentions(text, mentions) {
|
|
3
|
-
let out = text;
|
|
4
|
-
for (const m of mentions ?? []) {
|
|
5
|
-
if (!m.key)
|
|
6
|
-
continue;
|
|
7
|
-
out = out.split(m.key).join(`@${m.name ?? "user"}`);
|
|
8
|
-
}
|
|
9
|
-
return out;
|
|
10
|
-
}
|
|
1
|
+
import { decodeFeishuContent } from "./normalize.js";
|
|
11
2
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* external input, and a message the agent cannot read should still say WHAT it couldn't read.
|
|
3
|
+
* Compatibility decoder for existing helpers/tests and parent-message resolution. The canonical
|
|
4
|
+
* decoder now emits typed resources; this wrapper projects them onto the historical parallel arrays.
|
|
15
5
|
*/
|
|
16
|
-
export function parseContent(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const imageKeys = [];
|
|
27
|
-
const fileRefs = [];
|
|
28
|
-
const str = (v) => (typeof v === "string" && v !== "" ? v : undefined);
|
|
29
|
-
switch (m.message_type) {
|
|
30
|
-
case "text":
|
|
31
|
-
return { text: restoreMentions(str(c.text) ?? "", m.mentions), imageKeys, fileRefs };
|
|
32
|
-
case "post": {
|
|
33
|
-
// A post is paragraphs of typed nodes; renders as text lines with inline markers. Mentions in a
|
|
34
|
-
// post are `at` NODES (user_name inline), not placeholders — no restore pass needed.
|
|
35
|
-
const lines = [];
|
|
36
|
-
const title = str(c.title);
|
|
37
|
-
if (title)
|
|
38
|
-
lines.push(title);
|
|
39
|
-
const paragraphs = Array.isArray(c.content) ? c.content : [];
|
|
40
|
-
for (const para of paragraphs) {
|
|
41
|
-
if (!Array.isArray(para))
|
|
42
|
-
continue;
|
|
43
|
-
const parts = [];
|
|
44
|
-
for (const node of para) {
|
|
45
|
-
if (typeof node !== "object" || node === null)
|
|
46
|
-
continue;
|
|
47
|
-
if (node.tag === "at")
|
|
48
|
-
parts.push(`@${str(node.user_name) ?? str(node.user_id) ?? "user"}`);
|
|
49
|
-
else if (node.tag === "a")
|
|
50
|
-
parts.push(node.href ? `${str(node.text) ?? node.href} (${node.href})` : (str(node.text) ?? ""));
|
|
51
|
-
else if (node.tag === "img") {
|
|
52
|
-
if (str(node.image_key))
|
|
53
|
-
imageKeys.push(node.image_key);
|
|
54
|
-
parts.push("[image]");
|
|
55
|
-
}
|
|
56
|
-
else if (node.tag === "media") {
|
|
57
|
-
if (str(node.file_key))
|
|
58
|
-
fileRefs.push({ key: node.file_key, name: str(node.file_name) });
|
|
59
|
-
parts.push("[video]");
|
|
60
|
-
}
|
|
61
|
-
else if (node.tag === "code_block")
|
|
62
|
-
parts.push(`\n\`\`\`${str(node.language)?.toLowerCase() ?? ""}\n${str(node.text) ?? ""}\n\`\`\`\n`);
|
|
63
|
-
else if (str(node.text))
|
|
64
|
-
parts.push(node.text);
|
|
65
|
-
}
|
|
66
|
-
const line = parts.join("").trim();
|
|
67
|
-
if (line)
|
|
68
|
-
lines.push(line);
|
|
69
|
-
}
|
|
70
|
-
return { text: lines.join("\n"), imageKeys, fileRefs };
|
|
71
|
-
}
|
|
72
|
-
case "image": {
|
|
73
|
-
if (str(c.image_key))
|
|
74
|
-
imageKeys.push(c.image_key);
|
|
75
|
-
return { text: "[image]", imageKeys, fileRefs };
|
|
76
|
-
}
|
|
77
|
-
case "file": {
|
|
78
|
-
const name = str(c.file_name);
|
|
79
|
-
if (str(c.file_key))
|
|
80
|
-
fileRefs.push({ key: c.file_key, name });
|
|
81
|
-
return { text: `[file: ${name ?? "file"}]`, imageKeys, fileRefs };
|
|
82
|
-
}
|
|
83
|
-
case "audio": {
|
|
84
|
-
if (str(c.file_key))
|
|
85
|
-
fileRefs.push({ key: c.file_key, name: "voice-message" });
|
|
86
|
-
return { text: "[voice message]", imageKeys, fileRefs };
|
|
87
|
-
}
|
|
88
|
-
case "media": {
|
|
89
|
-
const name = str(c.file_name);
|
|
90
|
-
if (str(c.file_key))
|
|
91
|
-
fileRefs.push({ key: c.file_key, name });
|
|
92
|
-
return { text: `[video: ${name ?? "video"}]`, imageKeys, fileRefs };
|
|
93
|
-
}
|
|
94
|
-
case "location": {
|
|
95
|
-
const name = str(c.name);
|
|
96
|
-
return {
|
|
97
|
-
text: `[location: ${name ? `${name} — ` : ""}${str(c.latitude) ?? "?"},${str(c.longitude) ?? "?"}]`,
|
|
98
|
-
imageKeys,
|
|
99
|
-
fileRefs,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
default:
|
|
103
|
-
// sticker / share_chat / share_user / system / … — name the type so the agent can say what it got.
|
|
104
|
-
return { text: `[${m.message_type} message]`, imageKeys, fileRefs };
|
|
105
|
-
}
|
|
6
|
+
export function parseContent(message) {
|
|
7
|
+
const decoded = decodeFeishuContent(message);
|
|
8
|
+
return {
|
|
9
|
+
text: decoded.text,
|
|
10
|
+
imageKeys: decoded.resources.filter((resource) => resource.kind === "image").map((resource) => resource.key),
|
|
11
|
+
fileRefs: decoded.resources
|
|
12
|
+
.filter((resource) => resource.kind === "file" || resource.kind === "audio" || resource.kind === "video")
|
|
13
|
+
.map((resource) => ({ key: resource.key, name: resource.name })),
|
|
14
|
+
};
|
|
106
15
|
}
|
|
107
|
-
/** A stable sender label for attribution.
|
|
108
|
-
* contacts-API scope), so the label is the open_id — stable across turns, which is what a shared
|
|
109
|
-
* multi-user session needs to tell participants apart. */
|
|
16
|
+
/** A stable sender label for attribution. Display names require an additional contacts permission. */
|
|
110
17
|
export function senderLabel(sender) {
|
|
111
18
|
const id = sender?.sender_id?.open_id ?? sender?.sender_id?.user_id ?? sender?.sender_id?.union_id;
|
|
112
19
|
return id ? `user ${id}` : undefined;
|
|
113
20
|
}
|
|
114
|
-
/** The place a message lives (chat, or chat:topic in a topic group) — the default session key. */
|
|
115
|
-
export function placeKey(
|
|
116
|
-
return
|
|
21
|
+
/** The place a message lives (chat, or chat:topic in a topic group) — the legacy default session key. */
|
|
22
|
+
export function placeKey(message) {
|
|
23
|
+
return message.thread_id ? `${message.chat_id}:${message.thread_id}` : message.chat_id;
|
|
117
24
|
}
|
|
118
|
-
/**
|
|
119
|
-
* The default base prompt: a context envelope (chat/thread/sender + a group note + a reply marker),
|
|
120
|
-
* then the message's decoded body. The sender is named on every message and a group chat is flagged —
|
|
121
|
-
* in a shared multi-user session that is how the model tells participants apart and knows it is not a
|
|
122
|
-
* 1:1. A reply carries only `[in reply to msg …]` here: the referent's CONTENT is not in the event, so
|
|
123
|
-
* the channel fetches and appends it in the IO half (invoke-turn.ts), keeping this layer pure. Exported
|
|
124
|
-
* so a custom Feishu `route` can reuse it, e.g. `text: `${feishuEnvelope(event)}\n\n[extra]``. The
|
|
125
|
-
* internal compatibility seam binds the same shape to `[lark: …]`; each kind's send tool reads the
|
|
126
|
-
* chat id from its own branded line.
|
|
127
|
-
*/
|
|
25
|
+
/** The canonical Feishu-branded prompt envelope. */
|
|
128
26
|
export function feishuEnvelope(event) {
|
|
129
27
|
return cloudEnvelope(event, "feishu");
|
|
130
28
|
}
|
|
131
29
|
/** Internal compatibility seam: bind the canonical envelope shape to one cloud's branded tag. */
|
|
132
30
|
export function cloudEnvelope(event, tag) {
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
31
|
+
const message = event.message;
|
|
32
|
+
if (!message)
|
|
135
33
|
return "";
|
|
34
|
+
const from = senderLabel(event.sender);
|
|
136
35
|
const meta = [
|
|
137
|
-
`chat ${
|
|
138
|
-
|
|
139
|
-
|
|
36
|
+
`chat ${message.chat_id} (${message.chat_type})`,
|
|
37
|
+
message.thread_id ? `topic ${message.thread_id}` : undefined,
|
|
38
|
+
from ? `from ${from}` : undefined,
|
|
140
39
|
]
|
|
141
40
|
.filter(Boolean)
|
|
142
41
|
.join(", ");
|
|
143
|
-
const scope =
|
|
144
|
-
const replyTo =
|
|
145
|
-
return `[${tag}: ${meta}]${scope}${replyTo}\n${parseContent(
|
|
42
|
+
const scope = message.chat_type === "group" ? "\n[group chat — multiple people; each message is prefixed with its sender]" : "";
|
|
43
|
+
const replyTo = message.parent_id ? `\n[in reply to msg ${message.parent_id}]` : "";
|
|
44
|
+
return `[${tag}: ${meta}]${scope}${replyTo}\n${parseContent(message).text}`;
|
|
146
45
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
* (never a regex over the text: a pasted `@bot` in a code block is not a mention entry), matched on the
|
|
150
|
-
* bot's open_id (stable identity; names are mutable). No id → fail closed (false): answering "is this
|
|
151
|
-
* mention me?" with "I don't know who I am, so yes" would mis-summon in every multi-bot group.
|
|
152
|
-
*/
|
|
153
|
-
export function mentionsBot(m, botOpenId) {
|
|
46
|
+
/** Whether the parsed mention list contains this bot's app-scoped open_id. */
|
|
47
|
+
export function mentionsBot(message, botOpenId) {
|
|
154
48
|
if (!botOpenId)
|
|
155
49
|
return false;
|
|
156
|
-
return (
|
|
50
|
+
return (message.mentions ?? []).some((mention) => mention.id?.open_id === botOpenId);
|
|
157
51
|
}
|
|
158
52
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* feishuChannel resolves via bot/v3/info). NOTE the platform side of the same coin: with the default
|
|
163
|
-
* `im:message.group_at_msg` scope, un-mentioned group messages are never even delivered — receiving
|
|
164
|
-
* everything needs the sensitive `im:message.group_msg` scope. Returns `{}` (act; the channel fills
|
|
165
|
-
* session/target/prompt from the message) or `null` (ignore).
|
|
53
|
+
* Default EXPLICIT-summon policy: ignore non-user senders, always answer p2p, and answer groups only
|
|
54
|
+
* when THIS bot is structurally mentioned. No bot identity means group routing fails closed. The
|
|
55
|
+
* stateful channel wiring may additionally admit unmentioned continuations from its managed-root index.
|
|
166
56
|
*/
|
|
167
57
|
export function defaultFeishuRoute(event, options) {
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
58
|
+
const message = event.message;
|
|
59
|
+
if (!message)
|
|
170
60
|
return null;
|
|
171
61
|
if (event.sender?.sender_type !== "user")
|
|
172
62
|
return null;
|
|
173
|
-
const summoned =
|
|
63
|
+
const summoned = message.chat_type === "p2p" || mentionsBot(message, options?.botOpenId);
|
|
174
64
|
return summoned ? {} : null;
|
|
175
65
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { AgentEvent } from "../../agent.ts";
|
|
2
2
|
import { type FeishuApi, type FeishuTarget } from "./feishu-api.ts";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
/** The customer-facing default: neutral, no leaked internals; differentiate only on whether to retry. */
|
|
9
|
-
export declare function defaultErrorMessage(failed: FeishuFailure): string;
|
|
3
|
+
import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
|
|
4
|
+
/** A terminal failure, as the channel hands it to `onError` — the shared channel shape. */
|
|
5
|
+
export type FeishuFailure = ChannelFailure;
|
|
6
|
+
export { defaultErrorMessage };
|
|
10
7
|
/** A visible preview mounted into the chat. Exported only for the channel wiring: a queued turn mounts
|
|
11
8
|
* one before execution, then hands the exact entity/message to {@link streamFeishuReply} for takeover. */
|
|
12
9
|
export type MountedFeishuPreview = {
|
|
@@ -23,41 +23,16 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
23
23
|
import { log } from "../../log.js";
|
|
24
24
|
import { ANSWER_ELEMENT_ID, CARD_MARKDOWN_MAX_BYTES, cardEntityContent, finalCardJson, streamingCardJson, } from "./card.js";
|
|
25
25
|
import { chunkFeishuText, isCardStreamingClosed } from "./feishu-api.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
return failed.retryable ? "⚠️ Temporary problem — please try again." : "⚠️ Sorry, something went wrong.";
|
|
30
|
-
}
|
|
26
|
+
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../preview-kit.js";
|
|
27
|
+
import { truncateUtf8 } from "../text.js";
|
|
28
|
+
export { defaultErrorMessage };
|
|
31
29
|
/** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. Cardkit
|
|
32
30
|
* allows 10 QPS per card entity (50 per app), but one snapshot a second reads smoothly (the client
|
|
33
31
|
* animates between snapshots).
|
|
34
32
|
* Doubles as the answer-preview aging window (see answerView). */
|
|
35
33
|
const STREAM_THROTTLE_MS = 1000;
|
|
36
|
-
/** Max length of a tool's arg preview in the live view. */
|
|
37
|
-
const TOOL_ARG_MAX = 48;
|
|
38
34
|
/** How much of the (growing) reasoning to peek at in the live view — the most recent tail. */
|
|
39
35
|
const THINKING_PREVIEW = 280;
|
|
40
|
-
/** The placeholder shown before any reasoning/tool/text arrives. */
|
|
41
|
-
const THINKING_PLACEHOLDER = "💭 Thinking…";
|
|
42
|
-
/** One-line, truncated: collapse whitespace so a multi-line command/arg stays on one line. */
|
|
43
|
-
function clip(s) {
|
|
44
|
-
const one = s.replace(/\s+/g, " ").trim();
|
|
45
|
-
return truncateCodePointPrefix(one, TOOL_ARG_MAX);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md`
|
|
49
|
-
* rather than just `🔧 read`. Generic (the channel knows no tool schemas): show the salient value — the
|
|
50
|
-
* first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
|
|
51
|
-
*/
|
|
52
|
-
function summarizeArgs(args) {
|
|
53
|
-
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
54
|
-
return clip(String(args));
|
|
55
|
-
const values = Object.values(args);
|
|
56
|
-
const primary = values.find((v) => typeof v === "string" || typeof v === "number");
|
|
57
|
-
if (primary !== undefined)
|
|
58
|
-
return clip(String(primary));
|
|
59
|
-
return values.length > 0 ? clip(JSON.stringify(args)) : "";
|
|
60
|
-
}
|
|
61
36
|
/** Cap a live view to the card budget, PREFIX-STABLE: the streaming client animates only when the old
|
|
62
37
|
* text is a prefix of the new, so an over-budget view freezes at its head rather than sliding a tail
|
|
63
38
|
* window (which would redraw the whole card every frame). The full answer still lands at settle. */
|
|
@@ -88,8 +63,8 @@ async function finalize(api, target, preview, text, seq) {
|
|
|
88
63
|
// Settle failed (card expired / rejected) — fall through to delete + fresh send below.
|
|
89
64
|
}
|
|
90
65
|
if (settled) {
|
|
91
|
-
//
|
|
92
|
-
// repeating the quote on every chunk. sendText owns the same distinction for its own chunking.
|
|
66
|
+
// Threaded continuations must keep reply_in_thread; continuous top-level group replies intentionally
|
|
67
|
+
// avoid repeating the quote on every chunk. sendText owns the same distinction for its own chunking.
|
|
93
68
|
// A continuation failure propagates: the card is already authoritative, so deleting it and sending
|
|
94
69
|
// the full answer again would deterministically duplicate every continuation that already landed.
|
|
95
70
|
const continuationTarget = target.replyInThread ? target : { chatId: target.chatId };
|
|
@@ -174,35 +149,16 @@ export async function settleFeishuPreview(api, target, preview, text) {
|
|
|
174
149
|
* than recalling it and posting another reply.
|
|
175
150
|
*/
|
|
176
151
|
export async function streamFeishuReply(events, api, target, formatError, initialPreview, label = "[feishu]") {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let answer = "";
|
|
181
|
-
let answerPreviewSince;
|
|
182
|
-
const mark = { running: "…", ok: "✓", error: "✗" };
|
|
183
|
-
const toolView = () => tools.map((t) => `🔧 ${t.label} ${mark[t.status]}`).join("\n");
|
|
184
|
-
// Reasoning is process, not the answer: shown (capped to its tail) in the live preview only, never
|
|
185
|
-
// in the settled final card (which is `answer` alone).
|
|
186
|
-
const thinkingView = () => {
|
|
187
|
-
const t = thinking.replace(/\s+/g, " ").trim();
|
|
188
|
-
if (t === "")
|
|
189
|
-
return "";
|
|
190
|
-
return `💭 ${truncateCodePointSuffix(t, THINKING_PREVIEW)}`;
|
|
191
|
-
};
|
|
192
|
-
// The answer is hidden until its first delta has aged one STREAM_THROTTLE_MS: the pump's leading-edge
|
|
193
|
-
// flush would otherwise turn the very first content delta (often a lone character) into its own frame
|
|
194
|
-
// — the short-reply flicker. Aging is anchored at delta ARRIVAL (set in the event loop, not here) so
|
|
195
|
-
// an in-flight update can't skew the clock; a turn completing within the window settles directly.
|
|
196
|
-
const answerView = () => {
|
|
197
|
-
if (answer.trim() === "" || answerPreviewSince === undefined)
|
|
198
|
-
return "";
|
|
199
|
-
return Date.now() - answerPreviewSince >= STREAM_THROTTLE_MS ? answer : "";
|
|
200
|
-
};
|
|
152
|
+
// Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
|
|
153
|
+
// policy, the card-budget cap, and delivery below.
|
|
154
|
+
const turn = createTurnView();
|
|
201
155
|
const view = () => {
|
|
202
|
-
const v = [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
156
|
+
const v = composeTurnBody([
|
|
157
|
+
thinkingLine(turn, THINKING_PREVIEW),
|
|
158
|
+
toolLines(turn),
|
|
159
|
+
turn.retrying ? RETRY_NOTICE : "",
|
|
160
|
+
revealedAnswer(turn, STREAM_THROTTLE_MS),
|
|
161
|
+
]);
|
|
206
162
|
return capBytes(v === "" ? THINKING_PLACEHOLDER : v, CARD_MARKDOWN_MAX_BYTES);
|
|
207
163
|
};
|
|
208
164
|
// The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview
|
|
@@ -243,105 +199,31 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
243
199
|
throw e;
|
|
244
200
|
}
|
|
245
201
|
};
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
let previewErrLogged = false;
|
|
254
|
-
let pumpDone;
|
|
255
|
-
let wakeThrottle; // set while the pump is mid-throttle; finish() cuts it short
|
|
256
|
-
const runPump = async () => {
|
|
257
|
-
pumping = true;
|
|
258
|
-
try {
|
|
259
|
-
while (dirty && !stopped) {
|
|
260
|
-
dirty = false;
|
|
261
|
-
try {
|
|
262
|
-
await flushPreview();
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
// Best-effort preview (the final write is authoritative), but a failing update must be visible —
|
|
266
|
-
// log once per turn so a never-rendering preview is diagnosable, not silent.
|
|
267
|
-
if (!previewErrLogged) {
|
|
268
|
-
previewErrLogged = true;
|
|
269
|
-
log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (dirty && !stopped) {
|
|
273
|
-
// Pace + coalesce a burst into one snapshot. Interruptible: finish() cuts this short so the
|
|
274
|
-
// final write is not delayed by up to STREAM_THROTTLE_MS after the turn completes.
|
|
275
|
-
await new Promise((resolve) => {
|
|
276
|
-
const t = setTimeout(resolve, STREAM_THROTTLE_MS);
|
|
277
|
-
wakeThrottle = () => {
|
|
278
|
-
clearTimeout(t);
|
|
279
|
-
resolve();
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
wakeThrottle = undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
pumping = false;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
// Mark the preview dirty and ensure the single writer is running (an update already in flight picks
|
|
291
|
-
// up the new state on its next loop). Synchronous — callers never await a network write.
|
|
292
|
-
const touch = () => {
|
|
293
|
-
dirty = true;
|
|
294
|
-
if (!pumping)
|
|
295
|
-
pumpDone = runPump();
|
|
296
|
-
};
|
|
202
|
+
// The shared single-writer pump (preview-kit) serializes snapshots to the one preview — which also
|
|
203
|
+
// guarantees the card's strictly-increasing `sequence` lands in order (no concurrent frames).
|
|
204
|
+
const { touch, finish } = createPreviewPump({
|
|
205
|
+
flush: flushPreview,
|
|
206
|
+
throttleMs: STREAM_THROTTLE_MS,
|
|
207
|
+
onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
|
|
208
|
+
});
|
|
297
209
|
touch(); // mount the "💭 Thinking…" preview immediately
|
|
298
|
-
// Stop the pump and await any in-flight update, so the final write below is strictly the LAST one to
|
|
299
|
-
// the preview (no stale frame landing after the answer).
|
|
300
|
-
const finish = async () => {
|
|
301
|
-
stopped = true;
|
|
302
|
-
wakeThrottle?.(); // cut an in-flight throttle so the final write is not delayed up to STREAM_THROTTLE_MS
|
|
303
|
-
await pumpDone?.catch(() => { });
|
|
304
|
-
};
|
|
305
210
|
/** Terminal write, whatever tier the preview reached. */
|
|
306
211
|
const settle = async (text) => {
|
|
307
212
|
await finalize(api, target, preview, text, nextSeq);
|
|
308
213
|
};
|
|
309
214
|
try {
|
|
310
215
|
for await (const e of events) {
|
|
311
|
-
if (e.type === "
|
|
312
|
-
answer += e.delta;
|
|
313
|
-
if (answerPreviewSince === undefined && answer.trim() !== "")
|
|
314
|
-
answerPreviewSince = Date.now();
|
|
315
|
-
touch();
|
|
316
|
-
}
|
|
317
|
-
else if (e.type === "thinking") {
|
|
318
|
-
thinking += e.delta;
|
|
319
|
-
touch();
|
|
320
|
-
}
|
|
321
|
-
else if (e.type === "tool_started") {
|
|
322
|
-
const arg = summarizeArgs(e.args);
|
|
323
|
-
toolIndexById.set(e.id, tools.length);
|
|
324
|
-
tools.push({ label: arg ? `${e.name} ${arg}` : e.name, status: "running" });
|
|
325
|
-
touch();
|
|
326
|
-
}
|
|
327
|
-
else if (e.type === "tool_ended") {
|
|
328
|
-
const i = toolIndexById.get(e.id);
|
|
329
|
-
const t = i === undefined ? undefined : tools[i];
|
|
330
|
-
if (t)
|
|
331
|
-
t.status = e.isError ? "error" : "ok";
|
|
332
|
-
touch();
|
|
333
|
-
}
|
|
334
|
-
else if (e.type === "completed") {
|
|
216
|
+
if (e.type === "completed") {
|
|
335
217
|
await finish();
|
|
336
218
|
// Settle the preview into the final answer; the persisted card is the answer alone — the
|
|
337
219
|
// process (thinking/tools) was preview-only. Mark finalized BEFORE delivering: the terminal was
|
|
338
220
|
// reached, so a delivery failure here is a plain failure, not an "abnormal exit" (which would
|
|
339
221
|
// wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
|
|
340
222
|
finalized = true;
|
|
341
|
-
await settle(answer.trim() !== "" ? answer : "(no reply)");
|
|
223
|
+
await settle(turn.answer.trim() !== "" ? turn.answer : "(no reply)");
|
|
342
224
|
return;
|
|
343
225
|
}
|
|
344
|
-
|
|
226
|
+
if (e.type === "failed") {
|
|
345
227
|
await finish();
|
|
346
228
|
// Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
|
|
347
229
|
// log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
|
|
@@ -361,6 +243,8 @@ export async function streamFeishuReply(events, api, target, formatError, initia
|
|
|
361
243
|
}
|
|
362
244
|
throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
|
|
363
245
|
}
|
|
246
|
+
if (applyTurnEvent(turn, e))
|
|
247
|
+
touch();
|
|
364
248
|
}
|
|
365
249
|
throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
|
|
366
250
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* encoding (JSON → gzip → base64url) follow the official SDKs (provenance: node-sdk
|
|
22
22
|
* scene/registration); item names unknown to the platform catalog are silently dropped by the page.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
interface FeishuAppAddons {
|
|
25
25
|
scopes?: {
|
|
26
26
|
tenant?: string[];
|
|
27
27
|
user?: string[];
|
|
@@ -68,3 +68,4 @@ export interface RegisteredFeishuApp {
|
|
|
68
68
|
* abort, or a transport failure — every rejection is a plain Error whose message says what to do.
|
|
69
69
|
*/
|
|
70
70
|
export declare function registerFeishuApp(options: RegisterFeishuAppOptions): Promise<RegisteredFeishuApp>;
|
|
71
|
+
export {};
|
|
@@ -7,7 +7,7 @@ import { type FeishuCloudKind } from "./cloud.ts";
|
|
|
7
7
|
*
|
|
8
8
|
* Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
interface FeishuManualRegistration {
|
|
11
11
|
consoleUrl: string;
|
|
12
12
|
requestUrl: string;
|
|
13
13
|
}
|
|
@@ -20,3 +20,4 @@ export interface RegisterFeishuWebhookOptions {
|
|
|
20
20
|
onManualRegistration?: (info: FeishuManualRegistration) => void;
|
|
21
21
|
}
|
|
22
22
|
export declare function registerFeishuWebhook(baseUrl: string, kind: FeishuCloudKind, opts?: RegisterFeishuWebhookOptions): Promise<RegistrationOutcome>;
|
|
23
|
+
export {};
|
|
@@ -7,7 +7,9 @@ import { feishuChannel } from "@fastagent-sh/fastagent/feishu";
|
|
|
7
7
|
// 1. create a custom app → enable the BOT capability → copy App ID / App Secret into .env
|
|
8
8
|
// 2. Permissions: add `im:message.p2p_msg:readonly` (direct messages), `im:message.group_at_msg:readonly`
|
|
9
9
|
// (group @mentions), `im:message:send_as_bot` (reply), `im:resource` (attachments), and the
|
|
10
|
-
// card scope ("Create and update card" — the live preview streams through a card)
|
|
10
|
+
// card scope ("Create and update card" — the live preview streams through a card). To answer bare
|
|
11
|
+
// messages in Agent-managed threads and buffer other unsummoned group/thread context, also add the
|
|
12
|
+
// sensitive `im:message.group_msg` scope (tenant-admin approval) and publish a new version.
|
|
11
13
|
// 3. Events & Callbacks → subscribe to `im.message.receive_v1`; copy the Verification Token into
|
|
12
14
|
// .env; RECOMMENDED: set an Encrypt Key there and mirror it in FEISHU_ENCRYPT_KEY
|
|
13
15
|
// 4. the event Request URL (https://your.host/feishu) is registered AUTOMATICALLY by
|
|
@@ -21,14 +23,19 @@ export default feishuChannel({
|
|
|
21
23
|
appSecret: process.env.FEISHU_APP_SECRET ?? "",
|
|
22
24
|
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN ?? "", // authenticates inbound events
|
|
23
25
|
encryptKey: process.env.FEISHU_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
|
|
26
|
+
// Direct and group chats default to one Agent session/thread per top-level ask. Opt out independently:
|
|
27
|
+
// directMessageSession: "continuous",
|
|
28
|
+
// groupMessageSession: "continuous",
|
|
24
29
|
// Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
|
|
25
30
|
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
26
31
|
// full details always go to the server log regardless.
|
|
27
32
|
onError: (failed) => `⚠️ ${failed.details}`,
|
|
28
33
|
// The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
|
|
29
34
|
// the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultFeishuRoute:
|
|
30
|
-
// p2p chats always answer
|
|
31
|
-
//
|
|
35
|
+
// p2p chats always answer; groups answer on @this-bot, plus bare continuations in Agent-managed
|
|
36
|
+
// threads. Other human group/thread discussion buffers until that place's next answered turn;
|
|
37
|
+
// @other-only messages in managed threads buffer rather than triggering the Agent.
|
|
38
|
+
// Override to customise explicit routing, reusing the export:
|
|
32
39
|
// route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
|
|
33
40
|
// route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { text: `${feishuEnvelope(e)}\n[extra]` },
|
|
34
41
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Feishu/Lark app-level event subscription choice used by onboarding and scaffolding. */
|
|
2
|
+
export type FeishuSubscriptionMode = "webhook" | "websocket";
|
|
3
|
+
/** Onboarding choice for group visibility. `context` needs the tenant-wide group-message scope; the
|
|
4
|
+
* runtime remains capability-driven because the platform, not channel source, decides which events
|
|
5
|
+
* are delivered. */
|
|
6
|
+
export type FeishuGroupBehavior = "context" | "mentions";
|
|
7
|
+
/** The sensitive tenant scope behind both managed-thread bare replies and group context buffering. */
|
|
8
|
+
export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LongConnection } from "../../host/node.ts";
|
|
2
|
+
import type { FeishuCloudKind } from "./cloud.ts";
|
|
3
|
+
import type { FeishuMessageEvent } from "./parse.ts";
|
|
4
|
+
interface FeishuWsClient {
|
|
5
|
+
start(): Promise<void>;
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
interface FeishuWsClientCallbacks {
|
|
9
|
+
onReady(): void;
|
|
10
|
+
onError(error: Error): void;
|
|
11
|
+
onReconnecting(): void;
|
|
12
|
+
onReconnected(): void;
|
|
13
|
+
onEvent(event: FeishuMessageEvent): void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export type CreateFeishuWsClient = (callbacks: FeishuWsClientCallbacks) => FeishuWsClient | Promise<FeishuWsClient>;
|
|
16
|
+
export interface FeishuWsConnectionOptions {
|
|
17
|
+
kind: FeishuCloudKind;
|
|
18
|
+
appId: string;
|
|
19
|
+
appSecret: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
onEvent(event: FeishuMessageEvent): void | Promise<void>;
|
|
22
|
+
/** Internal SDK seam for deterministic tests. */
|
|
23
|
+
createClient?: CreateFeishuWsClient;
|
|
24
|
+
}
|
|
25
|
+
/** Open Feishu/Lark's official-SDK WebSocket connection. The SDK ACKs only after `onEvent` settles;
|
|
26
|
+
* a persistence throw therefore becomes a 500 response frame and the platform re-pushes the event. */
|
|
27
|
+
export declare function connectFeishuWs(options: FeishuWsConnectionOptions, signal: AbortSignal): LongConnection;
|
|
28
|
+
export {};
|