@nextclaw/channel-plugin-feishu 0.2.28 → 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.
- package/dist/index.d.ts +23 -0
- package/dist/index.js +45 -0
- package/dist/src/accounts.js +141 -0
- package/dist/src/app-scope-checker.js +36 -0
- package/dist/src/async.js +34 -0
- package/dist/src/auth-errors.js +72 -0
- package/dist/src/bitable.js +495 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +941 -0
- package/dist/src/calendar-calendar.js +54 -0
- package/dist/src/calendar-event-attendee.js +98 -0
- package/dist/src/calendar-event.js +193 -0
- package/dist/src/calendar-freebusy.js +40 -0
- package/dist/src/calendar-shared.js +23 -0
- package/dist/src/calendar.js +16 -0
- package/dist/src/card-action.js +49 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +413 -0
- package/dist/src/chat-schema.js +25 -0
- package/dist/src/chat.js +87 -0
- package/dist/src/client.d.ts +16 -0
- package/dist/src/client.js +112 -0
- package/dist/src/config-schema.d.ts +357 -0
- package/dist/src/dedup.js +126 -0
- package/dist/src/device-flow.js +109 -0
- package/dist/src/directory.js +101 -0
- package/dist/src/doc-schema.js +148 -0
- package/dist/src/docx-batch-insert.js +104 -0
- package/dist/src/docx-color-text.js +80 -0
- package/dist/src/docx-table-ops.js +197 -0
- package/dist/src/docx.js +858 -0
- package/dist/src/domains.js +14 -0
- package/dist/src/drive-schema.js +41 -0
- package/dist/src/drive.js +126 -0
- package/dist/src/dynamic-agent.js +93 -0
- package/dist/src/external-keys.js +13 -0
- package/dist/src/feishu-fetch.js +12 -0
- package/dist/src/identity.js +92 -0
- package/dist/src/lark-ticket.js +11 -0
- package/dist/src/media.d.ts +75 -0
- package/dist/src/media.js +304 -0
- package/dist/src/mention.d.ts +52 -0
- package/dist/src/mention.js +82 -0
- package/dist/src/monitor.account.d.ts +1 -0
- package/dist/src/monitor.account.js +393 -0
- package/dist/src/monitor.d.ts +11 -0
- package/dist/src/monitor.js +58 -0
- package/dist/src/monitor.startup.js +24 -0
- package/dist/src/monitor.state.d.ts +1 -0
- package/dist/src/monitor.state.js +80 -0
- package/dist/src/monitor.transport.js +167 -0
- package/dist/src/nextclaw-sdk/account-id.js +15 -0
- package/dist/src/nextclaw-sdk/core-channel.js +150 -0
- package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
- package/dist/src/nextclaw-sdk/dedupe.js +164 -0
- package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
- package/dist/src/nextclaw-sdk/feishu.js +14 -0
- package/dist/src/nextclaw-sdk/history.js +69 -0
- package/dist/src/nextclaw-sdk/network-body.js +180 -0
- package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
- package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
- package/dist/src/nextclaw-sdk/network.js +4 -0
- package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
- package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
- package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
- package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
- package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets.js +4 -0
- package/dist/src/nextclaw-sdk/types.d.ts +242 -0
- package/dist/src/oauth.js +171 -0
- package/dist/src/onboarding.js +381 -0
- package/dist/src/outbound.js +150 -0
- package/dist/src/perm-schema.js +49 -0
- package/dist/src/perm.js +90 -0
- package/dist/src/policy.js +61 -0
- package/dist/src/post.js +160 -0
- package/dist/src/probe.d.ts +11 -0
- package/dist/src/probe.js +85 -0
- package/dist/src/raw-request.js +24 -0
- package/dist/src/reactions.d.ts +67 -0
- package/dist/src/reactions.js +91 -0
- package/dist/src/reply-dispatcher.js +250 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/secret-input.js +3 -0
- package/dist/src/send-result.js +12 -0
- package/dist/src/send-target.js +22 -0
- package/dist/src/send.d.ts +51 -0
- package/dist/src/send.js +265 -0
- package/dist/src/sheets-shared.js +193 -0
- package/dist/src/sheets.js +95 -0
- package/dist/src/streaming-card.js +263 -0
- package/dist/src/targets.js +39 -0
- package/dist/src/task-comment.js +76 -0
- package/dist/src/task-shared.js +13 -0
- package/dist/src/task-subtask.js +79 -0
- package/dist/src/task-task.js +144 -0
- package/dist/src/task-tasklist.js +136 -0
- package/dist/src/task.js +16 -0
- package/dist/src/token-store.js +154 -0
- package/dist/src/tool-account.js +65 -0
- package/dist/src/tool-result.js +18 -0
- package/dist/src/tool-scopes.js +62 -0
- package/dist/src/tools-config.js +30 -0
- package/dist/src/types.d.ts +43 -0
- package/dist/src/typing.js +145 -0
- package/dist/src/uat-client.js +102 -0
- package/dist/src/user-tool-client.js +132 -0
- package/dist/src/user-tool-helpers.js +110 -0
- package/dist/src/user-tool-result.js +10 -0
- package/dist/src/wiki-schema.js +45 -0
- package/dist/src/wiki.js +144 -0
- package/package.json +8 -4
- package/src/media.ts +1 -1
- package/src/monitor.state.ts +2 -2
- 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 };
|
package/dist/src/post.js
ADDED
|
@@ -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 };
|