@gakr-gakr/feishu 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 (133) hide show
  1. package/api.ts +32 -0
  2. package/autobot.plugin.json +180 -0
  3. package/channel-entry.ts +20 -0
  4. package/channel-plugin-api.ts +1 -0
  5. package/contract-api.ts +16 -0
  6. package/index.ts +82 -0
  7. package/package.json +62 -0
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.ts +13 -0
  14. package/skills/feishu-doc/SKILL.md +211 -0
  15. package/skills/feishu-doc/references/block-types.md +103 -0
  16. package/skills/feishu-drive/SKILL.md +97 -0
  17. package/skills/feishu-perm/SKILL.md +119 -0
  18. package/skills/feishu-wiki/SKILL.md +113 -0
  19. package/src/accounts.ts +333 -0
  20. package/src/agent-config.ts +21 -0
  21. package/src/app-registration.ts +331 -0
  22. package/src/approval-auth.ts +25 -0
  23. package/src/async.ts +104 -0
  24. package/src/audio-preflight.runtime.ts +9 -0
  25. package/src/bitable.ts +762 -0
  26. package/src/bot-content.ts +485 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.ts +1703 -0
  30. package/src/card-action.ts +447 -0
  31. package/src/card-interaction.ts +159 -0
  32. package/src/card-test-helpers.ts +54 -0
  33. package/src/card-ux-approval.ts +65 -0
  34. package/src/card-ux-launcher.ts +121 -0
  35. package/src/card-ux-shared.ts +33 -0
  36. package/src/channel-runtime-api.ts +16 -0
  37. package/src/channel.runtime.ts +47 -0
  38. package/src/channel.ts +1423 -0
  39. package/src/chat-schema.ts +25 -0
  40. package/src/chat.ts +188 -0
  41. package/src/client-timeout.ts +42 -0
  42. package/src/client.ts +262 -0
  43. package/src/comment-dispatcher-runtime-api.ts +6 -0
  44. package/src/comment-dispatcher.ts +107 -0
  45. package/src/comment-handler-runtime-api.ts +3 -0
  46. package/src/comment-handler.ts +303 -0
  47. package/src/comment-reaction.ts +259 -0
  48. package/src/comment-shared.ts +406 -0
  49. package/src/comment-target.ts +44 -0
  50. package/src/config-schema.ts +335 -0
  51. package/src/conversation-id.ts +199 -0
  52. package/src/dedup-runtime-api.ts +1 -0
  53. package/src/dedup.ts +141 -0
  54. package/src/dedupe-key.ts +72 -0
  55. package/src/directory.static.ts +61 -0
  56. package/src/directory.ts +124 -0
  57. package/src/doc-schema.ts +182 -0
  58. package/src/docx-batch-insert.ts +223 -0
  59. package/src/docx-color-text.ts +154 -0
  60. package/src/docx-table-ops.ts +316 -0
  61. package/src/docx-types.ts +38 -0
  62. package/src/docx.ts +1596 -0
  63. package/src/drive-schema.ts +92 -0
  64. package/src/drive.ts +829 -0
  65. package/src/dynamic-agent.ts +143 -0
  66. package/src/event-types.ts +45 -0
  67. package/src/external-keys.ts +19 -0
  68. package/src/lifecycle.test-support.ts +220 -0
  69. package/src/media.ts +1105 -0
  70. package/src/mention-target.types.ts +5 -0
  71. package/src/mention.ts +114 -0
  72. package/src/message-action-contract.ts +13 -0
  73. package/src/monitor-state-runtime-api.ts +7 -0
  74. package/src/monitor-transport-runtime-api.ts +10 -0
  75. package/src/monitor.account.ts +492 -0
  76. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  77. package/src/monitor.bot-identity.ts +86 -0
  78. package/src/monitor.bot-menu-handler.ts +165 -0
  79. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  80. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  81. package/src/monitor.card-action.lifecycle.test-support.ts +421 -0
  82. package/src/monitor.comment-notice-handler.ts +105 -0
  83. package/src/monitor.comment.ts +1386 -0
  84. package/src/monitor.message-handler.ts +350 -0
  85. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  86. package/src/monitor.startup.ts +74 -0
  87. package/src/monitor.state.ts +170 -0
  88. package/src/monitor.synthetic-error.ts +18 -0
  89. package/src/monitor.test-mocks.ts +46 -0
  90. package/src/monitor.transport.ts +451 -0
  91. package/src/monitor.ts +100 -0
  92. package/src/outbound-runtime-api.ts +1 -0
  93. package/src/outbound.ts +785 -0
  94. package/src/perm-schema.ts +52 -0
  95. package/src/perm.ts +170 -0
  96. package/src/pins.ts +108 -0
  97. package/src/policy.ts +321 -0
  98. package/src/post.ts +275 -0
  99. package/src/probe.ts +166 -0
  100. package/src/processing-claims.ts +59 -0
  101. package/src/qr-terminal.ts +1 -0
  102. package/src/reactions.ts +123 -0
  103. package/src/reasoning-preview.ts +28 -0
  104. package/src/reply-dispatcher-runtime-api.ts +7 -0
  105. package/src/reply-dispatcher.ts +748 -0
  106. package/src/runtime.ts +9 -0
  107. package/src/secret-contract.ts +145 -0
  108. package/src/secret-input.ts +1 -0
  109. package/src/security-audit-shared.ts +69 -0
  110. package/src/security-audit.ts +1 -0
  111. package/src/send-result.ts +80 -0
  112. package/src/send-target.ts +35 -0
  113. package/src/send.ts +861 -0
  114. package/src/sequential-key.ts +28 -0
  115. package/src/sequential-queue.ts +86 -0
  116. package/src/session-conversation.ts +42 -0
  117. package/src/session-route.ts +48 -0
  118. package/src/setup-core.ts +51 -0
  119. package/src/setup-surface.ts +618 -0
  120. package/src/streaming-card.ts +571 -0
  121. package/src/subagent-hooks.ts +413 -0
  122. package/src/targets.ts +97 -0
  123. package/src/thread-bindings.ts +331 -0
  124. package/src/tool-account.ts +93 -0
  125. package/src/tool-factory-test-harness.ts +79 -0
  126. package/src/tool-result.ts +16 -0
  127. package/src/tools-config.ts +22 -0
  128. package/src/types.ts +106 -0
  129. package/src/typing.ts +214 -0
  130. package/src/wiki-schema.ts +69 -0
  131. package/src/wiki.ts +270 -0
  132. package/subagent-hooks-api.ts +31 -0
  133. package/tsconfig.json +16 -0
package/src/post.ts ADDED
@@ -0,0 +1,275 @@
1
+ import { normalizeLowercaseStringOrEmpty } from "autobot/plugin-sdk/string-coerce-runtime";
2
+ import { isRecord } from "./comment-shared.js";
3
+ import { normalizeFeishuExternalKey } from "./external-keys.js";
4
+
5
+ const FALLBACK_POST_TEXT = "[Rich text message]";
6
+ const MARKDOWN_SPECIAL_CHARS = /([\\`*_{}[\]()#+\-!|>~])/g;
7
+
8
+ type PostParseResult = {
9
+ textContent: string;
10
+ imageKeys: string[];
11
+ mediaKeys: Array<{ fileKey: string; fileName?: string }>;
12
+ mentionedOpenIds: string[];
13
+ };
14
+
15
+ type PostPayload = {
16
+ title: string;
17
+ content: unknown[];
18
+ };
19
+
20
+ function toStringOrEmpty(value: unknown): string {
21
+ return typeof value === "string" ? value : "";
22
+ }
23
+
24
+ function escapeMarkdownText(text: string): string {
25
+ return text.replace(MARKDOWN_SPECIAL_CHARS, "\\$1");
26
+ }
27
+
28
+ function toBoolean(value: unknown): boolean {
29
+ return value === true || value === 1 || value === "true";
30
+ }
31
+
32
+ function isStyleEnabled(style: Record<string, unknown> | undefined, key: string): boolean {
33
+ if (!style) {
34
+ return false;
35
+ }
36
+ return toBoolean(style[key]);
37
+ }
38
+
39
+ function wrapInlineCode(text: string): string {
40
+ const maxRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
41
+ const fence = "`".repeat(maxRun + 1);
42
+ const needsPadding = text.startsWith("`") || text.endsWith("`");
43
+ const body = needsPadding ? ` ${text} ` : text;
44
+ return `${fence}${body}${fence}`;
45
+ }
46
+
47
+ function sanitizeFenceLanguage(language: string): string {
48
+ return language.trim().replace(/[^A-Za-z0-9_+#.-]/g, "");
49
+ }
50
+
51
+ function renderTextElement(element: Record<string, unknown>): string {
52
+ const text = toStringOrEmpty(element.text);
53
+ const style = isRecord(element.style) ? element.style : undefined;
54
+
55
+ if (isStyleEnabled(style, "code")) {
56
+ return wrapInlineCode(text);
57
+ }
58
+
59
+ let rendered = escapeMarkdownText(text);
60
+ if (!rendered) {
61
+ return "";
62
+ }
63
+
64
+ if (isStyleEnabled(style, "bold")) {
65
+ rendered = `**${rendered}**`;
66
+ }
67
+ if (isStyleEnabled(style, "italic")) {
68
+ rendered = `*${rendered}*`;
69
+ }
70
+ if (isStyleEnabled(style, "underline")) {
71
+ rendered = `<u>${rendered}</u>`;
72
+ }
73
+ if (
74
+ isStyleEnabled(style, "strikethrough") ||
75
+ isStyleEnabled(style, "line_through") ||
76
+ isStyleEnabled(style, "lineThrough")
77
+ ) {
78
+ rendered = `~~${rendered}~~`;
79
+ }
80
+ return rendered;
81
+ }
82
+
83
+ function renderLinkElement(element: Record<string, unknown>): string {
84
+ const href = toStringOrEmpty(element.href).trim();
85
+ const rawText = toStringOrEmpty(element.text);
86
+ const text = rawText || href;
87
+ if (!text) {
88
+ return "";
89
+ }
90
+ if (!href) {
91
+ return escapeMarkdownText(text);
92
+ }
93
+ return `[${escapeMarkdownText(text)}](${href})`;
94
+ }
95
+
96
+ function renderMentionElement(element: Record<string, unknown>): string {
97
+ const mention =
98
+ toStringOrEmpty(element.user_name) ||
99
+ toStringOrEmpty(element.user_id) ||
100
+ toStringOrEmpty(element.open_id);
101
+ if (!mention) {
102
+ return "";
103
+ }
104
+ return `@${escapeMarkdownText(mention)}`;
105
+ }
106
+
107
+ function renderEmotionElement(element: Record<string, unknown>): string {
108
+ const text =
109
+ toStringOrEmpty(element.emoji) ||
110
+ toStringOrEmpty(element.text) ||
111
+ toStringOrEmpty(element.emoji_type);
112
+ return escapeMarkdownText(text);
113
+ }
114
+
115
+ function renderCodeBlockElement(element: Record<string, unknown>): string {
116
+ const language = sanitizeFenceLanguage(
117
+ toStringOrEmpty(element.language) || toStringOrEmpty(element.lang),
118
+ );
119
+ const code = (toStringOrEmpty(element.text) || toStringOrEmpty(element.content)).replace(
120
+ /\r\n/g,
121
+ "\n",
122
+ );
123
+ const trailingNewline = code.endsWith("\n") ? "" : "\n";
124
+ return `\`\`\`${language}\n${code}${trailingNewline}\`\`\``;
125
+ }
126
+
127
+ function renderElement(
128
+ element: unknown,
129
+ imageKeys: string[],
130
+ mediaKeys: Array<{ fileKey: string; fileName?: string }>,
131
+ mentionedOpenIds: string[],
132
+ ): string {
133
+ if (!isRecord(element)) {
134
+ return escapeMarkdownText(toStringOrEmpty(element));
135
+ }
136
+
137
+ const tag = normalizeLowercaseStringOrEmpty(toStringOrEmpty(element.tag));
138
+ switch (tag) {
139
+ case "text":
140
+ return renderTextElement(element);
141
+ case "a":
142
+ return renderLinkElement(element);
143
+ case "at":
144
+ {
145
+ const mentioned = toStringOrEmpty(element.open_id) || toStringOrEmpty(element.user_id);
146
+ const normalizedMention = normalizeFeishuExternalKey(mentioned);
147
+ if (normalizedMention) {
148
+ mentionedOpenIds.push(normalizedMention);
149
+ }
150
+ }
151
+ return renderMentionElement(element);
152
+ case "img": {
153
+ const imageKey = normalizeFeishuExternalKey(toStringOrEmpty(element.image_key));
154
+ if (imageKey) {
155
+ imageKeys.push(imageKey);
156
+ }
157
+ return "![image]";
158
+ }
159
+ case "media": {
160
+ const fileKey = normalizeFeishuExternalKey(toStringOrEmpty(element.file_key));
161
+ if (fileKey) {
162
+ const fileName = toStringOrEmpty(element.file_name) || undefined;
163
+ mediaKeys.push({ fileKey, fileName });
164
+ }
165
+ return "[media]";
166
+ }
167
+ case "emotion":
168
+ return renderEmotionElement(element);
169
+ case "md":
170
+ case "lark_md":
171
+ return toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
172
+ case "br":
173
+ return "\n";
174
+ case "hr":
175
+ return "\n\n---\n\n";
176
+ case "code": {
177
+ const code = toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
178
+ return code ? wrapInlineCode(code) : "";
179
+ }
180
+ case "code_block":
181
+ case "pre":
182
+ return renderCodeBlockElement(element);
183
+ default:
184
+ return escapeMarkdownText(toStringOrEmpty(element.text));
185
+ }
186
+ }
187
+
188
+ function toPostPayload(candidate: unknown): PostPayload | null {
189
+ if (!isRecord(candidate) || !Array.isArray(candidate.content)) {
190
+ return null;
191
+ }
192
+ return {
193
+ title: toStringOrEmpty(candidate.title),
194
+ content: candidate.content,
195
+ };
196
+ }
197
+
198
+ function resolveLocalePayload(candidate: unknown): PostPayload | null {
199
+ const direct = toPostPayload(candidate);
200
+ if (direct) {
201
+ return direct;
202
+ }
203
+ if (!isRecord(candidate)) {
204
+ return null;
205
+ }
206
+ for (const value of Object.values(candidate)) {
207
+ const localePayload = toPostPayload(value);
208
+ if (localePayload) {
209
+ return localePayload;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ function resolvePostPayload(parsed: unknown): PostPayload | null {
216
+ const direct = toPostPayload(parsed);
217
+ if (direct) {
218
+ return direct;
219
+ }
220
+
221
+ if (!isRecord(parsed)) {
222
+ return null;
223
+ }
224
+
225
+ const wrappedPost = resolveLocalePayload(parsed.post);
226
+ if (wrappedPost) {
227
+ return wrappedPost;
228
+ }
229
+
230
+ return resolveLocalePayload(parsed);
231
+ }
232
+
233
+ export function parsePostContent(content: string): PostParseResult {
234
+ try {
235
+ const parsed = JSON.parse(content);
236
+ const payload = resolvePostPayload(parsed);
237
+ if (!payload) {
238
+ return {
239
+ textContent: FALLBACK_POST_TEXT,
240
+ imageKeys: [],
241
+ mediaKeys: [],
242
+ mentionedOpenIds: [],
243
+ };
244
+ }
245
+
246
+ const imageKeys: string[] = [];
247
+ const mediaKeys: Array<{ fileKey: string; fileName?: string }> = [];
248
+ const mentionedOpenIds: string[] = [];
249
+ const paragraphs: string[] = [];
250
+
251
+ for (const paragraph of payload.content) {
252
+ if (!Array.isArray(paragraph)) {
253
+ continue;
254
+ }
255
+ let renderedParagraph = "";
256
+ for (const element of paragraph) {
257
+ renderedParagraph += renderElement(element, imageKeys, mediaKeys, mentionedOpenIds);
258
+ }
259
+ paragraphs.push(renderedParagraph);
260
+ }
261
+
262
+ const title = escapeMarkdownText(payload.title.trim());
263
+ const body = paragraphs.join("\n").trim();
264
+ const textContent = [title, body].filter(Boolean).join("\n\n").trim();
265
+
266
+ return {
267
+ textContent: textContent || FALLBACK_POST_TEXT,
268
+ imageKeys,
269
+ mediaKeys,
270
+ mentionedOpenIds,
271
+ };
272
+ } catch {
273
+ return { textContent: FALLBACK_POST_TEXT, imageKeys: [], mediaKeys: [], mentionedOpenIds: [] };
274
+ }
275
+ }
package/src/probe.ts ADDED
@@ -0,0 +1,166 @@
1
+ import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
2
+ import { raceWithTimeoutAndAbort } from "./async.js";
3
+ import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
4
+ import type { FeishuProbeResult } from "./types.js";
5
+
6
+ /** Cache probe results to reduce repeated health-check calls.
7
+ * Gateway health checks call probeFeishu() every minute; without caching this
8
+ * burns ~43,200 calls/month, easily exceeding Feishu's free-tier quota.
9
+ * Successful bot info is effectively static, while failures are cached briefly
10
+ * to avoid hammering the API during transient outages. */
11
+ const probeCache = new Map<string, { result: FeishuProbeResult; expiresAt: number }>();
12
+ const PROBE_SUCCESS_TTL_MS = 10 * 60 * 1000; // 10 minutes
13
+ const PROBE_ERROR_TTL_MS = 60 * 1000; // 1 minute
14
+ const MAX_PROBE_CACHE_SIZE = 64;
15
+ export const FEISHU_PROBE_REQUEST_TIMEOUT_MS = 10_000;
16
+ export type ProbeFeishuOptions = {
17
+ timeoutMs?: number;
18
+ abortSignal?: AbortSignal;
19
+ };
20
+
21
+ type FeishuPingResponse = {
22
+ code: number;
23
+ msg?: string;
24
+ data?: { pingBotInfo?: { botID?: string; botName?: string } };
25
+ };
26
+
27
+ type FeishuRequestClient = ReturnType<typeof createFeishuClient> & {
28
+ request(params: {
29
+ method: "POST";
30
+ url: string;
31
+ data: Record<string, unknown>;
32
+ timeout: number;
33
+ }): Promise<FeishuPingResponse>;
34
+ };
35
+
36
+ function setCachedProbeResult(
37
+ cacheKey: string,
38
+ result: FeishuProbeResult,
39
+ ttlMs: number,
40
+ ): FeishuProbeResult {
41
+ probeCache.set(cacheKey, { result, expiresAt: Date.now() + ttlMs });
42
+ if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
43
+ const oldest = probeCache.keys().next().value;
44
+ if (oldest !== undefined) {
45
+ probeCache.delete(oldest);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+
51
+ export async function probeFeishu(
52
+ creds?: FeishuClientCredentials,
53
+ options: ProbeFeishuOptions = {},
54
+ ): Promise<FeishuProbeResult> {
55
+ if (!creds?.appId || !creds?.appSecret) {
56
+ return {
57
+ ok: false,
58
+ error: "missing credentials (appId, appSecret)",
59
+ };
60
+ }
61
+ if (options.abortSignal?.aborted) {
62
+ return {
63
+ ok: false,
64
+ appId: creds.appId,
65
+ error: "probe aborted",
66
+ };
67
+ }
68
+
69
+ const timeoutMs = options.timeoutMs ?? FEISHU_PROBE_REQUEST_TIMEOUT_MS;
70
+
71
+ // Return cached result if still valid.
72
+ // Use accountId when available; otherwise include appSecret prefix so two
73
+ // accounts sharing the same appId (e.g. after secret rotation) don't
74
+ // pollute each other's cache entry.
75
+ const cacheKey = creds.accountId ?? `${creds.appId}:${creds.appSecret.slice(0, 8)}`;
76
+ const cached = probeCache.get(cacheKey);
77
+ if (cached && cached.expiresAt > Date.now()) {
78
+ return cached.result;
79
+ }
80
+
81
+ try {
82
+ const client = createFeishuClient(creds) as FeishuRequestClient;
83
+ // Feishu-provided endpoint for AutoBot, supported on both Feishu (CN)
84
+ // and Lark (international). No OAuth scopes required. Validates
85
+ // credentials and registers the app as an AI agent (智能体).
86
+ const responseResult = await raceWithTimeoutAndAbort<FeishuPingResponse>(
87
+ client.request({
88
+ method: "POST",
89
+ url: "/open-apis/bot/v1/autobot_bot/ping",
90
+ data: { needBotInfo: true },
91
+ timeout: timeoutMs,
92
+ }),
93
+ {
94
+ timeoutMs,
95
+ abortSignal: options.abortSignal,
96
+ },
97
+ );
98
+
99
+ if (responseResult.status === "aborted") {
100
+ return {
101
+ ok: false,
102
+ appId: creds.appId,
103
+ error: "probe aborted",
104
+ };
105
+ }
106
+ if (responseResult.status === "timeout") {
107
+ return setCachedProbeResult(
108
+ cacheKey,
109
+ {
110
+ ok: false,
111
+ appId: creds.appId,
112
+ error: `probe timed out after ${timeoutMs}ms`,
113
+ },
114
+ PROBE_ERROR_TTL_MS,
115
+ );
116
+ }
117
+
118
+ const response = responseResult.value;
119
+ if (options.abortSignal?.aborted) {
120
+ return {
121
+ ok: false,
122
+ appId: creds.appId,
123
+ error: "probe aborted",
124
+ };
125
+ }
126
+
127
+ if (response.code !== 0) {
128
+ return setCachedProbeResult(
129
+ cacheKey,
130
+ {
131
+ ok: false,
132
+ appId: creds.appId,
133
+ error: `API error: ${response.msg || `code ${response.code}`}`,
134
+ },
135
+ PROBE_ERROR_TTL_MS,
136
+ );
137
+ }
138
+
139
+ const botInfo = response.data?.pingBotInfo;
140
+ return setCachedProbeResult(
141
+ cacheKey,
142
+ {
143
+ ok: true,
144
+ appId: creds.appId,
145
+ botName: botInfo?.botName,
146
+ botOpenId: botInfo?.botID,
147
+ },
148
+ PROBE_SUCCESS_TTL_MS,
149
+ );
150
+ } catch (err) {
151
+ return setCachedProbeResult(
152
+ cacheKey,
153
+ {
154
+ ok: false,
155
+ appId: creds.appId,
156
+ error: formatErrorMessage(err),
157
+ },
158
+ PROBE_ERROR_TTL_MS,
159
+ );
160
+ }
161
+ }
162
+
163
+ /** Clear the probe cache (for testing). */
164
+ export function clearProbeCache(): void {
165
+ probeCache.clear();
166
+ }
@@ -0,0 +1,59 @@
1
+ const EVENT_DEDUP_TTL_MS = 5 * 60 * 1000;
2
+ const EVENT_MEMORY_MAX_SIZE = 2_000;
3
+
4
+ const processingClaims = new Map<string, number>();
5
+
6
+ function resolveEventDedupeKey(
7
+ namespace: string,
8
+ messageId: string | undefined | null,
9
+ ): string | null {
10
+ const trimmed = messageId?.trim();
11
+ return trimmed ? `${namespace}:${trimmed}` : null;
12
+ }
13
+
14
+ function pruneProcessingClaims(now: number): void {
15
+ const cutoff = now - EVENT_DEDUP_TTL_MS;
16
+ for (const [key, seenAt] of processingClaims) {
17
+ if (seenAt < cutoff) {
18
+ processingClaims.delete(key);
19
+ }
20
+ }
21
+ while (processingClaims.size > EVENT_MEMORY_MAX_SIZE) {
22
+ const oldestKey = processingClaims.keys().next().value;
23
+ if (!oldestKey) {
24
+ return;
25
+ }
26
+ processingClaims.delete(oldestKey);
27
+ }
28
+ }
29
+
30
+ export function tryBeginFeishuMessageProcessing(
31
+ messageId: string | undefined | null,
32
+ namespace = "global",
33
+ ): boolean {
34
+ const key = resolveEventDedupeKey(namespace, messageId);
35
+ if (!key) {
36
+ return true;
37
+ }
38
+ const now = Date.now();
39
+ pruneProcessingClaims(now);
40
+ if (processingClaims.has(key)) {
41
+ processingClaims.delete(key);
42
+ processingClaims.set(key, now);
43
+ pruneProcessingClaims(now);
44
+ return false;
45
+ }
46
+ processingClaims.set(key, now);
47
+ pruneProcessingClaims(now);
48
+ return true;
49
+ }
50
+
51
+ export function releaseFeishuMessageProcessing(
52
+ messageId: string | undefined | null,
53
+ namespace = "global",
54
+ ): void {
55
+ const key = resolveEventDedupeKey(namespace, messageId);
56
+ if (key) {
57
+ processingClaims.delete(key);
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export { renderQrTerminal } from "autobot/plugin-sdk/media-runtime";
@@ -0,0 +1,123 @@
1
+ import type { ClawdbotConfig } from "../runtime-api.js";
2
+ import { resolveFeishuRuntimeAccount } from "./accounts.js";
3
+ import { createFeishuClient } from "./client.js";
4
+
5
+ type FeishuReaction = {
6
+ reactionId: string;
7
+ emojiType: string;
8
+ operatorType: "app" | "user";
9
+ operatorId: string;
10
+ };
11
+
12
+ function resolveConfiguredFeishuClient(params: { cfg: ClawdbotConfig; accountId?: string }) {
13
+ const account = resolveFeishuRuntimeAccount(params);
14
+ if (!account.configured) {
15
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
16
+ }
17
+ return createFeishuClient(account);
18
+ }
19
+
20
+ function assertFeishuReactionApiSuccess(response: { code?: number; msg?: string }, action: string) {
21
+ if (response.code !== 0) {
22
+ throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Add a reaction (emoji) to a message.
28
+ * @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
29
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
30
+ */
31
+ export async function addReactionFeishu(params: {
32
+ cfg: ClawdbotConfig;
33
+ messageId: string;
34
+ emojiType: string;
35
+ accountId?: string;
36
+ }): Promise<{ reactionId: string }> {
37
+ const { cfg, messageId, emojiType, accountId } = params;
38
+ const client = resolveConfiguredFeishuClient({ cfg, accountId });
39
+
40
+ const response = (await client.im.messageReaction.create({
41
+ path: { message_id: messageId },
42
+ data: {
43
+ reaction_type: {
44
+ emoji_type: emojiType,
45
+ },
46
+ },
47
+ })) as {
48
+ code?: number;
49
+ msg?: string;
50
+ data?: { reaction_id?: string };
51
+ };
52
+
53
+ assertFeishuReactionApiSuccess(response, "add reaction");
54
+
55
+ const reactionId = response.data?.reaction_id;
56
+ if (!reactionId) {
57
+ throw new Error("Feishu add reaction failed: no reaction_id returned");
58
+ }
59
+
60
+ return { reactionId };
61
+ }
62
+
63
+ /**
64
+ * Remove a reaction from a message.
65
+ */
66
+ export async function removeReactionFeishu(params: {
67
+ cfg: ClawdbotConfig;
68
+ messageId: string;
69
+ reactionId: string;
70
+ accountId?: string;
71
+ }): Promise<void> {
72
+ const { cfg, messageId, reactionId, accountId } = params;
73
+ const client = resolveConfiguredFeishuClient({ cfg, accountId });
74
+
75
+ const response = (await client.im.messageReaction.delete({
76
+ path: {
77
+ message_id: messageId,
78
+ reaction_id: reactionId,
79
+ },
80
+ })) as { code?: number; msg?: string };
81
+
82
+ assertFeishuReactionApiSuccess(response, "remove reaction");
83
+ }
84
+
85
+ /**
86
+ * List all reactions for a message.
87
+ */
88
+ export async function listReactionsFeishu(params: {
89
+ cfg: ClawdbotConfig;
90
+ messageId: string;
91
+ emojiType?: string;
92
+ accountId?: string;
93
+ }): Promise<FeishuReaction[]> {
94
+ const { cfg, messageId, emojiType, accountId } = params;
95
+ const client = resolveConfiguredFeishuClient({ cfg, accountId });
96
+
97
+ const response = (await client.im.messageReaction.list({
98
+ path: { message_id: messageId },
99
+ params: emojiType ? { reaction_type: emojiType } : undefined,
100
+ })) as {
101
+ code?: number;
102
+ msg?: string;
103
+ data?: {
104
+ items?: Array<{
105
+ reaction_id?: string;
106
+ reaction_type?: { emoji_type?: string };
107
+ operator_type?: string;
108
+ operator_id?: { open_id?: string; user_id?: string; union_id?: string };
109
+ }>;
110
+ };
111
+ };
112
+
113
+ assertFeishuReactionApiSuccess(response, "list reactions");
114
+
115
+ const items = response.data?.items ?? [];
116
+ return items.map((item) => ({
117
+ reactionId: item.reaction_id ?? "",
118
+ emojiType: item.reaction_type?.emoji_type ?? "",
119
+ operatorType: item.operator_type === "app" ? "app" : "user",
120
+ operatorId:
121
+ item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
122
+ }));
123
+ }
@@ -0,0 +1,28 @@
1
+ import { resolveFeishuConfigReasoningDefault } from "./agent-config.js";
2
+ import { loadSessionStore, resolveSessionStoreEntry } from "./bot-runtime-api.js";
3
+ import type { ClawdbotConfig } from "./bot-runtime-api.js";
4
+
5
+ export function resolveFeishuReasoningPreviewEnabled(params: {
6
+ cfg: ClawdbotConfig;
7
+ agentId: string;
8
+ storePath: string;
9
+ sessionKey?: string;
10
+ }): boolean {
11
+ const configDefault = resolveFeishuConfigReasoningDefault(params.cfg, params.agentId);
12
+
13
+ if (!params.sessionKey) {
14
+ return configDefault === "stream";
15
+ }
16
+
17
+ try {
18
+ const store = loadSessionStore(params.storePath, { skipCache: true });
19
+ const level = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
20
+ ?.reasoningLevel;
21
+ if (level === "on" || level === "stream" || level === "off") {
22
+ return level === "stream";
23
+ }
24
+ } catch {
25
+ return false;
26
+ }
27
+ return configDefault === "stream";
28
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ createReplyPrefixContext,
3
+ type ClawdbotConfig,
4
+ type OutboundIdentity,
5
+ type ReplyPayload,
6
+ type RuntimeEnv,
7
+ } from "../runtime-api.js";