@bobotu/feishu-fork 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +922 -0
  3. package/index.ts +65 -0
  4. package/openclaw.plugin.json +10 -0
  5. package/package.json +72 -0
  6. package/skills/feishu-doc/SKILL.md +161 -0
  7. package/skills/feishu-doc/references/block-types.md +102 -0
  8. package/skills/feishu-drive/SKILL.md +96 -0
  9. package/skills/feishu-perm/SKILL.md +90 -0
  10. package/skills/feishu-task/SKILL.md +210 -0
  11. package/skills/feishu-wiki/SKILL.md +96 -0
  12. package/src/accounts.ts +140 -0
  13. package/src/bitable-tools/actions.ts +199 -0
  14. package/src/bitable-tools/common.ts +90 -0
  15. package/src/bitable-tools/index.ts +1 -0
  16. package/src/bitable-tools/meta.ts +80 -0
  17. package/src/bitable-tools/register.ts +195 -0
  18. package/src/bitable-tools/schemas.ts +221 -0
  19. package/src/bot.ts +1125 -0
  20. package/src/channel.ts +334 -0
  21. package/src/client.ts +114 -0
  22. package/src/config-schema.ts +237 -0
  23. package/src/dedup.ts +54 -0
  24. package/src/directory.ts +165 -0
  25. package/src/doc-tools/actions.ts +341 -0
  26. package/src/doc-tools/common.ts +33 -0
  27. package/src/doc-tools/index.ts +2 -0
  28. package/src/doc-tools/register.ts +90 -0
  29. package/src/doc-tools/schemas.ts +85 -0
  30. package/src/doc-write-service.ts +711 -0
  31. package/src/drive-tools/actions.ts +182 -0
  32. package/src/drive-tools/common.ts +18 -0
  33. package/src/drive-tools/index.ts +2 -0
  34. package/src/drive-tools/register.ts +71 -0
  35. package/src/drive-tools/schemas.ts +67 -0
  36. package/src/dynamic-agent.ts +135 -0
  37. package/src/external-keys.ts +19 -0
  38. package/src/media.ts +510 -0
  39. package/src/mention.ts +121 -0
  40. package/src/monitor.ts +323 -0
  41. package/src/onboarding.ts +449 -0
  42. package/src/outbound.ts +40 -0
  43. package/src/perm-tools/actions.ts +111 -0
  44. package/src/perm-tools/common.ts +18 -0
  45. package/src/perm-tools/index.ts +2 -0
  46. package/src/perm-tools/register.ts +65 -0
  47. package/src/perm-tools/schemas.ts +52 -0
  48. package/src/policy.ts +117 -0
  49. package/src/probe.ts +147 -0
  50. package/src/reactions.ts +160 -0
  51. package/src/reply-dispatcher.ts +240 -0
  52. package/src/runtime.ts +14 -0
  53. package/src/send.ts +391 -0
  54. package/src/streaming-card.ts +211 -0
  55. package/src/targets.ts +58 -0
  56. package/src/task-tools/actions.ts +590 -0
  57. package/src/task-tools/common.ts +18 -0
  58. package/src/task-tools/constants.ts +13 -0
  59. package/src/task-tools/index.ts +1 -0
  60. package/src/task-tools/register.ts +263 -0
  61. package/src/task-tools/schemas.ts +567 -0
  62. package/src/text/markdown-links.ts +104 -0
  63. package/src/tools-common/feishu-api.ts +184 -0
  64. package/src/tools-common/tool-context.ts +23 -0
  65. package/src/tools-common/tool-exec.ts +73 -0
  66. package/src/tools-config.ts +22 -0
  67. package/src/types.ts +79 -0
  68. package/src/typing.ts +75 -0
  69. package/src/wiki-tools/actions.ts +166 -0
  70. package/src/wiki-tools/common.ts +18 -0
  71. package/src/wiki-tools/index.ts +2 -0
  72. package/src/wiki-tools/register.ts +66 -0
  73. package/src/wiki-tools/schemas.ts +55 -0
package/src/monitor.ts ADDED
@@ -0,0 +1,323 @@
1
+ import * as Lark from "@larksuiteoapi/node-sdk";
2
+ import * as http from "http";
3
+ import {
4
+ type ClawdbotConfig,
5
+ type RuntimeEnv,
6
+ type HistoryEntry,
7
+ installRequestBodyLimitGuard,
8
+ } from "openclaw/plugin-sdk";
9
+ import type { ResolvedFeishuAccount } from "./types.js";
10
+ import { createFeishuWSClient, createEventDispatcher } from "./client.js";
11
+ import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
12
+ import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
13
+ import { probeFeishu } from "./probe.js";
14
+
15
+ export type MonitorFeishuOpts = {
16
+ config?: ClawdbotConfig;
17
+ runtime?: RuntimeEnv;
18
+ abortSignal?: AbortSignal;
19
+ accountId?: string;
20
+ };
21
+
22
+ // Per-account WebSocket clients, HTTP servers, and bot info
23
+ const wsClients = new Map<string, Lark.WSClient>();
24
+ const httpServers = new Map<string, http.Server>();
25
+ const botOpenIds = new Map<string, string>();
26
+ const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
27
+ const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
28
+
29
+ async function fetchBotOpenId(
30
+ account: ResolvedFeishuAccount,
31
+ ): Promise<string | undefined> {
32
+ try {
33
+ const result = await probeFeishu(account);
34
+ return result.ok ? result.botOpenId : undefined;
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Register common event handlers on an EventDispatcher.
42
+ * When fireAndForget is true (webhook mode), message handling is not awaited
43
+ * to avoid blocking the HTTP response (Lark requires <3s response).
44
+ */
45
+ function registerEventHandlers(
46
+ eventDispatcher: Lark.EventDispatcher,
47
+ context: {
48
+ cfg: ClawdbotConfig;
49
+ accountId: string;
50
+ runtime?: RuntimeEnv;
51
+ chatHistories: Map<string, HistoryEntry[]>;
52
+ fireAndForget?: boolean;
53
+ },
54
+ ) {
55
+ const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
56
+ const log = runtime?.log ?? console.log;
57
+ const error = runtime?.error ?? console.error;
58
+
59
+ eventDispatcher.register({
60
+ "im.message.receive_v1": async (data) => {
61
+ try {
62
+ const event = data as unknown as FeishuMessageEvent;
63
+ const promise = handleFeishuMessage({
64
+ cfg,
65
+ event,
66
+ botOpenId: botOpenIds.get(accountId),
67
+ runtime,
68
+ chatHistories,
69
+ accountId,
70
+ });
71
+ if (fireAndForget) {
72
+ promise.catch((err) => {
73
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
74
+ });
75
+ } else {
76
+ await promise;
77
+ }
78
+ } catch (err) {
79
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
80
+ }
81
+ },
82
+ "im.message.message_read_v1": async () => {
83
+ // Ignore read receipts
84
+ },
85
+ "im.chat.member.bot.added_v1": async (data) => {
86
+ try {
87
+ const event = data as unknown as FeishuBotAddedEvent;
88
+ log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
89
+ } catch (err) {
90
+ error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
91
+ }
92
+ },
93
+ "im.chat.member.bot.deleted_v1": async (data) => {
94
+ try {
95
+ const event = data as unknown as { chat_id: string };
96
+ log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
97
+ } catch (err) {
98
+ error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
99
+ }
100
+ },
101
+ });
102
+ }
103
+
104
+ type MonitorAccountParams = {
105
+ cfg: ClawdbotConfig;
106
+ account: ResolvedFeishuAccount;
107
+ runtime?: RuntimeEnv;
108
+ abortSignal?: AbortSignal;
109
+ };
110
+
111
+ /**
112
+ * Monitor a single Feishu account.
113
+ */
114
+ async function monitorSingleAccount(params: MonitorAccountParams): Promise<void> {
115
+ const { cfg, account, runtime, abortSignal } = params;
116
+ const { accountId } = account;
117
+ const log = runtime?.log ?? console.log;
118
+
119
+ // Fetch bot open_id
120
+ const botOpenId = await fetchBotOpenId(account);
121
+ botOpenIds.set(accountId, botOpenId ?? "");
122
+ log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
123
+
124
+ const connectionMode = account.config.connectionMode ?? "websocket";
125
+ const eventDispatcher = createEventDispatcher(account);
126
+ const chatHistories = new Map<string, HistoryEntry[]>();
127
+
128
+ registerEventHandlers(eventDispatcher, {
129
+ cfg,
130
+ accountId,
131
+ runtime,
132
+ chatHistories,
133
+ fireAndForget: connectionMode === "webhook",
134
+ });
135
+
136
+ if (connectionMode === "webhook") {
137
+ return monitorWebhook({ params, accountId, eventDispatcher });
138
+ }
139
+
140
+ return monitorWebSocket({ params, accountId, eventDispatcher });
141
+ }
142
+
143
+ type ConnectionParams = {
144
+ params: MonitorAccountParams;
145
+ accountId: string;
146
+ eventDispatcher: Lark.EventDispatcher;
147
+ };
148
+
149
+ async function monitorWebSocket({ params, accountId, eventDispatcher }: ConnectionParams): Promise<void> {
150
+ const { account, runtime, abortSignal } = params;
151
+ const log = runtime?.log ?? console.log;
152
+ const error = runtime?.error ?? console.error;
153
+
154
+ log(`feishu[${accountId}]: starting WebSocket connection...`);
155
+
156
+ const wsClient = createFeishuWSClient(account);
157
+ wsClients.set(accountId, wsClient);
158
+
159
+ return new Promise((resolve, reject) => {
160
+ const cleanup = () => {
161
+ wsClients.delete(accountId);
162
+ botOpenIds.delete(accountId);
163
+ };
164
+
165
+ const handleAbort = () => {
166
+ log(`feishu[${accountId}]: abort signal received, stopping`);
167
+ cleanup();
168
+ resolve();
169
+ };
170
+
171
+ if (abortSignal?.aborted) {
172
+ cleanup();
173
+ resolve();
174
+ return;
175
+ }
176
+
177
+ abortSignal?.addEventListener("abort", handleAbort, { once: true });
178
+
179
+ try {
180
+ wsClient.start({ eventDispatcher });
181
+ log(`feishu[${accountId}]: WebSocket client started`);
182
+ } catch (err) {
183
+ cleanup();
184
+ abortSignal?.removeEventListener("abort", handleAbort);
185
+ reject(err);
186
+ }
187
+ });
188
+ }
189
+
190
+ async function monitorWebhook({ params, accountId, eventDispatcher }: ConnectionParams): Promise<void> {
191
+ const { account, runtime, abortSignal } = params;
192
+ const log = runtime?.log ?? console.log;
193
+ const error = runtime?.error ?? console.error;
194
+
195
+ const port = account.config.webhookPort ?? 3000;
196
+ const path = account.config.webhookPath ?? "/feishu/events";
197
+
198
+ log(`feishu[${accountId}]: starting Webhook server on port ${port}, path ${path}...`);
199
+
200
+ const server = http.createServer();
201
+ const webhookHandler = Lark.adaptDefault(path, eventDispatcher, { autoChallenge: true });
202
+ server.on("request", (req, res) => {
203
+ const guard = installRequestBodyLimitGuard(req, res, {
204
+ maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
205
+ timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
206
+ responseFormat: "text",
207
+ });
208
+ if (guard.isTripped()) {
209
+ return;
210
+ }
211
+
212
+ void Promise.resolve(webhookHandler(req, res))
213
+ .catch((err) => {
214
+ if (!guard.isTripped()) {
215
+ error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
216
+ }
217
+ })
218
+ .finally(() => {
219
+ guard.dispose();
220
+ });
221
+ });
222
+ httpServers.set(accountId, server);
223
+
224
+ return new Promise((resolve, reject) => {
225
+ const cleanup = () => {
226
+ server.close();
227
+ httpServers.delete(accountId);
228
+ botOpenIds.delete(accountId);
229
+ };
230
+
231
+ const handleAbort = () => {
232
+ log(`feishu[${accountId}]: abort signal received, stopping Webhook server`);
233
+ cleanup();
234
+ resolve();
235
+ };
236
+
237
+ if (abortSignal?.aborted) {
238
+ cleanup();
239
+ resolve();
240
+ return;
241
+ }
242
+
243
+ abortSignal?.addEventListener("abort", handleAbort, { once: true });
244
+
245
+ server.listen(port, () => {
246
+ log(`feishu[${accountId}]: Webhook server listening on port ${port}`);
247
+ });
248
+
249
+ server.on("error", (err) => {
250
+ error(`feishu[${accountId}]: Webhook server error: ${err}`);
251
+ abortSignal?.removeEventListener("abort", handleAbort);
252
+ reject(err);
253
+ });
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Main entry: start monitoring for all enabled accounts.
259
+ */
260
+ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
261
+ const cfg = opts.config;
262
+ if (!cfg) {
263
+ throw new Error("Config is required for Feishu monitor");
264
+ }
265
+
266
+ const log = opts.runtime?.log ?? console.log;
267
+
268
+ // If accountId is specified, only monitor that account
269
+ if (opts.accountId) {
270
+ const account = resolveFeishuAccount({ cfg, accountId: opts.accountId });
271
+ if (!account.enabled || !account.configured) {
272
+ throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
273
+ }
274
+ return monitorSingleAccount({
275
+ cfg,
276
+ account,
277
+ runtime: opts.runtime,
278
+ abortSignal: opts.abortSignal,
279
+ });
280
+ }
281
+
282
+ // Otherwise, start all enabled accounts
283
+ const accounts = listEnabledFeishuAccounts(cfg);
284
+ if (accounts.length === 0) {
285
+ throw new Error("No enabled Feishu accounts configured");
286
+ }
287
+
288
+ log(`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
289
+
290
+ // Start all accounts in parallel
291
+ await Promise.all(
292
+ accounts.map((account) =>
293
+ monitorSingleAccount({
294
+ cfg,
295
+ account,
296
+ runtime: opts.runtime,
297
+ abortSignal: opts.abortSignal,
298
+ }),
299
+ ),
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Stop monitoring for a specific account or all accounts.
305
+ */
306
+ export function stopFeishuMonitor(accountId?: string): void {
307
+ if (accountId) {
308
+ wsClients.delete(accountId);
309
+ const server = httpServers.get(accountId);
310
+ if (server) {
311
+ server.close();
312
+ httpServers.delete(accountId);
313
+ }
314
+ botOpenIds.delete(accountId);
315
+ } else {
316
+ wsClients.clear();
317
+ for (const server of httpServers.values()) {
318
+ server.close();
319
+ }
320
+ httpServers.clear();
321
+ botOpenIds.clear();
322
+ }
323
+ }