@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.2
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/index.ts +0 -75
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createReplyPrefixContext, createTypingCallbacks, logTypingFailure } from "./nextclaw-sdk/core-pairing.js";
|
|
2
|
+
import "./nextclaw-sdk/feishu.js";
|
|
3
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
|
+
import { createFeishuClient } from "./client.js";
|
|
5
|
+
import { resolveReceiveIdType } from "./targets.js";
|
|
6
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
7
|
+
import { sendMediaFeishu } from "./media.js";
|
|
8
|
+
import { buildMentionedCardContent } from "./mention.js";
|
|
9
|
+
import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
|
|
10
|
+
import { FeishuStreamingSession, mergeStreamingText } from "./streaming-card.js";
|
|
11
|
+
import { addTypingIndicator, removeTypingIndicator } from "./typing.js";
|
|
12
|
+
//#region src/reply-dispatcher.ts
|
|
13
|
+
/** Detect if text contains markdown elements that benefit from card rendering */
|
|
14
|
+
function shouldUseCard(text) {
|
|
15
|
+
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
|
|
16
|
+
}
|
|
17
|
+
/** Maximum age (ms) for a message to receive a typing indicator reaction.
|
|
18
|
+
* Messages older than this are likely replays after context compaction (#30418). */
|
|
19
|
+
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
|
|
20
|
+
const MS_EPOCH_MIN = 0xe8d4a51000;
|
|
21
|
+
function normalizeEpochMs(timestamp) {
|
|
22
|
+
if (!Number.isFinite(timestamp) || timestamp === void 0 || timestamp <= 0) return;
|
|
23
|
+
return timestamp < MS_EPOCH_MIN ? timestamp * 1e3 : timestamp;
|
|
24
|
+
}
|
|
25
|
+
function createFeishuReplyDispatcher(params) {
|
|
26
|
+
const core = getFeishuRuntime();
|
|
27
|
+
const { cfg, agentId, chatId, replyToMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, mentionTargets, accountId } = params;
|
|
28
|
+
const sendReplyToMessageId = skipReplyToInMessages ? void 0 : replyToMessageId;
|
|
29
|
+
const threadReplyMode = threadReply === true;
|
|
30
|
+
const effectiveReplyInThread = threadReplyMode ? true : replyInThread;
|
|
31
|
+
const account = resolveFeishuAccount({
|
|
32
|
+
cfg,
|
|
33
|
+
accountId
|
|
34
|
+
});
|
|
35
|
+
const prefixContext = createReplyPrefixContext({
|
|
36
|
+
cfg,
|
|
37
|
+
agentId
|
|
38
|
+
});
|
|
39
|
+
let typingState = null;
|
|
40
|
+
const typingCallbacks = createTypingCallbacks({
|
|
41
|
+
start: async () => {
|
|
42
|
+
if (!(account.config.typingIndicator ?? true)) return;
|
|
43
|
+
if (!replyToMessageId) return;
|
|
44
|
+
const messageCreateTimeMs = normalizeEpochMs(params.messageCreateTimeMs);
|
|
45
|
+
if (messageCreateTimeMs !== void 0 && Date.now() - messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS) return;
|
|
46
|
+
if (typingState?.reactionId) return;
|
|
47
|
+
typingState = await addTypingIndicator({
|
|
48
|
+
cfg,
|
|
49
|
+
messageId: replyToMessageId,
|
|
50
|
+
accountId,
|
|
51
|
+
runtime: params.runtime
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
stop: async () => {
|
|
55
|
+
if (!typingState) return;
|
|
56
|
+
await removeTypingIndicator({
|
|
57
|
+
cfg,
|
|
58
|
+
state: typingState,
|
|
59
|
+
accountId,
|
|
60
|
+
runtime: params.runtime
|
|
61
|
+
});
|
|
62
|
+
typingState = null;
|
|
63
|
+
},
|
|
64
|
+
onStartError: (err) => logTypingFailure({
|
|
65
|
+
log: (message) => params.runtime.log?.(message),
|
|
66
|
+
channel: "feishu",
|
|
67
|
+
action: "start",
|
|
68
|
+
error: err
|
|
69
|
+
}),
|
|
70
|
+
onStopError: (err) => logTypingFailure({
|
|
71
|
+
log: (message) => params.runtime.log?.(message),
|
|
72
|
+
channel: "feishu",
|
|
73
|
+
action: "stop",
|
|
74
|
+
error: err
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "feishu", accountId, { fallbackLimit: 4e3 });
|
|
78
|
+
const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu");
|
|
79
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
80
|
+
cfg,
|
|
81
|
+
channel: "feishu"
|
|
82
|
+
});
|
|
83
|
+
const renderMode = account.config?.renderMode ?? "auto";
|
|
84
|
+
const streamingEnabled = !threadReplyMode && account.config?.streaming !== false && renderMode !== "raw";
|
|
85
|
+
let streaming = null;
|
|
86
|
+
let streamText = "";
|
|
87
|
+
let lastPartial = "";
|
|
88
|
+
const deliveredFinalTexts = /* @__PURE__ */ new Set();
|
|
89
|
+
let partialUpdateQueue = Promise.resolve();
|
|
90
|
+
let streamingStartPromise = null;
|
|
91
|
+
const queueStreamingUpdate = (nextText, options) => {
|
|
92
|
+
if (!nextText) return;
|
|
93
|
+
if (options?.dedupeWithLastPartial && nextText === lastPartial) return;
|
|
94
|
+
if (options?.dedupeWithLastPartial) lastPartial = nextText;
|
|
95
|
+
streamText = (options?.mode ?? "snapshot") === "delta" ? `${streamText}${nextText}` : mergeStreamingText(streamText, nextText);
|
|
96
|
+
partialUpdateQueue = partialUpdateQueue.then(async () => {
|
|
97
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
98
|
+
if (streaming?.isActive()) await streaming.update(streamText);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const startStreaming = () => {
|
|
102
|
+
if (!streamingEnabled || streamingStartPromise || streaming) return;
|
|
103
|
+
streamingStartPromise = (async () => {
|
|
104
|
+
const creds = account.appId && account.appSecret ? {
|
|
105
|
+
appId: account.appId,
|
|
106
|
+
appSecret: account.appSecret,
|
|
107
|
+
domain: account.domain
|
|
108
|
+
} : null;
|
|
109
|
+
if (!creds) return;
|
|
110
|
+
streaming = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
|
|
111
|
+
try {
|
|
112
|
+
await streaming.start(chatId, resolveReceiveIdType(chatId), {
|
|
113
|
+
replyToMessageId,
|
|
114
|
+
replyInThread: effectiveReplyInThread,
|
|
115
|
+
rootId
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
params.runtime.error?.(`feishu: streaming start failed: ${String(error)}`);
|
|
119
|
+
streaming = null;
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
};
|
|
123
|
+
const closeStreaming = async () => {
|
|
124
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
125
|
+
await partialUpdateQueue;
|
|
126
|
+
if (streaming?.isActive()) {
|
|
127
|
+
let text = streamText;
|
|
128
|
+
if (mentionTargets?.length) text = buildMentionedCardContent(mentionTargets, text);
|
|
129
|
+
await streaming.close(text);
|
|
130
|
+
}
|
|
131
|
+
streaming = null;
|
|
132
|
+
streamingStartPromise = null;
|
|
133
|
+
streamText = "";
|
|
134
|
+
lastPartial = "";
|
|
135
|
+
};
|
|
136
|
+
const sendChunkedTextReply = async (params) => {
|
|
137
|
+
let first = true;
|
|
138
|
+
const chunkSource = params.useCard ? params.text : core.channel.text.convertMarkdownTables(params.text, tableMode);
|
|
139
|
+
for (const chunk of core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode)) {
|
|
140
|
+
const message = {
|
|
141
|
+
cfg,
|
|
142
|
+
to: chatId,
|
|
143
|
+
text: chunk,
|
|
144
|
+
replyToMessageId: sendReplyToMessageId,
|
|
145
|
+
replyInThread: effectiveReplyInThread,
|
|
146
|
+
mentions: first ? mentionTargets : void 0,
|
|
147
|
+
accountId
|
|
148
|
+
};
|
|
149
|
+
if (params.useCard) await sendMarkdownCardFeishu(message);
|
|
150
|
+
else await sendMessageFeishu(message);
|
|
151
|
+
first = false;
|
|
152
|
+
}
|
|
153
|
+
if (params.infoKind === "final") deliveredFinalTexts.add(params.text);
|
|
154
|
+
};
|
|
155
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
156
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
157
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
158
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
159
|
+
onReplyStart: () => {
|
|
160
|
+
deliveredFinalTexts.clear();
|
|
161
|
+
typingCallbacks.onReplyStart?.();
|
|
162
|
+
},
|
|
163
|
+
deliver: async (payload, info) => {
|
|
164
|
+
const text = payload.text ?? "";
|
|
165
|
+
const mediaList = payload.mediaUrls && payload.mediaUrls.length > 0 ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
166
|
+
const hasText = Boolean(text.trim());
|
|
167
|
+
const hasMedia = mediaList.length > 0;
|
|
168
|
+
const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
|
169
|
+
const shouldDeliverText = hasText && !skipTextForDuplicateFinal;
|
|
170
|
+
if (!shouldDeliverText && !hasMedia) return;
|
|
171
|
+
if (shouldDeliverText) {
|
|
172
|
+
const useCard = renderMode === "card" || renderMode === "auto" && shouldUseCard(text);
|
|
173
|
+
if (info?.kind === "block") {
|
|
174
|
+
if (!(streamingEnabled && useCard)) return;
|
|
175
|
+
startStreaming();
|
|
176
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
177
|
+
}
|
|
178
|
+
if (info?.kind === "final" && streamingEnabled && useCard) {
|
|
179
|
+
startStreaming();
|
|
180
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
181
|
+
}
|
|
182
|
+
if (streaming?.isActive()) {
|
|
183
|
+
if (info?.kind === "block") queueStreamingUpdate(text, { mode: "delta" });
|
|
184
|
+
if (info?.kind === "final") {
|
|
185
|
+
streamText = mergeStreamingText(streamText, text);
|
|
186
|
+
await closeStreaming();
|
|
187
|
+
deliveredFinalTexts.add(text);
|
|
188
|
+
}
|
|
189
|
+
if (hasMedia) for (const mediaUrl of mediaList) await sendMediaFeishu({
|
|
190
|
+
cfg,
|
|
191
|
+
to: chatId,
|
|
192
|
+
mediaUrl,
|
|
193
|
+
replyToMessageId: sendReplyToMessageId,
|
|
194
|
+
replyInThread: effectiveReplyInThread,
|
|
195
|
+
accountId
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (useCard) await sendChunkedTextReply({
|
|
200
|
+
text,
|
|
201
|
+
useCard: true,
|
|
202
|
+
infoKind: info?.kind
|
|
203
|
+
});
|
|
204
|
+
else await sendChunkedTextReply({
|
|
205
|
+
text,
|
|
206
|
+
useCard: false,
|
|
207
|
+
infoKind: info?.kind
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (hasMedia) for (const mediaUrl of mediaList) await sendMediaFeishu({
|
|
211
|
+
cfg,
|
|
212
|
+
to: chatId,
|
|
213
|
+
mediaUrl,
|
|
214
|
+
replyToMessageId: sendReplyToMessageId,
|
|
215
|
+
replyInThread: effectiveReplyInThread,
|
|
216
|
+
accountId
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
onError: async (error, info) => {
|
|
220
|
+
params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
|
|
221
|
+
await closeStreaming();
|
|
222
|
+
typingCallbacks.onIdle?.();
|
|
223
|
+
},
|
|
224
|
+
onIdle: async () => {
|
|
225
|
+
await closeStreaming();
|
|
226
|
+
typingCallbacks.onIdle?.();
|
|
227
|
+
},
|
|
228
|
+
onCleanup: () => {
|
|
229
|
+
typingCallbacks.onCleanup?.();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
dispatcher,
|
|
234
|
+
replyOptions: {
|
|
235
|
+
...replyOptions,
|
|
236
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
237
|
+
disableBlockStreaming: true,
|
|
238
|
+
onPartialReply: streamingEnabled ? (payload) => {
|
|
239
|
+
if (!payload.text) return;
|
|
240
|
+
queueStreamingUpdate(payload.text, {
|
|
241
|
+
dedupeWithLastPartial: true,
|
|
242
|
+
mode: "snapshot"
|
|
243
|
+
});
|
|
244
|
+
} : void 0
|
|
245
|
+
},
|
|
246
|
+
markDispatchIdle
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
export { createFeishuReplyDispatcher };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "./nextclaw-sdk/runtime-store.js";
|
|
2
|
+
//#region src/runtime.ts
|
|
3
|
+
const { setRuntime: setFeishuRuntime, getRuntime: getFeishuRuntime } = createPluginRuntimeStore("Feishu runtime not initialized");
|
|
4
|
+
//#endregion
|
|
5
|
+
export { getFeishuRuntime, setFeishuRuntime };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/send-result.ts
|
|
2
|
+
function assertFeishuMessageApiSuccess(response, errorPrefix) {
|
|
3
|
+
if (response.code !== 0) throw new Error(`${errorPrefix}: ${response.msg || `code ${response.code}`}`);
|
|
4
|
+
}
|
|
5
|
+
function toFeishuSendResult(response, chatId) {
|
|
6
|
+
return {
|
|
7
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
8
|
+
chatId
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { assertFeishuMessageApiSuccess, toFeishuSendResult };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
2
|
+
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
|
|
4
|
+
//#region src/send-target.ts
|
|
5
|
+
function resolveFeishuSendTarget(params) {
|
|
6
|
+
const target = params.to.trim();
|
|
7
|
+
const account = resolveFeishuAccount({
|
|
8
|
+
cfg: params.cfg,
|
|
9
|
+
accountId: params.accountId
|
|
10
|
+
});
|
|
11
|
+
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
12
|
+
const client = createFeishuClient(account);
|
|
13
|
+
const receiveId = normalizeFeishuTarget(target);
|
|
14
|
+
if (!receiveId) throw new Error(`Invalid Feishu target: ${params.to}`);
|
|
15
|
+
return {
|
|
16
|
+
client,
|
|
17
|
+
receiveId,
|
|
18
|
+
receiveIdType: resolveReceiveIdType(target.replace(/^(feishu|lark):/i, ""))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { resolveFeishuSendTarget };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ClawdbotConfig } from "./nextclaw-sdk/types.js";
|
|
2
|
+
import { MentionTarget } from "./mention.js";
|
|
3
|
+
import { FeishuMessageInfo, FeishuSendResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
//#region src/send.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Get a message by its ID.
|
|
8
|
+
* Useful for fetching quoted/replied message content.
|
|
9
|
+
*/
|
|
10
|
+
declare function getMessageFeishu(params: {
|
|
11
|
+
cfg: ClawdbotConfig;
|
|
12
|
+
messageId: string;
|
|
13
|
+
accountId?: string;
|
|
14
|
+
}): Promise<FeishuMessageInfo | null>;
|
|
15
|
+
type SendFeishuMessageParams = {
|
|
16
|
+
cfg: ClawdbotConfig;
|
|
17
|
+
to: string;
|
|
18
|
+
text: string;
|
|
19
|
+
replyToMessageId?: string; /** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
20
|
+
replyInThread?: boolean; /** Mention target users */
|
|
21
|
+
mentions?: MentionTarget[]; /** Account ID (optional, uses default if not specified) */
|
|
22
|
+
accountId?: string;
|
|
23
|
+
};
|
|
24
|
+
declare function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult>;
|
|
25
|
+
type SendFeishuCardParams = {
|
|
26
|
+
cfg: ClawdbotConfig;
|
|
27
|
+
to: string;
|
|
28
|
+
card: Record<string, unknown>;
|
|
29
|
+
replyToMessageId?: string; /** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
30
|
+
replyInThread?: boolean;
|
|
31
|
+
accountId?: string;
|
|
32
|
+
};
|
|
33
|
+
declare function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult>;
|
|
34
|
+
declare function updateCardFeishu(params: {
|
|
35
|
+
cfg: ClawdbotConfig;
|
|
36
|
+
messageId: string;
|
|
37
|
+
card: Record<string, unknown>;
|
|
38
|
+
accountId?: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Edit an existing text message.
|
|
42
|
+
* Note: Feishu only allows editing messages within 24 hours.
|
|
43
|
+
*/
|
|
44
|
+
declare function editMessageFeishu(params: {
|
|
45
|
+
cfg: ClawdbotConfig;
|
|
46
|
+
messageId: string;
|
|
47
|
+
text: string;
|
|
48
|
+
accountId?: string;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { editMessageFeishu, getMessageFeishu, sendCardFeishu, sendMessageFeishu, updateCardFeishu };
|
package/dist/src/send.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
2
|
+
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
4
|
+
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
5
|
+
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
6
|
+
import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
|
|
7
|
+
import { parsePostContent } from "./post.js";
|
|
8
|
+
//#region src/send.ts
|
|
9
|
+
const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
|
|
10
|
+
function shouldFallbackFromReplyTarget(response) {
|
|
11
|
+
if (response.code !== void 0 && WITHDRAWN_REPLY_ERROR_CODES.has(response.code)) return true;
|
|
12
|
+
const msg = response.msg?.toLowerCase() ?? "";
|
|
13
|
+
return msg.includes("withdrawn") || msg.includes("not found");
|
|
14
|
+
}
|
|
15
|
+
/** Check whether a thrown error indicates a withdrawn/not-found reply target. */
|
|
16
|
+
function isWithdrawnReplyError(err) {
|
|
17
|
+
if (typeof err !== "object" || err === null) return false;
|
|
18
|
+
const code = err.code;
|
|
19
|
+
if (typeof code === "number" && WITHDRAWN_REPLY_ERROR_CODES.has(code)) return true;
|
|
20
|
+
const response = err.response;
|
|
21
|
+
if (typeof response?.data?.code === "number" && WITHDRAWN_REPLY_ERROR_CODES.has(response.data.code)) return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
/** Send a direct message as a fallback when a reply target is unavailable. */
|
|
25
|
+
async function sendFallbackDirect(client, params, errorPrefix) {
|
|
26
|
+
const response = await client.im.message.create({
|
|
27
|
+
params: { receive_id_type: params.receiveIdType },
|
|
28
|
+
data: {
|
|
29
|
+
receive_id: params.receiveId,
|
|
30
|
+
content: params.content,
|
|
31
|
+
msg_type: params.msgType
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
assertFeishuMessageApiSuccess(response, errorPrefix);
|
|
35
|
+
return toFeishuSendResult(response, params.receiveId);
|
|
36
|
+
}
|
|
37
|
+
async function sendReplyOrFallbackDirect(client, params) {
|
|
38
|
+
if (!params.replyToMessageId) return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await client.im.message.reply({
|
|
42
|
+
path: { message_id: params.replyToMessageId },
|
|
43
|
+
data: {
|
|
44
|
+
content: params.content,
|
|
45
|
+
msg_type: params.msgType,
|
|
46
|
+
...params.replyInThread ? { reply_in_thread: true } : {}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (!isWithdrawnReplyError(err)) throw err;
|
|
51
|
+
return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
52
|
+
}
|
|
53
|
+
if (shouldFallbackFromReplyTarget(response)) return sendFallbackDirect(client, params.directParams, params.directErrorPrefix);
|
|
54
|
+
assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
|
|
55
|
+
return toFeishuSendResult(response, params.directParams.receiveId);
|
|
56
|
+
}
|
|
57
|
+
function parseInteractiveCardContent(parsed) {
|
|
58
|
+
if (!parsed || typeof parsed !== "object") return "[Interactive Card]";
|
|
59
|
+
const candidate = parsed;
|
|
60
|
+
if (!Array.isArray(candidate.elements)) return "[Interactive Card]";
|
|
61
|
+
const texts = [];
|
|
62
|
+
for (const element of candidate.elements) {
|
|
63
|
+
if (!element || typeof element !== "object") continue;
|
|
64
|
+
const item = element;
|
|
65
|
+
if (item.tag === "div" && typeof item.text?.content === "string") {
|
|
66
|
+
texts.push(item.text.content);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (item.tag === "markdown" && typeof item.content === "string") texts.push(item.content);
|
|
70
|
+
}
|
|
71
|
+
return texts.join("\n").trim() || "[Interactive Card]";
|
|
72
|
+
}
|
|
73
|
+
function parseQuotedMessageContent(rawContent, msgType) {
|
|
74
|
+
if (!rawContent) return "";
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(rawContent);
|
|
78
|
+
} catch {
|
|
79
|
+
return rawContent;
|
|
80
|
+
}
|
|
81
|
+
if (msgType === "text") {
|
|
82
|
+
const text = parsed?.text;
|
|
83
|
+
return typeof text === "string" ? text : "[Text message]";
|
|
84
|
+
}
|
|
85
|
+
if (msgType === "post") return parsePostContent(rawContent).textContent;
|
|
86
|
+
if (msgType === "interactive") return parseInteractiveCardContent(parsed);
|
|
87
|
+
if (typeof parsed === "string") return parsed;
|
|
88
|
+
const genericText = parsed?.text;
|
|
89
|
+
if (typeof genericText === "string" && genericText.trim()) return genericText;
|
|
90
|
+
const genericTitle = parsed?.title;
|
|
91
|
+
if (typeof genericTitle === "string" && genericTitle.trim()) return genericTitle;
|
|
92
|
+
return `[${msgType || "unknown"} message]`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get a message by its ID.
|
|
96
|
+
* Useful for fetching quoted/replied message content.
|
|
97
|
+
*/
|
|
98
|
+
async function getMessageFeishu(params) {
|
|
99
|
+
const { cfg, messageId, accountId } = params;
|
|
100
|
+
const account = resolveFeishuAccount({
|
|
101
|
+
cfg,
|
|
102
|
+
accountId
|
|
103
|
+
});
|
|
104
|
+
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
105
|
+
const client = createFeishuClient(account);
|
|
106
|
+
try {
|
|
107
|
+
const response = await client.im.message.get({ path: { message_id: messageId } });
|
|
108
|
+
if (response.code !== 0) return null;
|
|
109
|
+
const rawItem = response.data?.items?.[0] ?? response.data;
|
|
110
|
+
const item = rawItem && (rawItem.body !== void 0 || rawItem.message_id !== void 0) ? rawItem : null;
|
|
111
|
+
if (!item) return null;
|
|
112
|
+
const msgType = item.msg_type ?? "text";
|
|
113
|
+
const content = parseQuotedMessageContent(item.body?.content ?? "", msgType);
|
|
114
|
+
return {
|
|
115
|
+
messageId: item.message_id ?? messageId,
|
|
116
|
+
chatId: item.chat_id ?? "",
|
|
117
|
+
chatType: item.chat_type === "group" || item.chat_type === "private" || item.chat_type === "p2p" ? item.chat_type : void 0,
|
|
118
|
+
senderId: item.sender?.id,
|
|
119
|
+
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : void 0,
|
|
120
|
+
senderType: item.sender?.sender_type,
|
|
121
|
+
content,
|
|
122
|
+
contentType: msgType,
|
|
123
|
+
createTime: item.create_time ? parseInt(String(item.create_time), 10) : void 0
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function buildFeishuPostMessagePayload(params) {
|
|
130
|
+
const { messageText } = params;
|
|
131
|
+
return {
|
|
132
|
+
content: JSON.stringify({ zh_cn: { content: [[{
|
|
133
|
+
tag: "md",
|
|
134
|
+
text: messageText
|
|
135
|
+
}]] } }),
|
|
136
|
+
msgType: "post"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function sendMessageFeishu(params) {
|
|
140
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
141
|
+
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({
|
|
142
|
+
cfg,
|
|
143
|
+
to,
|
|
144
|
+
accountId
|
|
145
|
+
});
|
|
146
|
+
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
147
|
+
cfg,
|
|
148
|
+
channel: "feishu"
|
|
149
|
+
});
|
|
150
|
+
let rawText = text ?? "";
|
|
151
|
+
if (mentions && mentions.length > 0) rawText = buildMentionedMessage(mentions, rawText);
|
|
152
|
+
const { content, msgType } = buildFeishuPostMessagePayload({ messageText: getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode) });
|
|
153
|
+
return sendReplyOrFallbackDirect(client, {
|
|
154
|
+
replyToMessageId,
|
|
155
|
+
replyInThread,
|
|
156
|
+
content,
|
|
157
|
+
msgType,
|
|
158
|
+
directParams: {
|
|
159
|
+
receiveId,
|
|
160
|
+
receiveIdType,
|
|
161
|
+
content,
|
|
162
|
+
msgType
|
|
163
|
+
},
|
|
164
|
+
directErrorPrefix: "Feishu send failed",
|
|
165
|
+
replyErrorPrefix: "Feishu reply failed"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function sendCardFeishu(params) {
|
|
169
|
+
const { cfg, to, card, replyToMessageId, replyInThread, accountId } = params;
|
|
170
|
+
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({
|
|
171
|
+
cfg,
|
|
172
|
+
to,
|
|
173
|
+
accountId
|
|
174
|
+
});
|
|
175
|
+
const content = JSON.stringify(card);
|
|
176
|
+
return sendReplyOrFallbackDirect(client, {
|
|
177
|
+
replyToMessageId,
|
|
178
|
+
replyInThread,
|
|
179
|
+
content,
|
|
180
|
+
msgType: "interactive",
|
|
181
|
+
directParams: {
|
|
182
|
+
receiveId,
|
|
183
|
+
receiveIdType,
|
|
184
|
+
content,
|
|
185
|
+
msgType: "interactive"
|
|
186
|
+
},
|
|
187
|
+
directErrorPrefix: "Feishu card send failed",
|
|
188
|
+
replyErrorPrefix: "Feishu card reply failed"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function updateCardFeishu(params) {
|
|
192
|
+
const { cfg, messageId, card, accountId } = params;
|
|
193
|
+
const account = resolveFeishuAccount({
|
|
194
|
+
cfg,
|
|
195
|
+
accountId
|
|
196
|
+
});
|
|
197
|
+
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
198
|
+
const client = createFeishuClient(account);
|
|
199
|
+
const content = JSON.stringify(card);
|
|
200
|
+
const response = await client.im.message.patch({
|
|
201
|
+
path: { message_id: messageId },
|
|
202
|
+
data: { content }
|
|
203
|
+
});
|
|
204
|
+
if (response.code !== 0) throw new Error(`Feishu card update failed: ${response.msg || `code ${response.code}`}`);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build a Feishu interactive card with markdown content.
|
|
208
|
+
* Cards render markdown properly (code blocks, tables, links, etc.)
|
|
209
|
+
* Uses schema 2.0 format for proper markdown rendering.
|
|
210
|
+
*/
|
|
211
|
+
function buildMarkdownCard(text) {
|
|
212
|
+
return {
|
|
213
|
+
schema: "2.0",
|
|
214
|
+
config: { wide_screen_mode: true },
|
|
215
|
+
body: { elements: [{
|
|
216
|
+
tag: "markdown",
|
|
217
|
+
content: text
|
|
218
|
+
}] }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Send a message as a markdown card (interactive message).
|
|
223
|
+
* This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
|
|
224
|
+
*/
|
|
225
|
+
async function sendMarkdownCardFeishu(params) {
|
|
226
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
227
|
+
let cardText = text;
|
|
228
|
+
if (mentions && mentions.length > 0) cardText = buildMentionedCardContent(mentions, text);
|
|
229
|
+
return sendCardFeishu({
|
|
230
|
+
cfg,
|
|
231
|
+
to,
|
|
232
|
+
card: buildMarkdownCard(cardText),
|
|
233
|
+
replyToMessageId,
|
|
234
|
+
replyInThread,
|
|
235
|
+
accountId
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Edit an existing text message.
|
|
240
|
+
* Note: Feishu only allows editing messages within 24 hours.
|
|
241
|
+
*/
|
|
242
|
+
async function editMessageFeishu(params) {
|
|
243
|
+
const { cfg, messageId, text, accountId } = params;
|
|
244
|
+
const account = resolveFeishuAccount({
|
|
245
|
+
cfg,
|
|
246
|
+
accountId
|
|
247
|
+
});
|
|
248
|
+
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
249
|
+
const client = createFeishuClient(account);
|
|
250
|
+
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
251
|
+
cfg,
|
|
252
|
+
channel: "feishu"
|
|
253
|
+
});
|
|
254
|
+
const { content, msgType } = buildFeishuPostMessagePayload({ messageText: getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode) });
|
|
255
|
+
const response = await client.im.message.update({
|
|
256
|
+
path: { message_id: messageId },
|
|
257
|
+
data: {
|
|
258
|
+
msg_type: msgType,
|
|
259
|
+
content
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (response.code !== 0) throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { editMessageFeishu, getMessageFeishu, sendCardFeishu, sendMarkdownCardFeishu, sendMessageFeishu, updateCardFeishu };
|