@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1

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 (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/index.ts +0 -75
@@ -0,0 +1,61 @@
1
+ import { evaluateSenderGroupAccessForPolicy } from "./nextclaw-sdk/core-channel.js";
2
+ import "./nextclaw-sdk/feishu.js";
3
+ import { normalizeFeishuTarget } from "./targets.js";
4
+ //#region src/policy.ts
5
+ function normalizeFeishuAllowEntry(raw) {
6
+ const trimmed = raw.trim();
7
+ if (!trimmed) return "";
8
+ if (trimmed === "*") return "*";
9
+ const withoutProviderPrefix = trimmed.replace(/^feishu:/i, "");
10
+ return (normalizeFeishuTarget(withoutProviderPrefix) ?? withoutProviderPrefix).trim().toLowerCase();
11
+ }
12
+ function resolveFeishuAllowlistMatch(params) {
13
+ const allowFrom = params.allowFrom.map((entry) => normalizeFeishuAllowEntry(String(entry))).filter(Boolean);
14
+ if (allowFrom.length === 0) return { allowed: false };
15
+ if (allowFrom.includes("*")) return {
16
+ allowed: true,
17
+ matchKey: "*",
18
+ matchSource: "wildcard"
19
+ };
20
+ const senderCandidates = [params.senderId, ...params.senderIds ?? []].map((entry) => normalizeFeishuAllowEntry(String(entry ?? ""))).filter(Boolean);
21
+ for (const senderId of senderCandidates) if (allowFrom.includes(senderId)) return {
22
+ allowed: true,
23
+ matchKey: senderId,
24
+ matchSource: "id"
25
+ };
26
+ return { allowed: false };
27
+ }
28
+ function resolveFeishuGroupConfig(params) {
29
+ const groups = params.cfg?.groups ?? {};
30
+ const wildcard = groups["*"];
31
+ const groupId = params.groupId?.trim();
32
+ if (!groupId) return;
33
+ const direct = groups[groupId];
34
+ if (direct) return direct;
35
+ const lowered = groupId.toLowerCase();
36
+ const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
37
+ if (matchKey) return groups[matchKey];
38
+ return wildcard;
39
+ }
40
+ function resolveFeishuGroupToolPolicy(params) {
41
+ const cfg = params.cfg.channels?.feishu;
42
+ if (!cfg) return;
43
+ return resolveFeishuGroupConfig({
44
+ cfg,
45
+ groupId: params.groupId
46
+ })?.tools;
47
+ }
48
+ function isFeishuGroupAllowed(params) {
49
+ return evaluateSenderGroupAccessForPolicy({
50
+ groupPolicy: params.groupPolicy === "allowall" ? "open" : params.groupPolicy,
51
+ groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
52
+ senderId: params.senderId,
53
+ isSenderAllowed: () => resolveFeishuAllowlistMatch(params).allowed
54
+ }).allowed;
55
+ }
56
+ function resolveFeishuReplyPolicy(params) {
57
+ if (params.isDirectMessage) return { requireMention: false };
58
+ return { requireMention: params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true };
59
+ }
60
+ //#endregion
61
+ export { isFeishuGroupAllowed, resolveFeishuAllowlistMatch, resolveFeishuGroupConfig, resolveFeishuGroupToolPolicy, resolveFeishuReplyPolicy };
@@ -0,0 +1,160 @@
1
+ import { normalizeFeishuExternalKey } from "./external-keys.js";
2
+ //#region src/post.ts
3
+ const FALLBACK_POST_TEXT = "[Rich text message]";
4
+ const MARKDOWN_SPECIAL_CHARS = /([\\`*_{}\[\]()#+\-!|>~])/g;
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null;
7
+ }
8
+ function toStringOrEmpty(value) {
9
+ return typeof value === "string" ? value : "";
10
+ }
11
+ function escapeMarkdownText(text) {
12
+ return text.replace(MARKDOWN_SPECIAL_CHARS, "\\$1");
13
+ }
14
+ function toBoolean(value) {
15
+ return value === true || value === 1 || value === "true";
16
+ }
17
+ function isStyleEnabled(style, key) {
18
+ if (!style) return false;
19
+ return toBoolean(style[key]);
20
+ }
21
+ function wrapInlineCode(text) {
22
+ const maxRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
23
+ const fence = "`".repeat(maxRun + 1);
24
+ return `${fence}${text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text}${fence}`;
25
+ }
26
+ function sanitizeFenceLanguage(language) {
27
+ return language.trim().replace(/[^A-Za-z0-9_+#.-]/g, "");
28
+ }
29
+ function renderTextElement(element) {
30
+ const text = toStringOrEmpty(element.text);
31
+ const style = isRecord(element.style) ? element.style : void 0;
32
+ if (isStyleEnabled(style, "code")) return wrapInlineCode(text);
33
+ let rendered = escapeMarkdownText(text);
34
+ if (!rendered) return "";
35
+ if (isStyleEnabled(style, "bold")) rendered = `**${rendered}**`;
36
+ if (isStyleEnabled(style, "italic")) rendered = `*${rendered}*`;
37
+ if (isStyleEnabled(style, "underline")) rendered = `<u>${rendered}</u>`;
38
+ if (isStyleEnabled(style, "strikethrough") || isStyleEnabled(style, "line_through") || isStyleEnabled(style, "lineThrough")) rendered = `~~${rendered}~~`;
39
+ return rendered;
40
+ }
41
+ function renderLinkElement(element) {
42
+ const href = toStringOrEmpty(element.href).trim();
43
+ const text = toStringOrEmpty(element.text) || href;
44
+ if (!text) return "";
45
+ if (!href) return escapeMarkdownText(text);
46
+ return `[${escapeMarkdownText(text)}](${href})`;
47
+ }
48
+ function renderMentionElement(element) {
49
+ const mention = toStringOrEmpty(element.user_name) || toStringOrEmpty(element.user_id) || toStringOrEmpty(element.open_id);
50
+ if (!mention) return "";
51
+ return `@${escapeMarkdownText(mention)}`;
52
+ }
53
+ function renderEmotionElement(element) {
54
+ return escapeMarkdownText(toStringOrEmpty(element.emoji) || toStringOrEmpty(element.text) || toStringOrEmpty(element.emoji_type));
55
+ }
56
+ function renderCodeBlockElement(element) {
57
+ const language = sanitizeFenceLanguage(toStringOrEmpty(element.language) || toStringOrEmpty(element.lang));
58
+ const code = (toStringOrEmpty(element.text) || toStringOrEmpty(element.content)).replace(/\r\n/g, "\n");
59
+ return `\`\`\`${language}\n${code}${code.endsWith("\n") ? "" : "\n"}\`\`\``;
60
+ }
61
+ function renderElement(element, imageKeys, mediaKeys, mentionedOpenIds) {
62
+ if (!isRecord(element)) return escapeMarkdownText(toStringOrEmpty(element));
63
+ switch (toStringOrEmpty(element.tag).toLowerCase()) {
64
+ case "text": return renderTextElement(element);
65
+ case "a": return renderLinkElement(element);
66
+ case "at":
67
+ {
68
+ const normalizedMention = normalizeFeishuExternalKey(toStringOrEmpty(element.open_id) || toStringOrEmpty(element.user_id));
69
+ if (normalizedMention) mentionedOpenIds.push(normalizedMention);
70
+ }
71
+ return renderMentionElement(element);
72
+ case "img": {
73
+ const imageKey = normalizeFeishuExternalKey(toStringOrEmpty(element.image_key));
74
+ if (imageKey) imageKeys.push(imageKey);
75
+ return "![image]";
76
+ }
77
+ case "media": {
78
+ const fileKey = normalizeFeishuExternalKey(toStringOrEmpty(element.file_key));
79
+ if (fileKey) {
80
+ const fileName = toStringOrEmpty(element.file_name) || void 0;
81
+ mediaKeys.push({
82
+ fileKey,
83
+ fileName
84
+ });
85
+ }
86
+ return "[media]";
87
+ }
88
+ case "emotion": return renderEmotionElement(element);
89
+ case "br": return "\n";
90
+ case "hr": return "\n\n---\n\n";
91
+ case "code": {
92
+ const code = toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
93
+ return code ? wrapInlineCode(code) : "";
94
+ }
95
+ case "code_block":
96
+ case "pre": return renderCodeBlockElement(element);
97
+ default: return escapeMarkdownText(toStringOrEmpty(element.text));
98
+ }
99
+ }
100
+ function toPostPayload(candidate) {
101
+ if (!isRecord(candidate) || !Array.isArray(candidate.content)) return null;
102
+ return {
103
+ title: toStringOrEmpty(candidate.title),
104
+ content: candidate.content
105
+ };
106
+ }
107
+ function resolveLocalePayload(candidate) {
108
+ const direct = toPostPayload(candidate);
109
+ if (direct) return direct;
110
+ if (!isRecord(candidate)) return null;
111
+ for (const value of Object.values(candidate)) {
112
+ const localePayload = toPostPayload(value);
113
+ if (localePayload) return localePayload;
114
+ }
115
+ return null;
116
+ }
117
+ function resolvePostPayload(parsed) {
118
+ const direct = toPostPayload(parsed);
119
+ if (direct) return direct;
120
+ if (!isRecord(parsed)) return null;
121
+ const wrappedPost = resolveLocalePayload(parsed.post);
122
+ if (wrappedPost) return wrappedPost;
123
+ return resolveLocalePayload(parsed);
124
+ }
125
+ function parsePostContent(content) {
126
+ try {
127
+ const payload = resolvePostPayload(JSON.parse(content));
128
+ if (!payload) return {
129
+ textContent: FALLBACK_POST_TEXT,
130
+ imageKeys: [],
131
+ mediaKeys: [],
132
+ mentionedOpenIds: []
133
+ };
134
+ const imageKeys = [];
135
+ const mediaKeys = [];
136
+ const mentionedOpenIds = [];
137
+ const paragraphs = [];
138
+ for (const paragraph of payload.content) {
139
+ if (!Array.isArray(paragraph)) continue;
140
+ let renderedParagraph = "";
141
+ for (const element of paragraph) renderedParagraph += renderElement(element, imageKeys, mediaKeys, mentionedOpenIds);
142
+ paragraphs.push(renderedParagraph);
143
+ }
144
+ return {
145
+ textContent: [escapeMarkdownText(payload.title.trim()), paragraphs.join("\n").trim()].filter(Boolean).join("\n\n").trim() || FALLBACK_POST_TEXT,
146
+ imageKeys,
147
+ mediaKeys,
148
+ mentionedOpenIds
149
+ };
150
+ } catch {
151
+ return {
152
+ textContent: FALLBACK_POST_TEXT,
153
+ imageKeys: [],
154
+ mediaKeys: [],
155
+ mentionedOpenIds: []
156
+ };
157
+ }
158
+ }
159
+ //#endregion
160
+ export { parsePostContent };
@@ -0,0 +1,11 @@
1
+ import { FeishuProbeResult } from "./types.js";
2
+ import { FeishuClientCredentials } from "./client.js";
3
+
4
+ //#region src/probe.d.ts
5
+ type ProbeFeishuOptions = {
6
+ timeoutMs?: number;
7
+ abortSignal?: AbortSignal;
8
+ };
9
+ declare function probeFeishu(creds?: FeishuClientCredentials, options?: ProbeFeishuOptions): Promise<FeishuProbeResult>;
10
+ //#endregion
11
+ export { probeFeishu };
@@ -0,0 +1,85 @@
1
+ import { createFeishuClient } from "./client.js";
2
+ import { raceWithTimeoutAndAbort } from "./async.js";
3
+ //#region src/probe.ts
4
+ /** Cache probe results to reduce repeated health-check calls.
5
+ * Gateway health checks call probeFeishu() every minute; without caching this
6
+ * burns ~43,200 calls/month, easily exceeding Feishu's free-tier quota.
7
+ * Successful bot info is effectively static, while failures are cached briefly
8
+ * to avoid hammering the API during transient outages. */
9
+ const probeCache = /* @__PURE__ */ new Map();
10
+ const PROBE_SUCCESS_TTL_MS = 600 * 1e3;
11
+ const PROBE_ERROR_TTL_MS = 60 * 1e3;
12
+ const MAX_PROBE_CACHE_SIZE = 64;
13
+ function setCachedProbeResult(cacheKey, result, ttlMs) {
14
+ probeCache.set(cacheKey, {
15
+ result,
16
+ expiresAt: Date.now() + ttlMs
17
+ });
18
+ if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
19
+ const oldest = probeCache.keys().next().value;
20
+ if (oldest !== void 0) probeCache.delete(oldest);
21
+ }
22
+ return result;
23
+ }
24
+ async function probeFeishu(creds, options = {}) {
25
+ if (!creds?.appId || !creds?.appSecret) return {
26
+ ok: false,
27
+ error: "missing credentials (appId, appSecret)"
28
+ };
29
+ if (options.abortSignal?.aborted) return {
30
+ ok: false,
31
+ appId: creds.appId,
32
+ error: "probe aborted"
33
+ };
34
+ const timeoutMs = options.timeoutMs ?? 1e4;
35
+ const cacheKey = creds.accountId ?? `${creds.appId}:${creds.appSecret.slice(0, 8)}`;
36
+ const cached = probeCache.get(cacheKey);
37
+ if (cached && cached.expiresAt > Date.now()) return cached.result;
38
+ try {
39
+ const responseResult = await raceWithTimeoutAndAbort(createFeishuClient(creds).request({
40
+ method: "GET",
41
+ url: "/open-apis/bot/v3/info",
42
+ data: {},
43
+ timeout: timeoutMs
44
+ }), {
45
+ timeoutMs,
46
+ abortSignal: options.abortSignal
47
+ });
48
+ if (responseResult.status === "aborted") return {
49
+ ok: false,
50
+ appId: creds.appId,
51
+ error: "probe aborted"
52
+ };
53
+ if (responseResult.status === "timeout") return setCachedProbeResult(cacheKey, {
54
+ ok: false,
55
+ appId: creds.appId,
56
+ error: `probe timed out after ${timeoutMs}ms`
57
+ }, PROBE_ERROR_TTL_MS);
58
+ const response = responseResult.value;
59
+ if (options.abortSignal?.aborted) return {
60
+ ok: false,
61
+ appId: creds.appId,
62
+ error: "probe aborted"
63
+ };
64
+ if (response.code !== 0) return setCachedProbeResult(cacheKey, {
65
+ ok: false,
66
+ appId: creds.appId,
67
+ error: `API error: ${response.msg || `code ${response.code}`}`
68
+ }, PROBE_ERROR_TTL_MS);
69
+ const bot = response.bot || response.data?.bot;
70
+ return setCachedProbeResult(cacheKey, {
71
+ ok: true,
72
+ appId: creds.appId,
73
+ botName: bot?.bot_name,
74
+ botOpenId: bot?.open_id
75
+ }, PROBE_SUCCESS_TTL_MS);
76
+ } catch (err) {
77
+ return setCachedProbeResult(cacheKey, {
78
+ ok: false,
79
+ appId: creds.appId,
80
+ error: err instanceof Error ? err.message : String(err)
81
+ }, PROBE_ERROR_TTL_MS);
82
+ }
83
+ }
84
+ //#endregion
85
+ export { probeFeishu };
@@ -0,0 +1,24 @@
1
+ import { resolveDomainUrl } from "./domains.js";
2
+ import { feishuFetch } from "./feishu-fetch.js";
3
+ //#region src/raw-request.ts
4
+ async function rawLarkRequest(options) {
5
+ const url = new URL(options.path, resolveDomainUrl(options.domain));
6
+ for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
7
+ const headers = { ...options.headers ?? {} };
8
+ if (options.accessToken) headers.Authorization = `Bearer ${options.accessToken}`;
9
+ if (options.body !== void 0 && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
10
+ const data = await (await feishuFetch(url.toString(), {
11
+ method: options.method ?? "GET",
12
+ headers,
13
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
14
+ })).json();
15
+ if (data.code !== void 0 && data.code !== 0) {
16
+ const error = new Error(data.msg ?? `Feishu API error: code=${data.code}`);
17
+ error.code = data.code;
18
+ error.msg = data.msg;
19
+ throw error;
20
+ }
21
+ return data;
22
+ }
23
+ //#endregion
24
+ export { rawLarkRequest };
@@ -0,0 +1,67 @@
1
+ import { ClawdbotConfig } from "./nextclaw-sdk/types.js";
2
+ //#region src/reactions.d.ts
3
+ type FeishuReaction = {
4
+ reactionId: string;
5
+ emojiType: string;
6
+ operatorType: "app" | "user";
7
+ operatorId: string;
8
+ };
9
+ /**
10
+ * Add a reaction (emoji) to a message.
11
+ * @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
12
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
13
+ */
14
+ declare function addReactionFeishu(params: {
15
+ cfg: ClawdbotConfig;
16
+ messageId: string;
17
+ emojiType: string;
18
+ accountId?: string;
19
+ }): Promise<{
20
+ reactionId: string;
21
+ }>;
22
+ /**
23
+ * Remove a reaction from a message.
24
+ */
25
+ declare function removeReactionFeishu(params: {
26
+ cfg: ClawdbotConfig;
27
+ messageId: string;
28
+ reactionId: string;
29
+ accountId?: string;
30
+ }): Promise<void>;
31
+ /**
32
+ * List all reactions for a message.
33
+ */
34
+ declare function listReactionsFeishu(params: {
35
+ cfg: ClawdbotConfig;
36
+ messageId: string;
37
+ emojiType?: string;
38
+ accountId?: string;
39
+ }): Promise<FeishuReaction[]>;
40
+ /**
41
+ * Common Feishu emoji types for convenience.
42
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
43
+ */
44
+ declare const FeishuEmoji: {
45
+ readonly THUMBSUP: "THUMBSUP";
46
+ readonly THUMBSDOWN: "THUMBSDOWN";
47
+ readonly HEART: "HEART";
48
+ readonly SMILE: "SMILE";
49
+ readonly GRINNING: "GRINNING";
50
+ readonly LAUGHING: "LAUGHING";
51
+ readonly CRY: "CRY";
52
+ readonly ANGRY: "ANGRY";
53
+ readonly SURPRISED: "SURPRISED";
54
+ readonly THINKING: "THINKING";
55
+ readonly CLAP: "CLAP";
56
+ readonly OK: "OK";
57
+ readonly FIST: "FIST";
58
+ readonly PRAY: "PRAY";
59
+ readonly FIRE: "FIRE";
60
+ readonly PARTY: "PARTY";
61
+ readonly CHECK: "CHECK";
62
+ readonly CROSS: "CROSS";
63
+ readonly QUESTION: "QUESTION";
64
+ readonly EXCLAMATION: "EXCLAMATION";
65
+ };
66
+ //#endregion
67
+ export { FeishuEmoji, addReactionFeishu, listReactionsFeishu, removeReactionFeishu };
@@ -0,0 +1,91 @@
1
+ import { resolveFeishuAccount } from "./accounts.js";
2
+ import { createFeishuClient } from "./client.js";
3
+ //#region src/reactions.ts
4
+ function resolveConfiguredFeishuClient(params) {
5
+ const account = resolveFeishuAccount(params);
6
+ if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
7
+ return createFeishuClient(account);
8
+ }
9
+ function assertFeishuReactionApiSuccess(response, action) {
10
+ if (response.code !== 0) throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
11
+ }
12
+ /**
13
+ * Add a reaction (emoji) to a message.
14
+ * @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
15
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
16
+ */
17
+ async function addReactionFeishu(params) {
18
+ const { cfg, messageId, emojiType, accountId } = params;
19
+ const response = await resolveConfiguredFeishuClient({
20
+ cfg,
21
+ accountId
22
+ }).im.messageReaction.create({
23
+ path: { message_id: messageId },
24
+ data: { reaction_type: { emoji_type: emojiType } }
25
+ });
26
+ assertFeishuReactionApiSuccess(response, "add reaction");
27
+ const reactionId = response.data?.reaction_id;
28
+ if (!reactionId) throw new Error("Feishu add reaction failed: no reaction_id returned");
29
+ return { reactionId };
30
+ }
31
+ /**
32
+ * Remove a reaction from a message.
33
+ */
34
+ async function removeReactionFeishu(params) {
35
+ const { cfg, messageId, reactionId, accountId } = params;
36
+ assertFeishuReactionApiSuccess(await resolveConfiguredFeishuClient({
37
+ cfg,
38
+ accountId
39
+ }).im.messageReaction.delete({ path: {
40
+ message_id: messageId,
41
+ reaction_id: reactionId
42
+ } }), "remove reaction");
43
+ }
44
+ /**
45
+ * List all reactions for a message.
46
+ */
47
+ async function listReactionsFeishu(params) {
48
+ const { cfg, messageId, emojiType, accountId } = params;
49
+ const response = await resolveConfiguredFeishuClient({
50
+ cfg,
51
+ accountId
52
+ }).im.messageReaction.list({
53
+ path: { message_id: messageId },
54
+ params: emojiType ? { reaction_type: emojiType } : void 0
55
+ });
56
+ assertFeishuReactionApiSuccess(response, "list reactions");
57
+ return (response.data?.items ?? []).map((item) => ({
58
+ reactionId: item.reaction_id ?? "",
59
+ emojiType: item.reaction_type?.emoji_type ?? "",
60
+ operatorType: item.operator_type === "app" ? "app" : "user",
61
+ operatorId: item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? ""
62
+ }));
63
+ }
64
+ /**
65
+ * Common Feishu emoji types for convenience.
66
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
67
+ */
68
+ const FeishuEmoji = {
69
+ THUMBSUP: "THUMBSUP",
70
+ THUMBSDOWN: "THUMBSDOWN",
71
+ HEART: "HEART",
72
+ SMILE: "SMILE",
73
+ GRINNING: "GRINNING",
74
+ LAUGHING: "LAUGHING",
75
+ CRY: "CRY",
76
+ ANGRY: "ANGRY",
77
+ SURPRISED: "SURPRISED",
78
+ THINKING: "THINKING",
79
+ CLAP: "CLAP",
80
+ OK: "OK",
81
+ FIST: "FIST",
82
+ PRAY: "PRAY",
83
+ FIRE: "FIRE",
84
+ PARTY: "PARTY",
85
+ CHECK: "CHECK",
86
+ CROSS: "CROSS",
87
+ QUESTION: "QUESTION",
88
+ EXCLAMATION: "EXCLAMATION"
89
+ };
90
+ //#endregion
91
+ export { FeishuEmoji, addReactionFeishu, listReactionsFeishu, removeReactionFeishu };