@holon-run/agentinbox 1.0.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters.js +1 -1
- package/dist/src/http.js +2 -0
- package/dist/src/service.js +54 -2
- package/dist/src/source_schema.js +1 -0
- package/dist/src/sources/feishu.js +544 -3
- package/package.json +1 -1
package/dist/src/adapters.js
CHANGED
|
@@ -197,7 +197,7 @@ class AdapterRegistry {
|
|
|
197
197
|
async listDeliveryOperations(source, handle) {
|
|
198
198
|
const module = await this.resolveDeliveryModule(source, handle);
|
|
199
199
|
if (!module?.listDeliveryOperations) {
|
|
200
|
-
return
|
|
200
|
+
return null;
|
|
201
201
|
}
|
|
202
202
|
return module.listDeliveryOperations({ handle, source });
|
|
203
203
|
}
|
package/dist/src/http.js
CHANGED
|
@@ -1870,6 +1870,8 @@ function isBadRequestError(message) {
|
|
|
1870
1870
|
message.startsWith("deliver send is not supported") ||
|
|
1871
1871
|
message.startsWith("deliveryHandle requires") ||
|
|
1872
1872
|
message.startsWith("delivery operations are not supported") ||
|
|
1873
|
+
message.startsWith("unsupported delivery surface") ||
|
|
1874
|
+
message.startsWith("unsupported delivery operation") ||
|
|
1873
1875
|
message.startsWith("source operations are not supported") ||
|
|
1874
1876
|
message.startsWith("unknown GitHub delivery operation") ||
|
|
1875
1877
|
message.startsWith("unknown Feishu delivery operation") ||
|
package/dist/src/service.js
CHANGED
|
@@ -1620,6 +1620,12 @@ class AgentInboxService {
|
|
|
1620
1620
|
const handle = resolveDeliveryHandle(request);
|
|
1621
1621
|
const source = resolveDeliverySource(this.store, request.sourceId, handle);
|
|
1622
1622
|
const operations = await this.adapters.listDeliveryOperations(source, handle);
|
|
1623
|
+
if (operations === null) {
|
|
1624
|
+
throw new Error(`delivery operations are not supported for provider ${handle.provider}`);
|
|
1625
|
+
}
|
|
1626
|
+
if (operations.length === 0) {
|
|
1627
|
+
throw new Error(noDeliveryOperationsMessage(handle));
|
|
1628
|
+
}
|
|
1623
1629
|
return {
|
|
1624
1630
|
sourceId: source?.sourceId ?? null,
|
|
1625
1631
|
handle,
|
|
@@ -1629,6 +1635,15 @@ class AgentInboxService {
|
|
|
1629
1635
|
async invokeDelivery(request) {
|
|
1630
1636
|
const handle = resolveDeliveryHandle(request);
|
|
1631
1637
|
const source = resolveDeliverySource(this.store, request.sourceId, handle);
|
|
1638
|
+
const operations = await this.adapters.listDeliveryOperations(source, handle);
|
|
1639
|
+
if (operations !== null) {
|
|
1640
|
+
if (operations.length === 0) {
|
|
1641
|
+
throw new Error(noDeliveryOperationsMessage(handle));
|
|
1642
|
+
}
|
|
1643
|
+
if (!operations.some((operation) => operation.name === request.operation)) {
|
|
1644
|
+
throw new Error(unsupportedDeliveryOperationMessage(handle, request.operation, operations));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1632
1647
|
const attempt = {
|
|
1633
1648
|
deliveryId: (0, util_1.generateCanonicalId)("dlv"),
|
|
1634
1649
|
provider: handle.provider,
|
|
@@ -1648,9 +1663,9 @@ class AgentInboxService {
|
|
|
1648
1663
|
}
|
|
1649
1664
|
async sendCanonicalDeliveryViaModule(source, handle, payload, attempt) {
|
|
1650
1665
|
const operations = await this.adapters.listDeliveryOperations(source, handle);
|
|
1651
|
-
const canonicalOperation = operations
|
|
1666
|
+
const canonicalOperation = operations?.find((operation) => operation.canonicalTextAlias);
|
|
1652
1667
|
if (!canonicalOperation) {
|
|
1653
|
-
throw new Error(
|
|
1668
|
+
throw new Error(deliverSendUnsupportedMessage(handle, operations));
|
|
1654
1669
|
}
|
|
1655
1670
|
const input = payload.text != null ? { ...payload, body: String(payload.text) } : payload;
|
|
1656
1671
|
return this.adapters.invokeDeliveryOperation(source, handle, canonicalOperation.name, input, attempt);
|
|
@@ -3885,6 +3900,43 @@ function listHostStreamKinds(hostType) {
|
|
|
3885
3900
|
return ["default"];
|
|
3886
3901
|
}
|
|
3887
3902
|
}
|
|
3903
|
+
const KNOWN_DELIVERY_SURFACES = {
|
|
3904
|
+
feishu: ["message_reply", "chat_message"],
|
|
3905
|
+
github: ["issue_comment", "pull_request_comment", "review_comment"],
|
|
3906
|
+
};
|
|
3907
|
+
function unsupportedDeliverySurfaceMessage(handle) {
|
|
3908
|
+
const supported = KNOWN_DELIVERY_SURFACES[handle.provider];
|
|
3909
|
+
const supportedHint = supported?.length
|
|
3910
|
+
? ` Supported surfaces for ${handle.provider}: ${supported.join(", ")}.`
|
|
3911
|
+
: "";
|
|
3912
|
+
return `unsupported delivery surface for ${handle.provider}: ${handle.surface}.${supportedHint} ${deliveryActionsHelp(handle)}`;
|
|
3913
|
+
}
|
|
3914
|
+
function noDeliveryOperationsMessage(handle) {
|
|
3915
|
+
if (!isKnownDeliverySurface(handle)) {
|
|
3916
|
+
return unsupportedDeliverySurfaceMessage(handle);
|
|
3917
|
+
}
|
|
3918
|
+
return `delivery operations are not supported for ${handle.provider}/${handle.surface}. ${deliveryActionsHelp(handle)}`;
|
|
3919
|
+
}
|
|
3920
|
+
function unsupportedDeliveryOperationMessage(handle, operation, operations) {
|
|
3921
|
+
const names = operations.map((item) => item.name).join(", ");
|
|
3922
|
+
return `unsupported delivery operation for ${handle.provider}/${handle.surface}: ${operation}. Available operations: ${names}. ${deliveryActionsHelp(handle)}`;
|
|
3923
|
+
}
|
|
3924
|
+
function deliverSendUnsupportedMessage(handle, operations) {
|
|
3925
|
+
if (!operations || operations.length === 0) {
|
|
3926
|
+
if (!isKnownDeliverySurface(handle)) {
|
|
3927
|
+
return unsupportedDeliverySurfaceMessage(handle);
|
|
3928
|
+
}
|
|
3929
|
+
return `deliver send is not supported for ${handle.provider}/${handle.surface}. ${deliveryActionsHelp(handle)}`;
|
|
3930
|
+
}
|
|
3931
|
+
const names = operations.map((item) => item.name).join(", ");
|
|
3932
|
+
return `deliver send is not supported for ${handle.provider}/${handle.surface}; use deliver invoke with one of: ${names}. ${deliveryActionsHelp(handle)}`;
|
|
3933
|
+
}
|
|
3934
|
+
function deliveryActionsHelp(handle) {
|
|
3935
|
+
return `Run \`agentinbox deliver actions --provider ${handle.provider} --surface ${handle.surface} --target ${handle.targetRef}\` to inspect supported operations and input schemas.`;
|
|
3936
|
+
}
|
|
3937
|
+
function isKnownDeliverySurface(handle) {
|
|
3938
|
+
return KNOWN_DELIVERY_SURFACES[handle.provider]?.includes(handle.surface) === true;
|
|
3939
|
+
}
|
|
3888
3940
|
function getHostConfigFields(hostType) {
|
|
3889
3941
|
switch (hostType) {
|
|
3890
3942
|
case "github":
|
|
@@ -134,6 +134,7 @@ const SOURCE_SCHEMAS = {
|
|
|
134
134
|
{ name: "mentions", type: "string[]", description: "Mention names extracted from the message." },
|
|
135
135
|
{ name: "mentionOpenIds", type: "string[]", description: "Mention open_ids extracted from the message." },
|
|
136
136
|
{ name: "content", type: "string|null", description: "Normalized message content string." },
|
|
137
|
+
{ name: "attachments", type: "object[]", description: "Message attachments and Feishu/Lark document links that can be saved by source operations." },
|
|
137
138
|
{ name: "threadId", type: "string|null", description: "Thread or root message ID when present." },
|
|
138
139
|
{ name: "parentId", type: "string|null", description: "Parent message ID when present." },
|
|
139
140
|
],
|
|
@@ -10,18 +10,24 @@ exports.invokeFeishuSourceOperation = invokeFeishuSourceOperation;
|
|
|
10
10
|
exports.normalizeFeishuBotEvent = normalizeFeishuBotEvent;
|
|
11
11
|
exports.parseFeishuSourceConfig = parseFeishuSourceConfig;
|
|
12
12
|
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
15
|
const node_fs_1 = require("node:fs");
|
|
14
16
|
const node_path_1 = require("node:path");
|
|
15
17
|
const node_os_1 = require("node:os");
|
|
18
|
+
const node_util_1 = require("node:util");
|
|
16
19
|
exports.FEISHU_OPENAPI_ENDPOINT = "https://open.feishu.cn/open-apis";
|
|
17
20
|
exports.FEISHU_IM_SCHEMA_URL = "https://raw.githubusercontent.com/holon-run/uxc/main/skills/feishu-openapi-skill/references/feishu-im.openapi.json";
|
|
18
21
|
const FEISHU_BOT_SCHEMA_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "agentinbox-feishu-bot.openapi.json");
|
|
19
22
|
exports.DEFAULT_FEISHU_EVENT_TYPES = ["im.message.receive_v1"];
|
|
20
23
|
const DEFAULT_CONTEXT_BOUND_SECONDS = 7 * 24 * 60 * 60;
|
|
24
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
21
25
|
class FeishuUxcClient {
|
|
22
26
|
client;
|
|
23
|
-
|
|
27
|
+
commandRunner;
|
|
28
|
+
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env }), commandRunner = defaultFeishuCommandRunner) {
|
|
24
29
|
this.client = client;
|
|
30
|
+
this.commandRunner = commandRunner;
|
|
25
31
|
}
|
|
26
32
|
async sendChatMessage(input) {
|
|
27
33
|
await this.client.call({
|
|
@@ -166,8 +172,97 @@ class FeishuUxcClient {
|
|
|
166
172
|
});
|
|
167
173
|
return response.data;
|
|
168
174
|
}
|
|
175
|
+
async fetchDocumentMarkdown(input) {
|
|
176
|
+
const result = await this.commandRunner("lark-cli", [
|
|
177
|
+
"docs",
|
|
178
|
+
"+fetch",
|
|
179
|
+
"--api-version",
|
|
180
|
+
"v2",
|
|
181
|
+
"--doc",
|
|
182
|
+
input.document,
|
|
183
|
+
"--doc-format",
|
|
184
|
+
"markdown",
|
|
185
|
+
"--format",
|
|
186
|
+
"json",
|
|
187
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
188
|
+
]);
|
|
189
|
+
const parsed = JSON.parse(result.stdout);
|
|
190
|
+
const content = findStringField(parsed, "content");
|
|
191
|
+
if (content == null) {
|
|
192
|
+
throw new Error("lark-cli docs +fetch response did not include document.content");
|
|
193
|
+
}
|
|
194
|
+
return content;
|
|
195
|
+
}
|
|
196
|
+
async downloadMessageResource(input) {
|
|
197
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
198
|
+
throw new Error("downloadMessageResource requires an absolute outputPath");
|
|
199
|
+
}
|
|
200
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
201
|
+
await this.commandRunner("lark-cli", [
|
|
202
|
+
"im",
|
|
203
|
+
"+messages-resources-download",
|
|
204
|
+
"--message-id",
|
|
205
|
+
input.messageId,
|
|
206
|
+
"--file-key",
|
|
207
|
+
input.fileKey,
|
|
208
|
+
"--type",
|
|
209
|
+
input.resourceType,
|
|
210
|
+
"--output",
|
|
211
|
+
(0, node_path_1.basename)(input.outputPath),
|
|
212
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
213
|
+
], { cwd: (0, node_path_1.dirname)(input.outputPath) });
|
|
214
|
+
}
|
|
215
|
+
async downloadDriveFile(input) {
|
|
216
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
217
|
+
throw new Error("downloadDriveFile requires an absolute outputPath");
|
|
218
|
+
}
|
|
219
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
220
|
+
await this.commandRunner("lark-cli", [
|
|
221
|
+
"drive",
|
|
222
|
+
"+download",
|
|
223
|
+
"--file-token",
|
|
224
|
+
input.fileToken,
|
|
225
|
+
"--output",
|
|
226
|
+
input.outputPath,
|
|
227
|
+
...(input.overwrite ? ["--overwrite"] : []),
|
|
228
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
async exportDriveResource(input) {
|
|
232
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
233
|
+
throw new Error("exportDriveResource requires an absolute outputPath");
|
|
234
|
+
}
|
|
235
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
236
|
+
await this.commandRunner("lark-cli", [
|
|
237
|
+
"drive",
|
|
238
|
+
"+export",
|
|
239
|
+
"--token",
|
|
240
|
+
input.token,
|
|
241
|
+
"--doc-type",
|
|
242
|
+
input.docType,
|
|
243
|
+
"--file-extension",
|
|
244
|
+
input.fileExtension,
|
|
245
|
+
"--output-dir",
|
|
246
|
+
(0, node_path_1.dirname)(input.outputPath),
|
|
247
|
+
"--file-name",
|
|
248
|
+
(0, node_path_1.basename)(input.outputPath),
|
|
249
|
+
...(input.subId ? ["--sub-id", input.subId] : []),
|
|
250
|
+
...(input.overwrite ? ["--overwrite"] : []),
|
|
251
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
169
254
|
}
|
|
170
255
|
exports.FeishuUxcClient = FeishuUxcClient;
|
|
256
|
+
async function defaultFeishuCommandRunner(command, args, options) {
|
|
257
|
+
const result = await execFileAsync(command, args, {
|
|
258
|
+
cwd: options?.cwd,
|
|
259
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
stdout: result.stdout,
|
|
263
|
+
stderr: result.stderr,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
171
266
|
class FeishuDeliveryAdapter {
|
|
172
267
|
client;
|
|
173
268
|
constructor(client) {
|
|
@@ -526,6 +621,54 @@ function feishuSourceOperations() {
|
|
|
526
621
|
required: ["chat", "warnings"],
|
|
527
622
|
},
|
|
528
623
|
},
|
|
624
|
+
{
|
|
625
|
+
name: "list_message_attachments",
|
|
626
|
+
title: "List Message Attachments",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: "object",
|
|
629
|
+
additionalProperties: false,
|
|
630
|
+
required: ["messageId"],
|
|
631
|
+
properties: {
|
|
632
|
+
messageId: { type: "string", minLength: 1 },
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
outputSchema: {
|
|
636
|
+
type: "object",
|
|
637
|
+
additionalProperties: true,
|
|
638
|
+
required: ["messageId", "attachments", "warnings"],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "save_attachment",
|
|
643
|
+
title: "Save Attachment",
|
|
644
|
+
inputSchema: {
|
|
645
|
+
type: "object",
|
|
646
|
+
additionalProperties: false,
|
|
647
|
+
anyOf: [
|
|
648
|
+
{ required: ["attachment", "outputPath"] },
|
|
649
|
+
{ required: ["attachment", "outputDir"] },
|
|
650
|
+
{ required: ["messageId", "attachmentId", "outputPath"] },
|
|
651
|
+
{ required: ["messageId", "attachmentId", "outputDir"] },
|
|
652
|
+
],
|
|
653
|
+
properties: {
|
|
654
|
+
attachment: { type: "object", additionalProperties: true },
|
|
655
|
+
messageId: { type: "string", minLength: 1 },
|
|
656
|
+
attachmentId: { type: "string", minLength: 1 },
|
|
657
|
+
outputPath: { type: "string", minLength: 1 },
|
|
658
|
+
outputDir: { type: "string", minLength: 1 },
|
|
659
|
+
fileName: { type: "string", minLength: 1 },
|
|
660
|
+
format: { type: "string", minLength: 1 },
|
|
661
|
+
subId: { type: "string", minLength: 1 },
|
|
662
|
+
identity: { type: "string", enum: ["user", "bot"] },
|
|
663
|
+
overwrite: { type: "boolean" },
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
outputSchema: {
|
|
667
|
+
type: "object",
|
|
668
|
+
additionalProperties: true,
|
|
669
|
+
required: ["attachmentId", "kind", "path", "format"],
|
|
670
|
+
},
|
|
671
|
+
},
|
|
529
672
|
];
|
|
530
673
|
}
|
|
531
674
|
async function invokeFeishuSourceOperation(source, operation, input, client = new FeishuUxcClient()) {
|
|
@@ -541,6 +684,12 @@ async function invokeFeishuSourceOperation(source, operation, input, client = ne
|
|
|
541
684
|
if (operation === "get_chat") {
|
|
542
685
|
return getFeishuChat(source, input, client);
|
|
543
686
|
}
|
|
687
|
+
if (operation === "list_message_attachments") {
|
|
688
|
+
return listFeishuMessageAttachments(source, input, client);
|
|
689
|
+
}
|
|
690
|
+
if (operation === "save_attachment") {
|
|
691
|
+
return saveFeishuAttachment(source, input, client);
|
|
692
|
+
}
|
|
544
693
|
if (operation !== "get_message_context") {
|
|
545
694
|
throw new Error(`unknown Feishu source operation: ${operation}`);
|
|
546
695
|
}
|
|
@@ -593,6 +742,243 @@ async function invokeFeishuSourceOperation(source, operation, input, client = ne
|
|
|
593
742
|
},
|
|
594
743
|
};
|
|
595
744
|
}
|
|
745
|
+
async function listFeishuMessageAttachments(source, input, client) {
|
|
746
|
+
const messageId = asString(input.messageId);
|
|
747
|
+
if (!messageId) {
|
|
748
|
+
throw new Error("list_message_attachments requires input.messageId");
|
|
749
|
+
}
|
|
750
|
+
const config = parseFeishuSourceConfig(source);
|
|
751
|
+
const raw = await client.getMessage({
|
|
752
|
+
endpoint: config.endpoint,
|
|
753
|
+
schemaUrl: config.schemaUrl,
|
|
754
|
+
auth: config.uxcAuth,
|
|
755
|
+
messageId,
|
|
756
|
+
});
|
|
757
|
+
const message = normalizeFeishuMessage(raw);
|
|
758
|
+
return {
|
|
759
|
+
messageId,
|
|
760
|
+
attachments: message?.attachments ?? [],
|
|
761
|
+
warnings: message ? [] : [{ code: "message_not_found", message: "Feishu message was not found or could not be normalized" }],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function saveFeishuAttachment(source, input, client) {
|
|
765
|
+
const attachment = await resolveFeishuAttachment(source, input, client);
|
|
766
|
+
const outputPath = resolveAttachmentOutputPath(attachment, input);
|
|
767
|
+
const identity = asString(input.identity);
|
|
768
|
+
const overwrite = asBoolean(input.overwrite) ?? false;
|
|
769
|
+
const requestedFormat = asString(input.format)?.toLowerCase();
|
|
770
|
+
const subId = asString(input.subId);
|
|
771
|
+
if (attachment.kind === "image" || attachment.resourceType === "image") {
|
|
772
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
773
|
+
const fileKey = attachment.fileKey;
|
|
774
|
+
const messageId = attachment.messageId ?? asString(input.messageId);
|
|
775
|
+
if (!fileKey || !messageId) {
|
|
776
|
+
throw new Error("save_attachment requires messageId and fileKey for Feishu image resources");
|
|
777
|
+
}
|
|
778
|
+
await client.downloadMessageResource({
|
|
779
|
+
messageId,
|
|
780
|
+
fileKey,
|
|
781
|
+
resourceType: "image",
|
|
782
|
+
outputPath,
|
|
783
|
+
identity: identity ?? undefined,
|
|
784
|
+
});
|
|
785
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
786
|
+
}
|
|
787
|
+
if (attachment.fileKey) {
|
|
788
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
789
|
+
const messageId = attachment.messageId ?? asString(input.messageId);
|
|
790
|
+
if (!messageId) {
|
|
791
|
+
throw new Error("save_attachment requires messageId for Feishu message file resources");
|
|
792
|
+
}
|
|
793
|
+
await client.downloadMessageResource({
|
|
794
|
+
messageId,
|
|
795
|
+
fileKey: attachment.fileKey,
|
|
796
|
+
resourceType: "file",
|
|
797
|
+
outputPath,
|
|
798
|
+
identity: identity ?? undefined,
|
|
799
|
+
});
|
|
800
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
801
|
+
}
|
|
802
|
+
if (attachment.kind === "doc" || attachment.kind === "docx" || attachment.kind === "wiki") {
|
|
803
|
+
const document = attachment.url ?? attachment.token;
|
|
804
|
+
if (!document) {
|
|
805
|
+
throw new Error("save_attachment requires url or token for Feishu doc/docx/wiki attachments");
|
|
806
|
+
}
|
|
807
|
+
const markdown = await client.fetchDocumentMarkdown({
|
|
808
|
+
document,
|
|
809
|
+
identity: identity ?? undefined,
|
|
810
|
+
});
|
|
811
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(outputPath), { recursive: true });
|
|
812
|
+
(0, node_fs_1.writeFileSync)(outputPath, markdown, {
|
|
813
|
+
encoding: "utf8",
|
|
814
|
+
flag: overwrite ? "w" : "wx",
|
|
815
|
+
});
|
|
816
|
+
return savedAttachmentResult(attachment, outputPath, "markdown");
|
|
817
|
+
}
|
|
818
|
+
if (attachment.kind === "sheet") {
|
|
819
|
+
const extension = requestedFormat ?? "xlsx";
|
|
820
|
+
if (extension === "csv" && !subId) {
|
|
821
|
+
throw new Error("save_attachment requires input.subId when exporting a Feishu sheet as csv");
|
|
822
|
+
}
|
|
823
|
+
if (!attachment.token) {
|
|
824
|
+
throw new Error("save_attachment requires token for Feishu sheet attachments");
|
|
825
|
+
}
|
|
826
|
+
await client.exportDriveResource({
|
|
827
|
+
token: attachment.token,
|
|
828
|
+
docType: "sheet",
|
|
829
|
+
fileExtension: extension,
|
|
830
|
+
outputPath,
|
|
831
|
+
subId: extension === "csv" ? subId ?? undefined : undefined,
|
|
832
|
+
identity: identity ?? undefined,
|
|
833
|
+
overwrite,
|
|
834
|
+
});
|
|
835
|
+
return savedAttachmentResult(attachment, outputPath, extension);
|
|
836
|
+
}
|
|
837
|
+
if (attachment.kind === "bitable" || attachment.kind === "base") {
|
|
838
|
+
const extension = requestedFormat ?? "base";
|
|
839
|
+
if (extension === "csv" && !subId) {
|
|
840
|
+
throw new Error("save_attachment requires input.subId when exporting a Feishu base as csv");
|
|
841
|
+
}
|
|
842
|
+
if (!attachment.token) {
|
|
843
|
+
throw new Error("save_attachment requires token for Feishu base attachments");
|
|
844
|
+
}
|
|
845
|
+
await client.exportDriveResource({
|
|
846
|
+
token: attachment.token,
|
|
847
|
+
docType: "bitable",
|
|
848
|
+
fileExtension: extension,
|
|
849
|
+
outputPath,
|
|
850
|
+
subId: extension === "csv" ? subId ?? undefined : undefined,
|
|
851
|
+
identity: identity ?? undefined,
|
|
852
|
+
overwrite,
|
|
853
|
+
});
|
|
854
|
+
return savedAttachmentResult(attachment, outputPath, extension);
|
|
855
|
+
}
|
|
856
|
+
if (attachment.token && attachment.kind === "drive_file") {
|
|
857
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
858
|
+
await client.downloadDriveFile({
|
|
859
|
+
fileToken: attachment.token,
|
|
860
|
+
outputPath,
|
|
861
|
+
identity: identity ?? undefined,
|
|
862
|
+
overwrite,
|
|
863
|
+
});
|
|
864
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
865
|
+
}
|
|
866
|
+
throw new Error(`save_attachment does not support Feishu attachment kind: ${attachment.kind}`);
|
|
867
|
+
}
|
|
868
|
+
function rejectConvertedOriginalDownload(requestedFormat, kind) {
|
|
869
|
+
if (requestedFormat && requestedFormat !== "original") {
|
|
870
|
+
throw new Error(`save_attachment cannot convert Feishu ${kind} attachments; omit input.format or use "original"`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function resolveFeishuAttachment(source, input, client) {
|
|
874
|
+
const direct = normalizeAttachmentInput(input.attachment);
|
|
875
|
+
if (direct) {
|
|
876
|
+
return direct;
|
|
877
|
+
}
|
|
878
|
+
const messageId = asString(input.messageId);
|
|
879
|
+
const attachmentId = asString(input.attachmentId);
|
|
880
|
+
if (!messageId || !attachmentId) {
|
|
881
|
+
throw new Error("save_attachment requires either input.attachment or input.messageId and input.attachmentId");
|
|
882
|
+
}
|
|
883
|
+
const listed = await listFeishuMessageAttachments(source, { messageId }, client);
|
|
884
|
+
const attachments = Array.isArray(listed.attachments) ? listed.attachments : [];
|
|
885
|
+
const found = attachments
|
|
886
|
+
.map((item) => normalizeAttachmentInput(item))
|
|
887
|
+
.find((item) => item?.attachmentId === attachmentId);
|
|
888
|
+
if (!found) {
|
|
889
|
+
throw new Error(`save_attachment could not find attachmentId ${attachmentId} on message ${messageId}`);
|
|
890
|
+
}
|
|
891
|
+
return found;
|
|
892
|
+
}
|
|
893
|
+
function normalizeAttachmentInput(value) {
|
|
894
|
+
const raw = asRecord(value);
|
|
895
|
+
const kind = asString(raw.kind);
|
|
896
|
+
if (!kind) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
const messageId = asString(raw.messageId) ?? asString(raw.message_id);
|
|
900
|
+
const fileKey = asString(raw.fileKey) ?? asString(raw.file_key);
|
|
901
|
+
const token = asString(raw.token);
|
|
902
|
+
const url = asString(raw.url);
|
|
903
|
+
const attachment = makeFeishuAttachment({
|
|
904
|
+
kind,
|
|
905
|
+
messageId,
|
|
906
|
+
fileKey,
|
|
907
|
+
resourceType: asString(raw.resourceType) === "image" ? "image" : asString(raw.resourceType) === "file" ? "file" : null,
|
|
908
|
+
token,
|
|
909
|
+
url,
|
|
910
|
+
name: asString(raw.name) ?? asString(raw.fileName) ?? asString(raw.file_name),
|
|
911
|
+
mimeType: asString(raw.mimeType) ?? asString(raw.mime_type),
|
|
912
|
+
size: asNumber(raw.size),
|
|
913
|
+
raw,
|
|
914
|
+
});
|
|
915
|
+
return {
|
|
916
|
+
...attachment,
|
|
917
|
+
attachmentId: asString(raw.attachmentId) ?? asString(raw.attachment_id) ?? attachment.attachmentId,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function resolveAttachmentOutputPath(attachment, input) {
|
|
921
|
+
const outputPath = asString(input.outputPath);
|
|
922
|
+
if (outputPath) {
|
|
923
|
+
if (!(0, node_path_1.isAbsolute)(outputPath)) {
|
|
924
|
+
throw new Error("save_attachment requires input.outputPath to be absolute");
|
|
925
|
+
}
|
|
926
|
+
return outputPath;
|
|
927
|
+
}
|
|
928
|
+
const outputDir = asString(input.outputDir);
|
|
929
|
+
if (!outputDir) {
|
|
930
|
+
throw new Error("save_attachment requires input.outputPath or input.outputDir");
|
|
931
|
+
}
|
|
932
|
+
if (!(0, node_path_1.isAbsolute)(outputDir)) {
|
|
933
|
+
throw new Error("save_attachment requires input.outputDir to be absolute");
|
|
934
|
+
}
|
|
935
|
+
return (0, node_path_1.join)(outputDir, safeAttachmentFileName(attachment, input));
|
|
936
|
+
}
|
|
937
|
+
function safeAttachmentFileName(attachment, input) {
|
|
938
|
+
const explicit = asString(input.fileName);
|
|
939
|
+
const format = asString(input.format)?.toLowerCase();
|
|
940
|
+
const baseName = explicit
|
|
941
|
+
?? attachment.name
|
|
942
|
+
?? attachment.token
|
|
943
|
+
?? attachment.fileKey
|
|
944
|
+
?? attachment.attachmentId;
|
|
945
|
+
const safe = baseName.replace(/[\/\\:\0]/g, "_").trim() || attachment.attachmentId;
|
|
946
|
+
if ((0, node_path_1.extname)(safe)) {
|
|
947
|
+
return safe;
|
|
948
|
+
}
|
|
949
|
+
const extension = attachmentExtension(attachment, format);
|
|
950
|
+
return extension ? `${safe}.${extension}` : safe;
|
|
951
|
+
}
|
|
952
|
+
function attachmentExtension(attachment, format) {
|
|
953
|
+
if (format && format !== "original") {
|
|
954
|
+
return format === "markdown" ? "md" : format;
|
|
955
|
+
}
|
|
956
|
+
if (attachment.kind === "doc" || attachment.kind === "docx" || attachment.kind === "wiki") {
|
|
957
|
+
return "md";
|
|
958
|
+
}
|
|
959
|
+
if (attachment.kind === "sheet") {
|
|
960
|
+
return "xlsx";
|
|
961
|
+
}
|
|
962
|
+
if (attachment.kind === "bitable" || attachment.kind === "base") {
|
|
963
|
+
return "base";
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
function savedAttachmentResult(attachment, outputPath, format) {
|
|
968
|
+
return {
|
|
969
|
+
attachmentId: attachment.attachmentId,
|
|
970
|
+
kind: attachment.kind,
|
|
971
|
+
path: outputPath,
|
|
972
|
+
format,
|
|
973
|
+
provider: "feishu",
|
|
974
|
+
source: {
|
|
975
|
+
messageId: attachment.messageId,
|
|
976
|
+
fileKey: attachment.fileKey,
|
|
977
|
+
token: attachment.token,
|
|
978
|
+
url: attachment.url,
|
|
979
|
+
},
|
|
980
|
+
};
|
|
981
|
+
}
|
|
596
982
|
async function listFeishuChats(source, input, client) {
|
|
597
983
|
const config = parseFeishuSourceConfig(source);
|
|
598
984
|
const raw = await withFeishuChatDiscoveryError("list_chats", () => client.listChats({
|
|
@@ -721,6 +1107,12 @@ function normalizeFeishuBotEvent(source, config, raw) {
|
|
|
721
1107
|
const senderId = asString(asRecord(sender.sender_id).open_id) ?? asString(sender.sender_id);
|
|
722
1108
|
const senderType = asString(sender.sender_type);
|
|
723
1109
|
const content = stringifyFeishuMessageContent(messageType, asString(message.content), message.mentions);
|
|
1110
|
+
const attachments = extractFeishuMessageAttachments({
|
|
1111
|
+
messageId,
|
|
1112
|
+
messageType,
|
|
1113
|
+
rawContent: asString(message.content),
|
|
1114
|
+
rawMessage: message,
|
|
1115
|
+
});
|
|
724
1116
|
const threadId = asString(message.thread_id) ?? asString(message.root_id);
|
|
725
1117
|
const parentId = asString(message.parent_id);
|
|
726
1118
|
return {
|
|
@@ -742,6 +1134,7 @@ function normalizeFeishuBotEvent(source, config, raw) {
|
|
|
742
1134
|
mentions,
|
|
743
1135
|
mentionOpenIds,
|
|
744
1136
|
content,
|
|
1137
|
+
attachments,
|
|
745
1138
|
threadId,
|
|
746
1139
|
parentId,
|
|
747
1140
|
},
|
|
@@ -945,8 +1338,10 @@ function normalizeFeishuMessage(raw) {
|
|
|
945
1338
|
}
|
|
946
1339
|
const sender = asRecord(message.sender);
|
|
947
1340
|
const messageType = asString(message.msg_type) ?? asString(message.message_type) ?? "unknown";
|
|
1341
|
+
const messageId = asString(message.message_id) ?? "";
|
|
1342
|
+
const rawContent = messageContentString(message);
|
|
948
1343
|
return {
|
|
949
|
-
messageId
|
|
1344
|
+
messageId,
|
|
950
1345
|
chatId: asString(message.chat_id),
|
|
951
1346
|
chatType: asString(message.chat_type),
|
|
952
1347
|
messageType,
|
|
@@ -954,10 +1349,16 @@ function normalizeFeishuMessage(raw) {
|
|
|
954
1349
|
senderType: asString(sender.sender_type) ?? asString(asRecord(sender.id).user_id) ?? null,
|
|
955
1350
|
mentions: extractMentionNames(message.mentions),
|
|
956
1351
|
mentionOpenIds: extractMentionOpenIds(message.mentions),
|
|
957
|
-
content: stringifyFeishuMessageContent(messageType,
|
|
1352
|
+
content: stringifyFeishuMessageContent(messageType, rawContent, message.mentions),
|
|
958
1353
|
createdAt: fromUnixMillisString(asString(message.create_time)),
|
|
959
1354
|
threadId: asString(message.thread_id) ?? asString(message.root_id),
|
|
960
1355
|
parentId: asString(message.parent_id),
|
|
1356
|
+
attachments: extractFeishuMessageAttachments({
|
|
1357
|
+
messageId,
|
|
1358
|
+
messageType,
|
|
1359
|
+
rawContent,
|
|
1360
|
+
rawMessage: message,
|
|
1361
|
+
}),
|
|
961
1362
|
raw: message,
|
|
962
1363
|
};
|
|
963
1364
|
}
|
|
@@ -976,6 +1377,146 @@ function normalizeFeishuMessages(raw) {
|
|
|
976
1377
|
.map((item) => normalizeFeishuMessage(item))
|
|
977
1378
|
.filter((item) => item !== null);
|
|
978
1379
|
}
|
|
1380
|
+
function extractFeishuMessageAttachments(input) {
|
|
1381
|
+
const parsedContent = parseJsonContent(input.rawContent);
|
|
1382
|
+
const attachments = [];
|
|
1383
|
+
const content = asRecord(parsedContent);
|
|
1384
|
+
if (input.messageType === "image") {
|
|
1385
|
+
const imageKey = asString(content.image_key);
|
|
1386
|
+
if (imageKey) {
|
|
1387
|
+
attachments.push(makeFeishuAttachment({
|
|
1388
|
+
kind: "image",
|
|
1389
|
+
messageId: input.messageId,
|
|
1390
|
+
fileKey: imageKey,
|
|
1391
|
+
resourceType: "image",
|
|
1392
|
+
raw: content,
|
|
1393
|
+
}));
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (input.messageType === "file" || input.messageType === "audio" || input.messageType === "video" || input.messageType === "media") {
|
|
1397
|
+
const fileKey = asString(content.file_key);
|
|
1398
|
+
if (fileKey) {
|
|
1399
|
+
attachments.push(makeFeishuAttachment({
|
|
1400
|
+
kind: input.messageType,
|
|
1401
|
+
messageId: input.messageId,
|
|
1402
|
+
fileKey,
|
|
1403
|
+
resourceType: "file",
|
|
1404
|
+
name: asString(content.file_name) ?? asString(content.name),
|
|
1405
|
+
mimeType: asString(content.mime_type),
|
|
1406
|
+
size: asNumber(content.size),
|
|
1407
|
+
raw: content,
|
|
1408
|
+
}));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
for (const url of extractUrls(parsedContent)) {
|
|
1412
|
+
const link = feishuAttachmentFromUrl(url, input.messageId);
|
|
1413
|
+
if (link) {
|
|
1414
|
+
attachments.push(link);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return dedupeFeishuAttachments(attachments);
|
|
1418
|
+
}
|
|
1419
|
+
function parseJsonContent(rawContent) {
|
|
1420
|
+
if (!rawContent) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
return JSON.parse(rawContent);
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return rawContent;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function makeFeishuAttachment(input) {
|
|
1431
|
+
const stable = [
|
|
1432
|
+
input.messageId ?? "",
|
|
1433
|
+
input.kind,
|
|
1434
|
+
input.fileKey ?? "",
|
|
1435
|
+
input.token ?? "",
|
|
1436
|
+
input.url ?? "",
|
|
1437
|
+
].join(":");
|
|
1438
|
+
return {
|
|
1439
|
+
attachmentId: `att_${(0, node_crypto_1.createHash)("sha1").update(stable).digest("hex").slice(0, 16)}`,
|
|
1440
|
+
provider: "feishu",
|
|
1441
|
+
kind: input.kind,
|
|
1442
|
+
messageId: input.messageId ?? null,
|
|
1443
|
+
fileKey: input.fileKey ?? null,
|
|
1444
|
+
resourceType: input.resourceType ?? null,
|
|
1445
|
+
token: input.token ?? null,
|
|
1446
|
+
url: input.url ?? null,
|
|
1447
|
+
name: input.name ?? null,
|
|
1448
|
+
mimeType: input.mimeType ?? null,
|
|
1449
|
+
size: input.size ?? null,
|
|
1450
|
+
raw: input.raw ?? {},
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function extractUrls(value) {
|
|
1454
|
+
const urls = new Set();
|
|
1455
|
+
const visit = (current) => {
|
|
1456
|
+
if (typeof current === "string") {
|
|
1457
|
+
const matches = current.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
|
|
1458
|
+
for (const match of matches) {
|
|
1459
|
+
urls.add(match.replace(/[),.;,。]+$/, ""));
|
|
1460
|
+
}
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (Array.isArray(current)) {
|
|
1464
|
+
for (const child of current) {
|
|
1465
|
+
visit(child);
|
|
1466
|
+
}
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (current && typeof current === "object") {
|
|
1470
|
+
for (const child of Object.values(current)) {
|
|
1471
|
+
visit(child);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
visit(value);
|
|
1476
|
+
return Array.from(urls).sort();
|
|
1477
|
+
}
|
|
1478
|
+
function feishuAttachmentFromUrl(url, messageId) {
|
|
1479
|
+
let parsed;
|
|
1480
|
+
try {
|
|
1481
|
+
parsed = new URL(url);
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1487
|
+
const patterns = [
|
|
1488
|
+
{ segment: "docx", kind: "docx" },
|
|
1489
|
+
{ segment: "doc", kind: "doc" },
|
|
1490
|
+
{ segment: "wiki", kind: "wiki" },
|
|
1491
|
+
{ segment: "sheets", kind: "sheet" },
|
|
1492
|
+
{ segment: "sheet", kind: "sheet" },
|
|
1493
|
+
{ segment: "base", kind: "bitable" },
|
|
1494
|
+
{ segment: "bitable", kind: "bitable" },
|
|
1495
|
+
{ segment: "file", kind: "drive_file" },
|
|
1496
|
+
];
|
|
1497
|
+
for (const pattern of patterns) {
|
|
1498
|
+
const index = parts.indexOf(pattern.segment);
|
|
1499
|
+
const token = index >= 0 ? parts[index + 1] : null;
|
|
1500
|
+
if (token) {
|
|
1501
|
+
return makeFeishuAttachment({
|
|
1502
|
+
kind: pattern.kind,
|
|
1503
|
+
messageId,
|
|
1504
|
+
token,
|
|
1505
|
+
url,
|
|
1506
|
+
name: token,
|
|
1507
|
+
raw: { url },
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
function dedupeFeishuAttachments(attachments) {
|
|
1514
|
+
const byId = new Map();
|
|
1515
|
+
for (const attachment of attachments) {
|
|
1516
|
+
byId.set(attachment.attachmentId, attachment);
|
|
1517
|
+
}
|
|
1518
|
+
return Array.from(byId.values());
|
|
1519
|
+
}
|
|
979
1520
|
function normalizeFeishuChat(raw) {
|
|
980
1521
|
const chat = firstFeishuChat(raw);
|
|
981
1522
|
if (!chat) {
|