@openclaw/feishu 2026.5.2 → 2026.5.3-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/accounts-Ba3-WP1z.js +423 -0
- package/dist/api.js +2280 -0
- package/dist/app-registration-B8qc1MCM.js +184 -0
- package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
- package/dist/card-interaction-BfRLgvw_.js +96 -0
- package/dist/channel-CSD_Jt8I.js +1668 -0
- package/dist/channel-entry.js +22 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-DYsXcD36.js +700 -0
- package/dist/client-DBVoQL5w.js +157 -0
- package/dist/contract-api.js +9 -0
- package/dist/conversation-id-DWS3Ep2A.js +139 -0
- package/dist/directory.static-f3EeoRJd.js +44 -0
- package/dist/drive-C5eJLJr7.js +883 -0
- package/dist/index.js +68 -0
- package/dist/monitor-CT189QfR.js +60 -0
- package/dist/monitor.account-dJV2jO8C.js +4990 -0
- package/dist/monitor.state-DYM02ipp.js +100 -0
- package/dist/policy-D6c-wMPl.js +118 -0
- package/dist/probe-BNzzU_uR.js +149 -0
- package/dist/rolldown-runtime-DUslC3ob.js +14 -0
- package/dist/runtime-CG0DuRCy.js +8 -0
- package/dist/runtime-api.js +14 -0
- package/dist/secret-contract-Dm4Z_zQN.js +119 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/security-audit-DqJdocrN.js +11 -0
- package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-DowxxbpH.js +1218 -0
- package/dist/session-conversation-B4nrW-vo.js +27 -0
- package/dist/session-key-api.js +2 -0
- package/dist/setup-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/subagent-hooks-C3UhPVLV.js +227 -0
- package/dist/subagent-hooks-api.js +23 -0
- package/dist/targets-JMFJRKSe.js +48 -0
- package/dist/thread-bindings-BmS6TLes.js +222 -0
- package/package.json +15 -6
- package/api.ts +0 -31
- package/channel-entry.ts +0 -20
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -16
- package/index.ts +0 -82
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/security-contract-api.ts +0 -1
- package/session-key-api.ts +0 -1
- package/setup-api.ts +0 -3
- package/setup-entry.test.ts +0 -14
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -459
- package/src/accounts.ts +0 -326
- package/src/app-registration.ts +0 -331
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -25
- package/src/async.test.ts +0 -35
- package/src/async.ts +0 -104
- package/src/audio-preflight.runtime.ts +0 -9
- package/src/bitable.test.ts +0 -131
- package/src/bitable.ts +0 -762
- package/src/bot-content.ts +0 -474
- package/src/bot-group-name.test.ts +0 -108
- package/src/bot-runtime-api.ts +0 -12
- package/src/bot-sender-name.ts +0 -125
- package/src/bot.broadcast.test.ts +0 -463
- package/src/bot.card-action.test.ts +0 -577
- package/src/bot.checkBotMentioned.test.ts +0 -265
- package/src/bot.helpers.test.ts +0 -118
- package/src/bot.stripBotMention.test.ts +0 -126
- package/src/bot.test.ts +0 -3040
- package/src/bot.ts +0 -1559
- package/src/card-action.ts +0 -447
- package/src/card-interaction.test.ts +0 -129
- package/src/card-interaction.ts +0 -159
- package/src/card-test-helpers.ts +0 -47
- package/src/card-ux-approval.ts +0 -65
- package/src/card-ux-launcher.test.ts +0 -99
- package/src/card-ux-launcher.ts +0 -121
- package/src/card-ux-shared.ts +0 -33
- package/src/channel-runtime-api.ts +0 -16
- package/src/channel.runtime.ts +0 -47
- package/src/channel.test.ts +0 -959
- package/src/channel.ts +0 -1313
- package/src/chat-schema.ts +0 -25
- package/src/chat.test.ts +0 -196
- package/src/chat.ts +0 -188
- package/src/client.test.ts +0 -433
- package/src/client.ts +0 -290
- package/src/comment-dispatcher-runtime-api.ts +0 -6
- package/src/comment-dispatcher.test.ts +0 -169
- package/src/comment-dispatcher.ts +0 -107
- package/src/comment-handler-runtime-api.ts +0 -3
- package/src/comment-handler.test.ts +0 -486
- package/src/comment-handler.ts +0 -309
- package/src/comment-reaction.test.ts +0 -166
- package/src/comment-reaction.ts +0 -259
- package/src/comment-shared.test.ts +0 -182
- package/src/comment-shared.ts +0 -406
- package/src/comment-target.ts +0 -44
- package/src/config-schema.test.ts +0 -309
- package/src/config-schema.ts +0 -333
- package/src/conversation-id.test.ts +0 -18
- package/src/conversation-id.ts +0 -199
- package/src/dedup-runtime-api.ts +0 -1
- package/src/dedup.ts +0 -141
- package/src/directory.static.ts +0 -61
- package/src/directory.test.ts +0 -136
- package/src/directory.ts +0 -124
- package/src/doc-schema.ts +0 -182
- package/src/docx-batch-insert.test.ts +0 -91
- package/src/docx-batch-insert.ts +0 -223
- package/src/docx-color-text.ts +0 -154
- package/src/docx-table-ops.test.ts +0 -53
- package/src/docx-table-ops.ts +0 -316
- package/src/docx-types.ts +0 -38
- package/src/docx.account-selection.test.ts +0 -79
- package/src/docx.test.ts +0 -685
- package/src/docx.ts +0 -1616
- package/src/drive-schema.ts +0 -92
- package/src/drive.test.ts +0 -1219
- package/src/drive.ts +0 -829
- package/src/dynamic-agent.ts +0 -137
- package/src/event-types.ts +0 -45
- package/src/external-keys.test.ts +0 -20
- package/src/external-keys.ts +0 -19
- package/src/lifecycle.test-support.ts +0 -220
- package/src/media.test.ts +0 -900
- package/src/media.ts +0 -861
- package/src/mention-target.types.ts +0 -5
- package/src/mention.ts +0 -114
- package/src/message-action-contract.ts +0 -13
- package/src/monitor-state-runtime-api.ts +0 -7
- package/src/monitor-transport-runtime-api.ts +0 -7
- package/src/monitor.account.ts +0 -468
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
- package/src/monitor.bot-identity.ts +0 -86
- package/src/monitor.bot-menu-handler.ts +0 -165
- package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
- package/src/monitor.bot-menu.test.ts +0 -178
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
- package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
- package/src/monitor.cleanup.test.ts +0 -376
- package/src/monitor.comment-notice-handler.ts +0 -105
- package/src/monitor.comment.test.ts +0 -937
- package/src/monitor.comment.ts +0 -1386
- package/src/monitor.lifecycle.test.ts +0 -4
- package/src/monitor.message-handler.ts +0 -339
- package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
- package/src/monitor.reaction.test.ts +0 -713
- package/src/monitor.startup.test.ts +0 -192
- package/src/monitor.startup.ts +0 -74
- package/src/monitor.state.defaults.test.ts +0 -46
- package/src/monitor.state.ts +0 -170
- package/src/monitor.synthetic-error.ts +0 -18
- package/src/monitor.test-mocks.ts +0 -45
- package/src/monitor.transport.ts +0 -424
- package/src/monitor.ts +0 -100
- package/src/monitor.webhook-e2e.test.ts +0 -272
- package/src/monitor.webhook-security.test.ts +0 -264
- package/src/monitor.webhook.test-helpers.ts +0 -116
- package/src/outbound-runtime-api.ts +0 -1
- package/src/outbound.test.ts +0 -935
- package/src/outbound.ts +0 -718
- package/src/perm-schema.ts +0 -52
- package/src/perm.ts +0 -170
- package/src/pins.ts +0 -108
- package/src/policy.test.ts +0 -334
- package/src/policy.ts +0 -236
- package/src/post.test.ts +0 -105
- package/src/post.ts +0 -275
- package/src/probe.test.ts +0 -275
- package/src/probe.ts +0 -166
- package/src/processing-claims.ts +0 -59
- package/src/qr-terminal.ts +0 -1
- package/src/reactions.ts +0 -123
- package/src/reasoning-preview.test.ts +0 -59
- package/src/reasoning-preview.ts +0 -20
- package/src/reply-dispatcher-runtime-api.ts +0 -7
- package/src/reply-dispatcher.test.ts +0 -1144
- package/src/reply-dispatcher.ts +0 -650
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -145
- package/src/secret-input.ts +0 -1
- package/src/security-audit-shared.ts +0 -69
- package/src/security-audit.test.ts +0 -61
- package/src/security-audit.ts +0 -1
- package/src/send-result.ts +0 -29
- package/src/send-target.test.ts +0 -80
- package/src/send-target.ts +0 -35
- package/src/send.reply-fallback.test.ts +0 -292
- package/src/send.test.ts +0 -550
- package/src/send.ts +0 -800
- package/src/sequential-key.test.ts +0 -72
- package/src/sequential-key.ts +0 -28
- package/src/sequential-queue.test.ts +0 -92
- package/src/sequential-queue.ts +0 -16
- package/src/session-conversation.ts +0 -42
- package/src/session-route.ts +0 -48
- package/src/setup-core.ts +0 -51
- package/src/setup-surface.test.ts +0 -174
- package/src/setup-surface.ts +0 -581
- package/src/streaming-card.test.ts +0 -190
- package/src/streaming-card.ts +0 -490
- package/src/subagent-hooks.test.ts +0 -603
- package/src/subagent-hooks.ts +0 -397
- package/src/targets.ts +0 -97
- package/src/test-support/lifecycle-test-support.ts +0 -453
- package/src/thread-bindings.test.ts +0 -143
- package/src/thread-bindings.ts +0 -330
- package/src/tool-account-routing.test.ts +0 -187
- package/src/tool-account.test.ts +0 -44
- package/src/tool-account.ts +0 -93
- package/src/tool-factory-test-harness.ts +0 -79
- package/src/tool-result.test.ts +0 -32
- package/src/tool-result.ts +0 -16
- package/src/tools-config.test.ts +0 -21
- package/src/tools-config.ts +0 -22
- package/src/types.ts +0 -104
- package/src/typing.test.ts +0 -144
- package/src/typing.ts +0 -214
- package/src/wiki-schema.ts +0 -55
- package/src/wiki.ts +0 -227
- package/subagent-hooks-api.ts +0 -31
- package/tsconfig.json +0 -16
package/src/outbound.ts
DELETED
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
attachChannelToResult,
|
|
5
|
-
createAttachedChannelResultAdapter,
|
|
6
|
-
} from "openclaw/plugin-sdk/channel-send-result";
|
|
7
|
-
import {
|
|
8
|
-
interactiveReplyToPresentation,
|
|
9
|
-
normalizeInteractiveReply,
|
|
10
|
-
normalizeMessagePresentation,
|
|
11
|
-
renderMessagePresentationFallbackText,
|
|
12
|
-
resolveInteractiveTextFallback,
|
|
13
|
-
type MessagePresentationBlock,
|
|
14
|
-
type MessagePresentationButton,
|
|
15
|
-
} from "openclaw/plugin-sdk/interactive-runtime";
|
|
16
|
-
import {
|
|
17
|
-
resolvePayloadMediaUrls,
|
|
18
|
-
sendPayloadMediaSequenceAndFinalize,
|
|
19
|
-
sendTextMediaPayload,
|
|
20
|
-
} from "openclaw/plugin-sdk/reply-payload";
|
|
21
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
22
|
-
import { resolveFeishuAccount } from "./accounts.js";
|
|
23
|
-
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
|
|
24
|
-
import { createFeishuClient } from "./client.js";
|
|
25
|
-
import { cleanupAmbientCommentTypingReaction } from "./comment-reaction.js";
|
|
26
|
-
import { parseFeishuCommentTarget } from "./comment-target.js";
|
|
27
|
-
import { deliverCommentThreadText } from "./drive.js";
|
|
28
|
-
import { sendMediaFeishu } from "./media.js";
|
|
29
|
-
import { chunkTextForOutbound, type ChannelOutboundAdapter } from "./outbound-runtime-api.js";
|
|
30
|
-
import {
|
|
31
|
-
resolveFeishuCardTemplate,
|
|
32
|
-
sendCardFeishu,
|
|
33
|
-
sendMarkdownCardFeishu,
|
|
34
|
-
sendMessageFeishu,
|
|
35
|
-
sendStructuredCardFeishu,
|
|
36
|
-
} from "./send.js";
|
|
37
|
-
|
|
38
|
-
const RENDERED_FEISHU_CARD = Symbol("openclaw.renderedFeishuCard");
|
|
39
|
-
|
|
40
|
-
function normalizePossibleLocalImagePath(text: string | undefined): string | null {
|
|
41
|
-
const raw = text?.trim();
|
|
42
|
-
if (!raw) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Only auto-convert when the message is a pure path-like payload.
|
|
47
|
-
// Avoid converting regular sentences that merely contain a path.
|
|
48
|
-
const hasWhitespace = /\s/.test(raw);
|
|
49
|
-
if (hasWhitespace) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Ignore links/data URLs; those should stay in normal mediaUrl/text paths.
|
|
54
|
-
if (/^(https?:\/\/|data:|file:\/\/)/i.test(raw)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ext = normalizeLowercaseStringOrEmpty(path.extname(raw));
|
|
59
|
-
const isImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(
|
|
60
|
-
ext,
|
|
61
|
-
);
|
|
62
|
-
if (!isImageExt) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!path.isAbsolute(raw)) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
if (!fs.existsSync(raw)) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Fix race condition: wrap statSync in try-catch to handle file deletion
|
|
74
|
-
// between existsSync and statSync
|
|
75
|
-
try {
|
|
76
|
-
if (!fs.statSync(raw).isFile()) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// File may have been deleted or became inaccessible between checks
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return raw;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function shouldUseCard(text: string): boolean {
|
|
88
|
-
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
92
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function escapeFeishuCardMarkdownText(text: string): string {
|
|
96
|
-
return text.replace(/[&<>]/g, (char) => {
|
|
97
|
-
switch (char) {
|
|
98
|
-
case "&":
|
|
99
|
-
return "&";
|
|
100
|
-
case "<":
|
|
101
|
-
return "<";
|
|
102
|
-
case ">":
|
|
103
|
-
return ">";
|
|
104
|
-
default:
|
|
105
|
-
return char;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function resolveSafeFeishuButtonUrl(url: string | undefined): string | undefined {
|
|
111
|
-
const trimmed = url?.trim();
|
|
112
|
-
if (!trimmed) {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const parsed = new URL(trimmed);
|
|
117
|
-
return parsed.protocol === "https:" || parsed.protocol === "http:" ? trimmed : undefined;
|
|
118
|
-
} catch {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function markRenderedFeishuCard(card: Record<string, unknown>): Record<string, unknown> {
|
|
124
|
-
Object.defineProperty(card, RENDERED_FEISHU_CARD, {
|
|
125
|
-
value: true,
|
|
126
|
-
enumerable: false,
|
|
127
|
-
});
|
|
128
|
-
return card;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function sanitizeNativeFeishuCardButton(button: unknown): Record<string, unknown> | undefined {
|
|
132
|
-
if (!isRecord(button)) {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
const text = isRecord(button.text) && typeof button.text.content === "string"
|
|
136
|
-
? button.text.content
|
|
137
|
-
: undefined;
|
|
138
|
-
if (!text?.trim()) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
const style =
|
|
142
|
-
button.type === "danger" ? "danger" : button.type === "primary" ? "primary" : undefined;
|
|
143
|
-
const rendered: Record<string, unknown> = {
|
|
144
|
-
tag: "button",
|
|
145
|
-
text: { tag: "plain_text", content: text },
|
|
146
|
-
type: mapFeishuButtonType(style),
|
|
147
|
-
};
|
|
148
|
-
const safeUrl = resolveSafeFeishuButtonUrl(
|
|
149
|
-
typeof button.url === "string" ? button.url : undefined,
|
|
150
|
-
);
|
|
151
|
-
if (safeUrl) {
|
|
152
|
-
rendered.url = safeUrl;
|
|
153
|
-
}
|
|
154
|
-
if (isRecord(button.value) && button.value.oc === "ocf1") {
|
|
155
|
-
rendered.value = button.value;
|
|
156
|
-
}
|
|
157
|
-
return rendered.url || rendered.value ? rendered : undefined;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function sanitizeNativeFeishuCardElement(element: unknown): Record<string, unknown> | undefined {
|
|
161
|
-
if (!isRecord(element) || typeof element.tag !== "string") {
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
if (element.tag === "hr") {
|
|
165
|
-
return { tag: "hr" };
|
|
166
|
-
}
|
|
167
|
-
if (element.tag === "markdown" && typeof element.content === "string") {
|
|
168
|
-
return { tag: "markdown", content: escapeFeishuCardMarkdownText(element.content) };
|
|
169
|
-
}
|
|
170
|
-
if (element.tag === "action" && Array.isArray(element.actions)) {
|
|
171
|
-
const actions = element.actions
|
|
172
|
-
.map((action) => sanitizeNativeFeishuCardButton(action))
|
|
173
|
-
.filter((action): action is Record<string, unknown> => Boolean(action));
|
|
174
|
-
return actions.length > 0 ? { tag: "action", actions } : undefined;
|
|
175
|
-
}
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function sanitizeNativeFeishuCard(card: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
180
|
-
const body = isRecord(card.body) ? card.body : undefined;
|
|
181
|
-
const rawElements = Array.isArray(body?.elements) ? body.elements : [];
|
|
182
|
-
const elements = rawElements
|
|
183
|
-
.map((element) => sanitizeNativeFeishuCardElement(element))
|
|
184
|
-
.filter((element): element is Record<string, unknown> => Boolean(element));
|
|
185
|
-
if (elements.length === 0) {
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const header = isRecord(card.header) ? card.header : undefined;
|
|
190
|
-
const title = isRecord(header?.title) && typeof header.title.content === "string"
|
|
191
|
-
? header.title.content
|
|
192
|
-
: undefined;
|
|
193
|
-
return markRenderedFeishuCard({
|
|
194
|
-
schema: "2.0",
|
|
195
|
-
config: { width_mode: "fill" },
|
|
196
|
-
...(title?.trim()
|
|
197
|
-
? {
|
|
198
|
-
header: {
|
|
199
|
-
title: { tag: "plain_text", content: title },
|
|
200
|
-
template:
|
|
201
|
-
resolveFeishuCardTemplate(
|
|
202
|
-
typeof header?.template === "string" ? header.template : undefined,
|
|
203
|
-
) ?? "blue",
|
|
204
|
-
},
|
|
205
|
-
}
|
|
206
|
-
: {}),
|
|
207
|
-
body: { elements },
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function readNativeFeishuCard(payload: { channelData?: Record<string, unknown> }) {
|
|
212
|
-
const feishuData = payload.channelData?.feishu;
|
|
213
|
-
if (!isRecord(feishuData)) {
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
const card = feishuData.card ?? feishuData.interactiveCard;
|
|
217
|
-
if (!isRecord(card)) {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
if ((card as { [RENDERED_FEISHU_CARD]?: true })[RENDERED_FEISHU_CARD] === true) {
|
|
221
|
-
return card;
|
|
222
|
-
}
|
|
223
|
-
return sanitizeNativeFeishuCard(card);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function mapFeishuButtonType(style: MessagePresentationButton["style"]) {
|
|
227
|
-
if (style === "primary" || style === "success") {
|
|
228
|
-
return "primary";
|
|
229
|
-
}
|
|
230
|
-
if (style === "danger") {
|
|
231
|
-
return "danger";
|
|
232
|
-
}
|
|
233
|
-
return "default";
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function buildFeishuPayloadButton(
|
|
237
|
-
button: MessagePresentationButton,
|
|
238
|
-
): Record<string, unknown> | undefined {
|
|
239
|
-
const rendered: Record<string, unknown> = {
|
|
240
|
-
tag: "button",
|
|
241
|
-
text: {
|
|
242
|
-
tag: "plain_text",
|
|
243
|
-
content: button.label,
|
|
244
|
-
},
|
|
245
|
-
type: mapFeishuButtonType(button.style),
|
|
246
|
-
};
|
|
247
|
-
if (button.url) {
|
|
248
|
-
const safeUrl = resolveSafeFeishuButtonUrl(button.url);
|
|
249
|
-
if (safeUrl) {
|
|
250
|
-
rendered.url = safeUrl;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (button.value) {
|
|
254
|
-
rendered.value = createFeishuCardInteractionEnvelope({
|
|
255
|
-
k: "quick",
|
|
256
|
-
a: "feishu.payload.button",
|
|
257
|
-
q: button.value,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
return rendered.url || rendered.value ? rendered : undefined;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function buildFeishuCardElementForBlock(
|
|
264
|
-
block: MessagePresentationBlock,
|
|
265
|
-
): Record<string, unknown> | undefined {
|
|
266
|
-
if (block.type === "text") {
|
|
267
|
-
return { tag: "markdown", content: escapeFeishuCardMarkdownText(block.text) };
|
|
268
|
-
}
|
|
269
|
-
if (block.type === "context") {
|
|
270
|
-
return {
|
|
271
|
-
tag: "markdown",
|
|
272
|
-
content: `<font color='grey'>${escapeFeishuCardMarkdownText(block.text)}</font>`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
if (block.type === "divider") {
|
|
276
|
-
return { tag: "hr" };
|
|
277
|
-
}
|
|
278
|
-
if (block.type === "buttons") {
|
|
279
|
-
const actions = block.buttons
|
|
280
|
-
.map((button) => buildFeishuPayloadButton(button))
|
|
281
|
-
.filter((button): button is Record<string, unknown> => Boolean(button));
|
|
282
|
-
if (actions.length === 0) {
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
tag: "action",
|
|
287
|
-
actions,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const labels = block.options.map((option) => `- ${option.label}`).join("\n");
|
|
291
|
-
return {
|
|
292
|
-
tag: "markdown",
|
|
293
|
-
content: `${escapeFeishuCardMarkdownText(
|
|
294
|
-
block.placeholder?.trim() || "Options",
|
|
295
|
-
)}:\n${escapeFeishuCardMarkdownText(labels)}`,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function buildFeishuPayloadCard(params: {
|
|
300
|
-
payload: Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0]["payload"];
|
|
301
|
-
text?: string;
|
|
302
|
-
identity?: Parameters<NonNullable<ChannelOutboundAdapter["sendPayload"]>>[0]["identity"];
|
|
303
|
-
}): Record<string, unknown> | undefined {
|
|
304
|
-
const nativeCard = readNativeFeishuCard(params.payload);
|
|
305
|
-
if (nativeCard) {
|
|
306
|
-
return nativeCard;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const interactive = normalizeInteractiveReply(params.payload.interactive);
|
|
310
|
-
const presentation =
|
|
311
|
-
normalizeMessagePresentation(params.payload.presentation) ??
|
|
312
|
-
(interactive ? interactiveReplyToPresentation(interactive) : undefined);
|
|
313
|
-
if (!presentation && !interactive) {
|
|
314
|
-
return undefined;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const text = resolveInteractiveTextFallback({
|
|
318
|
-
text: params.text ?? params.payload.text,
|
|
319
|
-
interactive,
|
|
320
|
-
});
|
|
321
|
-
const elements: Record<string, unknown>[] = [];
|
|
322
|
-
if (text?.trim()) {
|
|
323
|
-
elements.push({ tag: "markdown", content: escapeFeishuCardMarkdownText(text) });
|
|
324
|
-
}
|
|
325
|
-
for (const block of presentation?.blocks ?? []) {
|
|
326
|
-
const element = buildFeishuCardElementForBlock(block);
|
|
327
|
-
if (element) {
|
|
328
|
-
elements.push(element);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (elements.length === 0) {
|
|
332
|
-
elements.push({
|
|
333
|
-
tag: "markdown",
|
|
334
|
-
content: renderMessagePresentationFallbackText({ text, presentation }),
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const identityTitle = params.identity
|
|
339
|
-
? params.identity.emoji
|
|
340
|
-
? `${params.identity.emoji} ${params.identity.name ?? ""}`.trim()
|
|
341
|
-
: (params.identity.name ?? "")
|
|
342
|
-
: "";
|
|
343
|
-
const title = presentation?.title ?? identityTitle;
|
|
344
|
-
const template = resolveFeishuCardTemplate(
|
|
345
|
-
presentation?.tone === "danger"
|
|
346
|
-
? "red"
|
|
347
|
-
: presentation?.tone === "warning"
|
|
348
|
-
? "orange"
|
|
349
|
-
: presentation?.tone === "success"
|
|
350
|
-
? "green"
|
|
351
|
-
: "blue",
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
return markRenderedFeishuCard({
|
|
355
|
-
schema: "2.0",
|
|
356
|
-
config: { width_mode: "fill" },
|
|
357
|
-
...(title
|
|
358
|
-
? {
|
|
359
|
-
header: {
|
|
360
|
-
title: { tag: "plain_text", content: title },
|
|
361
|
-
template: template ?? "blue",
|
|
362
|
-
},
|
|
363
|
-
}
|
|
364
|
-
: {}),
|
|
365
|
-
body: { elements },
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function renderFeishuPresentationPayload({
|
|
370
|
-
payload,
|
|
371
|
-
presentation,
|
|
372
|
-
ctx,
|
|
373
|
-
}: Parameters<NonNullable<ChannelOutboundAdapter["renderPresentation"]>>[0]) {
|
|
374
|
-
const card = buildFeishuPayloadCard({
|
|
375
|
-
payload,
|
|
376
|
-
text: payload.text,
|
|
377
|
-
identity: ctx.identity,
|
|
378
|
-
});
|
|
379
|
-
if (!card) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
const existingFeishuData = isRecord(payload.channelData?.feishu)
|
|
383
|
-
? payload.channelData.feishu
|
|
384
|
-
: undefined;
|
|
385
|
-
return {
|
|
386
|
-
...payload,
|
|
387
|
-
text: renderMessagePresentationFallbackText({ text: payload.text, presentation }),
|
|
388
|
-
channelData: {
|
|
389
|
-
...payload.channelData,
|
|
390
|
-
feishu: {
|
|
391
|
-
...existingFeishuData,
|
|
392
|
-
card,
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function resolveReplyToMessageId(params: {
|
|
399
|
-
replyToId?: string | null;
|
|
400
|
-
threadId?: string | number | null;
|
|
401
|
-
}): string | undefined {
|
|
402
|
-
const replyToId = params.replyToId?.trim();
|
|
403
|
-
if (replyToId) {
|
|
404
|
-
return replyToId;
|
|
405
|
-
}
|
|
406
|
-
if (params.threadId == null) {
|
|
407
|
-
return undefined;
|
|
408
|
-
}
|
|
409
|
-
const trimmed = String(params.threadId).trim();
|
|
410
|
-
return trimmed || undefined;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function sendCommentThreadReply(params: {
|
|
414
|
-
cfg: Parameters<typeof sendMessageFeishu>[0]["cfg"];
|
|
415
|
-
to: string;
|
|
416
|
-
text: string;
|
|
417
|
-
replyId?: string;
|
|
418
|
-
accountId?: string;
|
|
419
|
-
}) {
|
|
420
|
-
const target = parseFeishuCommentTarget(params.to);
|
|
421
|
-
if (!target) {
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
425
|
-
const client = createFeishuClient(account);
|
|
426
|
-
const replyId = params.replyId?.trim();
|
|
427
|
-
try {
|
|
428
|
-
const result = await deliverCommentThreadText(client, {
|
|
429
|
-
file_token: target.fileToken,
|
|
430
|
-
file_type: target.fileType,
|
|
431
|
-
comment_id: target.commentId,
|
|
432
|
-
content: params.text,
|
|
433
|
-
});
|
|
434
|
-
return {
|
|
435
|
-
messageId:
|
|
436
|
-
(typeof result.reply_id === "string" && result.reply_id) ||
|
|
437
|
-
(typeof result.comment_id === "string" && result.comment_id) ||
|
|
438
|
-
"",
|
|
439
|
-
chatId: target.commentId,
|
|
440
|
-
result,
|
|
441
|
-
};
|
|
442
|
-
} finally {
|
|
443
|
-
if (replyId) {
|
|
444
|
-
void cleanupAmbientCommentTypingReaction({
|
|
445
|
-
client,
|
|
446
|
-
deliveryContext: {
|
|
447
|
-
channel: "feishu",
|
|
448
|
-
to: params.to,
|
|
449
|
-
threadId: replyId,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function sendOutboundText(params: {
|
|
457
|
-
cfg: Parameters<typeof sendMessageFeishu>[0]["cfg"];
|
|
458
|
-
to: string;
|
|
459
|
-
text: string;
|
|
460
|
-
replyToMessageId?: string;
|
|
461
|
-
accountId?: string;
|
|
462
|
-
}) {
|
|
463
|
-
const { cfg, to, text, accountId, replyToMessageId } = params;
|
|
464
|
-
const commentResult = await sendCommentThreadReply({
|
|
465
|
-
cfg,
|
|
466
|
-
to,
|
|
467
|
-
text,
|
|
468
|
-
replyId: replyToMessageId,
|
|
469
|
-
accountId,
|
|
470
|
-
});
|
|
471
|
-
if (commentResult) {
|
|
472
|
-
return commentResult;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const account = resolveFeishuAccount({ cfg, accountId });
|
|
476
|
-
const renderMode = account.config?.renderMode ?? "auto";
|
|
477
|
-
|
|
478
|
-
if (renderMode === "card" || (renderMode === "auto" && shouldUseCard(text))) {
|
|
479
|
-
return sendMarkdownCardFeishu({ cfg, to, text, accountId, replyToMessageId });
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return sendMessageFeishu({ cfg, to, text, accountId, replyToMessageId });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export const feishuOutbound: ChannelOutboundAdapter = {
|
|
486
|
-
deliveryMode: "direct",
|
|
487
|
-
chunker: chunkTextForOutbound,
|
|
488
|
-
chunkerMode: "markdown",
|
|
489
|
-
textChunkLimit: 4000,
|
|
490
|
-
presentationCapabilities: {
|
|
491
|
-
supported: true,
|
|
492
|
-
buttons: true,
|
|
493
|
-
selects: false,
|
|
494
|
-
context: true,
|
|
495
|
-
divider: true,
|
|
496
|
-
},
|
|
497
|
-
renderPresentation: renderFeishuPresentationPayload,
|
|
498
|
-
sendPayload: async (ctx) => {
|
|
499
|
-
const card = buildFeishuPayloadCard({
|
|
500
|
-
payload: ctx.payload,
|
|
501
|
-
text: ctx.text,
|
|
502
|
-
identity: ctx.identity,
|
|
503
|
-
});
|
|
504
|
-
if (!card) {
|
|
505
|
-
return await sendTextMediaPayload({
|
|
506
|
-
channel: "feishu",
|
|
507
|
-
ctx,
|
|
508
|
-
adapter: feishuOutbound,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const replyToMessageId = resolveReplyToMessageId({
|
|
513
|
-
replyToId: ctx.replyToId,
|
|
514
|
-
threadId: ctx.threadId,
|
|
515
|
-
});
|
|
516
|
-
const commentTarget = parseFeishuCommentTarget(ctx.to);
|
|
517
|
-
if (commentTarget) {
|
|
518
|
-
return await sendTextMediaPayload({
|
|
519
|
-
channel: "feishu",
|
|
520
|
-
ctx: {
|
|
521
|
-
...ctx,
|
|
522
|
-
payload: {
|
|
523
|
-
...ctx.payload,
|
|
524
|
-
text: renderMessagePresentationFallbackText({
|
|
525
|
-
text: ctx.payload.text,
|
|
526
|
-
presentation:
|
|
527
|
-
normalizeMessagePresentation(ctx.payload.presentation) ??
|
|
528
|
-
(() => {
|
|
529
|
-
const interactive = normalizeInteractiveReply(ctx.payload.interactive);
|
|
530
|
-
return interactive ? interactiveReplyToPresentation(interactive) : undefined;
|
|
531
|
-
})(),
|
|
532
|
-
}),
|
|
533
|
-
interactive: undefined,
|
|
534
|
-
presentation: undefined,
|
|
535
|
-
channelData: undefined,
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
adapter: feishuOutbound,
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const mediaUrls = resolvePayloadMediaUrls(ctx.payload)
|
|
543
|
-
.map((entry) => entry.trim())
|
|
544
|
-
.filter(Boolean);
|
|
545
|
-
return attachChannelToResult(
|
|
546
|
-
"feishu",
|
|
547
|
-
await sendPayloadMediaSequenceAndFinalize({
|
|
548
|
-
text: ctx.payload.text ?? "",
|
|
549
|
-
mediaUrls,
|
|
550
|
-
send: async ({ mediaUrl }) =>
|
|
551
|
-
await sendMediaFeishu({
|
|
552
|
-
cfg: ctx.cfg,
|
|
553
|
-
to: ctx.to,
|
|
554
|
-
mediaUrl,
|
|
555
|
-
accountId: ctx.accountId ?? undefined,
|
|
556
|
-
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
557
|
-
replyToMessageId,
|
|
558
|
-
...(ctx.payload.audioAsVoice === true || ctx.audioAsVoice === true
|
|
559
|
-
? { audioAsVoice: true }
|
|
560
|
-
: {}),
|
|
561
|
-
}),
|
|
562
|
-
finalize: async () =>
|
|
563
|
-
await sendCardFeishu({
|
|
564
|
-
cfg: ctx.cfg,
|
|
565
|
-
to: ctx.to,
|
|
566
|
-
card,
|
|
567
|
-
replyToMessageId,
|
|
568
|
-
replyInThread: ctx.threadId != null && !ctx.replyToId,
|
|
569
|
-
accountId: ctx.accountId ?? undefined,
|
|
570
|
-
}),
|
|
571
|
-
}),
|
|
572
|
-
);
|
|
573
|
-
},
|
|
574
|
-
...createAttachedChannelResultAdapter({
|
|
575
|
-
channel: "feishu",
|
|
576
|
-
sendText: async ({
|
|
577
|
-
cfg,
|
|
578
|
-
to,
|
|
579
|
-
text,
|
|
580
|
-
accountId,
|
|
581
|
-
replyToId,
|
|
582
|
-
threadId,
|
|
583
|
-
mediaLocalRoots,
|
|
584
|
-
identity,
|
|
585
|
-
}) => {
|
|
586
|
-
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
|
|
587
|
-
// Scheme A compatibility shim:
|
|
588
|
-
// when upstream accidentally returns a local image path as plain text,
|
|
589
|
-
// auto-upload and send as Feishu image message instead of leaking path text.
|
|
590
|
-
const localImagePath = normalizePossibleLocalImagePath(text);
|
|
591
|
-
if (localImagePath) {
|
|
592
|
-
try {
|
|
593
|
-
return await sendMediaFeishu({
|
|
594
|
-
cfg,
|
|
595
|
-
to,
|
|
596
|
-
mediaUrl: localImagePath,
|
|
597
|
-
accountId: accountId ?? undefined,
|
|
598
|
-
replyToMessageId,
|
|
599
|
-
mediaLocalRoots,
|
|
600
|
-
});
|
|
601
|
-
} catch (err) {
|
|
602
|
-
console.error(`[feishu] local image path auto-send failed:`, err);
|
|
603
|
-
// fall through to plain text as last resort
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (parseFeishuCommentTarget(to)) {
|
|
608
|
-
return await sendOutboundText({
|
|
609
|
-
cfg,
|
|
610
|
-
to,
|
|
611
|
-
text,
|
|
612
|
-
accountId: accountId ?? undefined,
|
|
613
|
-
replyToMessageId,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const account = resolveFeishuAccount({ cfg, accountId: accountId ?? undefined });
|
|
618
|
-
const renderMode = account.config?.renderMode ?? "auto";
|
|
619
|
-
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
620
|
-
if (useCard) {
|
|
621
|
-
const header = identity
|
|
622
|
-
? {
|
|
623
|
-
title: identity.emoji
|
|
624
|
-
? `${identity.emoji} ${identity.name ?? ""}`.trim()
|
|
625
|
-
: (identity.name ?? ""),
|
|
626
|
-
template: "blue" as const,
|
|
627
|
-
}
|
|
628
|
-
: undefined;
|
|
629
|
-
return await sendStructuredCardFeishu({
|
|
630
|
-
cfg,
|
|
631
|
-
to,
|
|
632
|
-
text,
|
|
633
|
-
replyToMessageId,
|
|
634
|
-
replyInThread: threadId != null && !replyToId,
|
|
635
|
-
accountId: accountId ?? undefined,
|
|
636
|
-
header: header?.title ? header : undefined,
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
return await sendOutboundText({
|
|
640
|
-
cfg,
|
|
641
|
-
to,
|
|
642
|
-
text,
|
|
643
|
-
accountId: accountId ?? undefined,
|
|
644
|
-
replyToMessageId,
|
|
645
|
-
});
|
|
646
|
-
},
|
|
647
|
-
sendMedia: async ({
|
|
648
|
-
cfg,
|
|
649
|
-
to,
|
|
650
|
-
text,
|
|
651
|
-
mediaUrl,
|
|
652
|
-
audioAsVoice,
|
|
653
|
-
accountId,
|
|
654
|
-
mediaLocalRoots,
|
|
655
|
-
replyToId,
|
|
656
|
-
threadId,
|
|
657
|
-
}) => {
|
|
658
|
-
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
|
|
659
|
-
const commentTarget = parseFeishuCommentTarget(to);
|
|
660
|
-
if (commentTarget) {
|
|
661
|
-
const commentText = [text?.trim(), mediaUrl?.trim()].filter(Boolean).join("\n\n");
|
|
662
|
-
return await sendOutboundText({
|
|
663
|
-
cfg,
|
|
664
|
-
to,
|
|
665
|
-
text: commentText || mediaUrl || text || "",
|
|
666
|
-
accountId: accountId ?? undefined,
|
|
667
|
-
replyToMessageId,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Send text first if provided
|
|
672
|
-
if (text?.trim()) {
|
|
673
|
-
await sendOutboundText({
|
|
674
|
-
cfg,
|
|
675
|
-
to,
|
|
676
|
-
text,
|
|
677
|
-
accountId: accountId ?? undefined,
|
|
678
|
-
replyToMessageId,
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Upload and send media if URL or local path provided
|
|
683
|
-
if (mediaUrl) {
|
|
684
|
-
try {
|
|
685
|
-
return await sendMediaFeishu({
|
|
686
|
-
cfg,
|
|
687
|
-
to,
|
|
688
|
-
mediaUrl,
|
|
689
|
-
accountId: accountId ?? undefined,
|
|
690
|
-
mediaLocalRoots,
|
|
691
|
-
replyToMessageId,
|
|
692
|
-
...(audioAsVoice === true ? { audioAsVoice: true } : {}),
|
|
693
|
-
});
|
|
694
|
-
} catch (err) {
|
|
695
|
-
// Log the error for debugging
|
|
696
|
-
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
|
697
|
-
// Fallback to URL link if upload fails
|
|
698
|
-
return await sendOutboundText({
|
|
699
|
-
cfg,
|
|
700
|
-
to,
|
|
701
|
-
text: `📎 ${mediaUrl}`,
|
|
702
|
-
accountId: accountId ?? undefined,
|
|
703
|
-
replyToMessageId,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// No media URL, just return text result
|
|
709
|
-
return await sendOutboundText({
|
|
710
|
-
cfg,
|
|
711
|
-
to,
|
|
712
|
-
text: text ?? "",
|
|
713
|
-
accountId: accountId ?? undefined,
|
|
714
|
-
replyToMessageId,
|
|
715
|
-
});
|
|
716
|
-
},
|
|
717
|
-
}),
|
|
718
|
-
};
|