@holon-run/agentinbox 1.1.0 → 1.3.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/cli.js +28 -0
- package/dist/src/http.js +39 -0
- package/dist/src/service.js +23 -0
- package/dist/src/source_schema.js +1 -0
- package/dist/src/sources/feishu.js +546 -56
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -533,6 +533,27 @@ async function main() {
|
|
|
533
533
|
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}/resume`, {});
|
|
534
534
|
return;
|
|
535
535
|
}
|
|
536
|
+
if (command === "agent" && normalized[1] === "target" && normalized[2] === "update") {
|
|
537
|
+
const [agentId, targetId] = normalized.slice(3, 5);
|
|
538
|
+
if (!agentId || !targetId) {
|
|
539
|
+
throw new Error("usage: agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]");
|
|
540
|
+
}
|
|
541
|
+
const body = {};
|
|
542
|
+
const url = takeFlagValue(normalized, "--url");
|
|
543
|
+
if (url)
|
|
544
|
+
body.url = url;
|
|
545
|
+
const activationMode = takeFlagValue(normalized, "--activation-mode");
|
|
546
|
+
if (activationMode)
|
|
547
|
+
body.activationMode = activationMode;
|
|
548
|
+
const notifyLeaseMs = takeFlagValue(normalized, "--notify-lease-ms");
|
|
549
|
+
if (notifyLeaseMs)
|
|
550
|
+
body.notifyLeaseMs = parseOptionalNumber(notifyLeaseMs);
|
|
551
|
+
const minUnackedItems = takeFlagValue(normalized, "--min-unacked-items");
|
|
552
|
+
if (minUnackedItems)
|
|
553
|
+
body.minUnackedItems = parseOptionalNumber(minUnackedItems);
|
|
554
|
+
await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}`, body, "PATCH");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
536
557
|
if (command === "subscription" && normalized[1] === "add") {
|
|
537
558
|
const args = normalized.slice(2);
|
|
538
559
|
const positionals = positionalArgs(args, [
|
|
@@ -1154,11 +1175,17 @@ function clientArgsForCommand(args) {
|
|
|
1154
1175
|
if (isAgentTargetAddWebhookCommand(args)) {
|
|
1155
1176
|
return withoutFlagValues(args, "--url");
|
|
1156
1177
|
}
|
|
1178
|
+
if (isAgentTargetUpdateCommand(args)) {
|
|
1179
|
+
return withoutFlagValues(args, "--url");
|
|
1180
|
+
}
|
|
1157
1181
|
return args;
|
|
1158
1182
|
}
|
|
1159
1183
|
function isAgentTargetAddWebhookCommand(args) {
|
|
1160
1184
|
return args[0] === "agent" && args[1] === "target" && args[2] === "add" && args[3] === "webhook";
|
|
1161
1185
|
}
|
|
1186
|
+
function isAgentTargetUpdateCommand(args) {
|
|
1187
|
+
return args[0] === "agent" && args[1] === "target" && args[2] === "update";
|
|
1188
|
+
}
|
|
1162
1189
|
function withoutFlagValues(args, flag) {
|
|
1163
1190
|
const filtered = [];
|
|
1164
1191
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1514,6 +1541,7 @@ Usage:
|
|
|
1514
1541
|
agentinbox agent target list <agentId> [--limit N]
|
|
1515
1542
|
agentinbox agent target remove <agentId> <targetId>
|
|
1516
1543
|
agentinbox agent target resume <agentId> <targetId>
|
|
1544
|
+
agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]
|
|
1517
1545
|
`,
|
|
1518
1546
|
timer: `agentinbox timer
|
|
1519
1547
|
|
package/dist/src/http.js
CHANGED
|
@@ -1061,6 +1061,45 @@ function buildFastifyServer(service) {
|
|
|
1061
1061
|
minUnackedItems: body.minUnackedItems ?? null,
|
|
1062
1062
|
});
|
|
1063
1063
|
});
|
|
1064
|
+
app.patch("/agents/:agentId/targets/:targetId", {
|
|
1065
|
+
schema: {
|
|
1066
|
+
tags: ["agents"],
|
|
1067
|
+
params: {
|
|
1068
|
+
type: "object",
|
|
1069
|
+
required: ["agentId", "targetId"],
|
|
1070
|
+
properties: {
|
|
1071
|
+
agentId: { type: "string", minLength: 1 },
|
|
1072
|
+
targetId: { type: "string", minLength: 1 },
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
body: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
additionalProperties: false,
|
|
1078
|
+
properties: {
|
|
1079
|
+
url: { type: "string" },
|
|
1080
|
+
activationMode: { type: "string" },
|
|
1081
|
+
notifyLeaseMs: { type: "integer", minimum: 1 },
|
|
1082
|
+
minUnackedItems: { type: "integer", minimum: 1 },
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
response: {
|
|
1086
|
+
200: jsonObjectSchema,
|
|
1087
|
+
400: errorResponseSchema,
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
}, async (request) => {
|
|
1091
|
+
const params = request.params;
|
|
1092
|
+
const body = request.body;
|
|
1093
|
+
if (!body.url && body.activationMode === undefined && body.notifyLeaseMs === undefined && body.minUnackedItems === undefined) {
|
|
1094
|
+
throw new Error("at least one of --url, --activation-mode, --notify-lease-ms, --min-unacked-items must be provided");
|
|
1095
|
+
}
|
|
1096
|
+
return service.updateWebhookActivationTarget(decodeURIComponent(params.agentId), decodeURIComponent(params.targetId), {
|
|
1097
|
+
url: body.url,
|
|
1098
|
+
activationMode: body.activationMode,
|
|
1099
|
+
notifyLeaseMs: body.notifyLeaseMs ?? null,
|
|
1100
|
+
minUnackedItems: body.minUnackedItems ?? null,
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1064
1103
|
app.delete("/agents/:agentId/targets/:targetId", {
|
|
1065
1104
|
schema: {
|
|
1066
1105
|
tags: ["agents"],
|
package/dist/src/service.js
CHANGED
|
@@ -774,6 +774,29 @@ class AgentInboxService {
|
|
|
774
774
|
this.markAgentActive(agentId);
|
|
775
775
|
return target;
|
|
776
776
|
}
|
|
777
|
+
updateWebhookActivationTarget(agentId, targetId, input) {
|
|
778
|
+
this.getAgent(agentId);
|
|
779
|
+
const existing = this.getActivationTarget(targetId);
|
|
780
|
+
if (existing.agentId !== agentId) {
|
|
781
|
+
throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
|
|
782
|
+
}
|
|
783
|
+
if (existing.kind !== "webhook") {
|
|
784
|
+
throw new Error(`activation target ${targetId} is not a webhook target`);
|
|
785
|
+
}
|
|
786
|
+
validateNotifyLeaseMs(input.notifyLeaseMs);
|
|
787
|
+
validateMinUnackedItems(input.minUnackedItems);
|
|
788
|
+
const mode = normalizeWebhookActivationMode(input.activationMode ?? existing.mode);
|
|
789
|
+
const now = (0, util_1.nowIso)();
|
|
790
|
+
const target = this.store.updateWebhookActivationTarget(targetId, {
|
|
791
|
+
url: input.url ?? existing.url,
|
|
792
|
+
mode,
|
|
793
|
+
notifyLeaseMs: input.notifyLeaseMs ?? existing.notifyLeaseMs,
|
|
794
|
+
minUnackedItems: input.minUnackedItems !== undefined ? input.minUnackedItems : existing.minUnackedItems ?? null,
|
|
795
|
+
updatedAt: now,
|
|
796
|
+
lastSeenAt: now,
|
|
797
|
+
});
|
|
798
|
+
return target;
|
|
799
|
+
}
|
|
777
800
|
listActivationTargets(agentId, limit) {
|
|
778
801
|
if (agentId) {
|
|
779
802
|
this.getAgent(agentId);
|
|
@@ -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,22 @@ 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
|
-
const
|
|
17
|
+
const node_util_1 = require("node:util");
|
|
16
18
|
exports.FEISHU_OPENAPI_ENDPOINT = "https://open.feishu.cn/open-apis";
|
|
17
19
|
exports.FEISHU_IM_SCHEMA_URL = "https://raw.githubusercontent.com/holon-run/uxc/main/skills/feishu-openapi-skill/references/feishu-im.openapi.json";
|
|
18
|
-
const FEISHU_BOT_SCHEMA_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "agentinbox-feishu-bot.openapi.json");
|
|
19
20
|
exports.DEFAULT_FEISHU_EVENT_TYPES = ["im.message.receive_v1"];
|
|
20
21
|
const DEFAULT_CONTEXT_BOUND_SECONDS = 7 * 24 * 60 * 60;
|
|
22
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
21
23
|
class FeishuUxcClient {
|
|
22
24
|
client;
|
|
23
|
-
|
|
25
|
+
commandRunner;
|
|
26
|
+
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env }), commandRunner = defaultFeishuCommandRunner) {
|
|
24
27
|
this.client = client;
|
|
28
|
+
this.commandRunner = commandRunner;
|
|
25
29
|
}
|
|
26
30
|
async sendChatMessage(input) {
|
|
27
31
|
await this.client.call({
|
|
@@ -125,10 +129,8 @@ class FeishuUxcClient {
|
|
|
125
129
|
operation: "get:/bot/v3/info",
|
|
126
130
|
payload: {},
|
|
127
131
|
options: {
|
|
128
|
-
auth: input.auth,
|
|
129
|
-
schema_url:
|
|
130
|
-
refresh_schema: true,
|
|
131
|
-
no_cache: true,
|
|
132
|
+
auth: input.auth ?? undefined,
|
|
133
|
+
schema_url: exports.FEISHU_IM_SCHEMA_URL,
|
|
132
134
|
},
|
|
133
135
|
});
|
|
134
136
|
const openId = findStringField(response.data, "open_id");
|
|
@@ -166,8 +168,97 @@ class FeishuUxcClient {
|
|
|
166
168
|
});
|
|
167
169
|
return response.data;
|
|
168
170
|
}
|
|
171
|
+
async fetchDocumentMarkdown(input) {
|
|
172
|
+
const result = await this.commandRunner("lark-cli", [
|
|
173
|
+
"docs",
|
|
174
|
+
"+fetch",
|
|
175
|
+
"--api-version",
|
|
176
|
+
"v2",
|
|
177
|
+
"--doc",
|
|
178
|
+
input.document,
|
|
179
|
+
"--doc-format",
|
|
180
|
+
"markdown",
|
|
181
|
+
"--format",
|
|
182
|
+
"json",
|
|
183
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
184
|
+
]);
|
|
185
|
+
const parsed = JSON.parse(result.stdout);
|
|
186
|
+
const content = findStringField(parsed, "content");
|
|
187
|
+
if (content == null) {
|
|
188
|
+
throw new Error("lark-cli docs +fetch response did not include document.content");
|
|
189
|
+
}
|
|
190
|
+
return content;
|
|
191
|
+
}
|
|
192
|
+
async downloadMessageResource(input) {
|
|
193
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
194
|
+
throw new Error("downloadMessageResource requires an absolute outputPath");
|
|
195
|
+
}
|
|
196
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
197
|
+
await this.commandRunner("lark-cli", [
|
|
198
|
+
"im",
|
|
199
|
+
"+messages-resources-download",
|
|
200
|
+
"--message-id",
|
|
201
|
+
input.messageId,
|
|
202
|
+
"--file-key",
|
|
203
|
+
input.fileKey,
|
|
204
|
+
"--type",
|
|
205
|
+
input.resourceType,
|
|
206
|
+
"--output",
|
|
207
|
+
(0, node_path_1.basename)(input.outputPath),
|
|
208
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
209
|
+
], { cwd: (0, node_path_1.dirname)(input.outputPath) });
|
|
210
|
+
}
|
|
211
|
+
async downloadDriveFile(input) {
|
|
212
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
213
|
+
throw new Error("downloadDriveFile requires an absolute outputPath");
|
|
214
|
+
}
|
|
215
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
216
|
+
await this.commandRunner("lark-cli", [
|
|
217
|
+
"drive",
|
|
218
|
+
"+download",
|
|
219
|
+
"--file-token",
|
|
220
|
+
input.fileToken,
|
|
221
|
+
"--output",
|
|
222
|
+
input.outputPath,
|
|
223
|
+
...(input.overwrite ? ["--overwrite"] : []),
|
|
224
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
async exportDriveResource(input) {
|
|
228
|
+
if (!(0, node_path_1.isAbsolute)(input.outputPath)) {
|
|
229
|
+
throw new Error("exportDriveResource requires an absolute outputPath");
|
|
230
|
+
}
|
|
231
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(input.outputPath), { recursive: true });
|
|
232
|
+
await this.commandRunner("lark-cli", [
|
|
233
|
+
"drive",
|
|
234
|
+
"+export",
|
|
235
|
+
"--token",
|
|
236
|
+
input.token,
|
|
237
|
+
"--doc-type",
|
|
238
|
+
input.docType,
|
|
239
|
+
"--file-extension",
|
|
240
|
+
input.fileExtension,
|
|
241
|
+
"--output-dir",
|
|
242
|
+
(0, node_path_1.dirname)(input.outputPath),
|
|
243
|
+
"--file-name",
|
|
244
|
+
(0, node_path_1.basename)(input.outputPath),
|
|
245
|
+
...(input.subId ? ["--sub-id", input.subId] : []),
|
|
246
|
+
...(input.overwrite ? ["--overwrite"] : []),
|
|
247
|
+
...(input.identity ? ["--as", input.identity] : []),
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
169
250
|
}
|
|
170
251
|
exports.FeishuUxcClient = FeishuUxcClient;
|
|
252
|
+
async function defaultFeishuCommandRunner(command, args, options) {
|
|
253
|
+
const result = await execFileAsync(command, args, {
|
|
254
|
+
cwd: options?.cwd,
|
|
255
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
stdout: result.stdout,
|
|
259
|
+
stderr: result.stderr,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
171
262
|
class FeishuDeliveryAdapter {
|
|
172
263
|
client;
|
|
173
264
|
constructor(client) {
|
|
@@ -526,6 +617,54 @@ function feishuSourceOperations() {
|
|
|
526
617
|
required: ["chat", "warnings"],
|
|
527
618
|
},
|
|
528
619
|
},
|
|
620
|
+
{
|
|
621
|
+
name: "list_message_attachments",
|
|
622
|
+
title: "List Message Attachments",
|
|
623
|
+
inputSchema: {
|
|
624
|
+
type: "object",
|
|
625
|
+
additionalProperties: false,
|
|
626
|
+
required: ["messageId"],
|
|
627
|
+
properties: {
|
|
628
|
+
messageId: { type: "string", minLength: 1 },
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
outputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
additionalProperties: true,
|
|
634
|
+
required: ["messageId", "attachments", "warnings"],
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: "save_attachment",
|
|
639
|
+
title: "Save Attachment",
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: "object",
|
|
642
|
+
additionalProperties: false,
|
|
643
|
+
anyOf: [
|
|
644
|
+
{ required: ["attachment", "outputPath"] },
|
|
645
|
+
{ required: ["attachment", "outputDir"] },
|
|
646
|
+
{ required: ["messageId", "attachmentId", "outputPath"] },
|
|
647
|
+
{ required: ["messageId", "attachmentId", "outputDir"] },
|
|
648
|
+
],
|
|
649
|
+
properties: {
|
|
650
|
+
attachment: { type: "object", additionalProperties: true },
|
|
651
|
+
messageId: { type: "string", minLength: 1 },
|
|
652
|
+
attachmentId: { type: "string", minLength: 1 },
|
|
653
|
+
outputPath: { type: "string", minLength: 1 },
|
|
654
|
+
outputDir: { type: "string", minLength: 1 },
|
|
655
|
+
fileName: { type: "string", minLength: 1 },
|
|
656
|
+
format: { type: "string", minLength: 1 },
|
|
657
|
+
subId: { type: "string", minLength: 1 },
|
|
658
|
+
identity: { type: "string", enum: ["user", "bot"] },
|
|
659
|
+
overwrite: { type: "boolean" },
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
outputSchema: {
|
|
663
|
+
type: "object",
|
|
664
|
+
additionalProperties: true,
|
|
665
|
+
required: ["attachmentId", "kind", "path", "format"],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
529
668
|
];
|
|
530
669
|
}
|
|
531
670
|
async function invokeFeishuSourceOperation(source, operation, input, client = new FeishuUxcClient()) {
|
|
@@ -541,6 +680,12 @@ async function invokeFeishuSourceOperation(source, operation, input, client = ne
|
|
|
541
680
|
if (operation === "get_chat") {
|
|
542
681
|
return getFeishuChat(source, input, client);
|
|
543
682
|
}
|
|
683
|
+
if (operation === "list_message_attachments") {
|
|
684
|
+
return listFeishuMessageAttachments(source, input, client);
|
|
685
|
+
}
|
|
686
|
+
if (operation === "save_attachment") {
|
|
687
|
+
return saveFeishuAttachment(source, input, client);
|
|
688
|
+
}
|
|
544
689
|
if (operation !== "get_message_context") {
|
|
545
690
|
throw new Error(`unknown Feishu source operation: ${operation}`);
|
|
546
691
|
}
|
|
@@ -593,6 +738,243 @@ async function invokeFeishuSourceOperation(source, operation, input, client = ne
|
|
|
593
738
|
},
|
|
594
739
|
};
|
|
595
740
|
}
|
|
741
|
+
async function listFeishuMessageAttachments(source, input, client) {
|
|
742
|
+
const messageId = asString(input.messageId);
|
|
743
|
+
if (!messageId) {
|
|
744
|
+
throw new Error("list_message_attachments requires input.messageId");
|
|
745
|
+
}
|
|
746
|
+
const config = parseFeishuSourceConfig(source);
|
|
747
|
+
const raw = await client.getMessage({
|
|
748
|
+
endpoint: config.endpoint,
|
|
749
|
+
schemaUrl: config.schemaUrl,
|
|
750
|
+
auth: config.uxcAuth,
|
|
751
|
+
messageId,
|
|
752
|
+
});
|
|
753
|
+
const message = normalizeFeishuMessage(raw);
|
|
754
|
+
return {
|
|
755
|
+
messageId,
|
|
756
|
+
attachments: message?.attachments ?? [],
|
|
757
|
+
warnings: message ? [] : [{ code: "message_not_found", message: "Feishu message was not found or could not be normalized" }],
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
async function saveFeishuAttachment(source, input, client) {
|
|
761
|
+
const attachment = await resolveFeishuAttachment(source, input, client);
|
|
762
|
+
const outputPath = resolveAttachmentOutputPath(attachment, input);
|
|
763
|
+
const identity = asString(input.identity);
|
|
764
|
+
const overwrite = asBoolean(input.overwrite) ?? false;
|
|
765
|
+
const requestedFormat = asString(input.format)?.toLowerCase();
|
|
766
|
+
const subId = asString(input.subId);
|
|
767
|
+
if (attachment.kind === "image" || attachment.resourceType === "image") {
|
|
768
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
769
|
+
const fileKey = attachment.fileKey;
|
|
770
|
+
const messageId = attachment.messageId ?? asString(input.messageId);
|
|
771
|
+
if (!fileKey || !messageId) {
|
|
772
|
+
throw new Error("save_attachment requires messageId and fileKey for Feishu image resources");
|
|
773
|
+
}
|
|
774
|
+
await client.downloadMessageResource({
|
|
775
|
+
messageId,
|
|
776
|
+
fileKey,
|
|
777
|
+
resourceType: "image",
|
|
778
|
+
outputPath,
|
|
779
|
+
identity: identity ?? undefined,
|
|
780
|
+
});
|
|
781
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
782
|
+
}
|
|
783
|
+
if (attachment.fileKey) {
|
|
784
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
785
|
+
const messageId = attachment.messageId ?? asString(input.messageId);
|
|
786
|
+
if (!messageId) {
|
|
787
|
+
throw new Error("save_attachment requires messageId for Feishu message file resources");
|
|
788
|
+
}
|
|
789
|
+
await client.downloadMessageResource({
|
|
790
|
+
messageId,
|
|
791
|
+
fileKey: attachment.fileKey,
|
|
792
|
+
resourceType: "file",
|
|
793
|
+
outputPath,
|
|
794
|
+
identity: identity ?? undefined,
|
|
795
|
+
});
|
|
796
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
797
|
+
}
|
|
798
|
+
if (attachment.kind === "doc" || attachment.kind === "docx" || attachment.kind === "wiki") {
|
|
799
|
+
const document = attachment.url ?? attachment.token;
|
|
800
|
+
if (!document) {
|
|
801
|
+
throw new Error("save_attachment requires url or token for Feishu doc/docx/wiki attachments");
|
|
802
|
+
}
|
|
803
|
+
const markdown = await client.fetchDocumentMarkdown({
|
|
804
|
+
document,
|
|
805
|
+
identity: identity ?? undefined,
|
|
806
|
+
});
|
|
807
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(outputPath), { recursive: true });
|
|
808
|
+
(0, node_fs_1.writeFileSync)(outputPath, markdown, {
|
|
809
|
+
encoding: "utf8",
|
|
810
|
+
flag: overwrite ? "w" : "wx",
|
|
811
|
+
});
|
|
812
|
+
return savedAttachmentResult(attachment, outputPath, "markdown");
|
|
813
|
+
}
|
|
814
|
+
if (attachment.kind === "sheet") {
|
|
815
|
+
const extension = requestedFormat ?? "xlsx";
|
|
816
|
+
if (extension === "csv" && !subId) {
|
|
817
|
+
throw new Error("save_attachment requires input.subId when exporting a Feishu sheet as csv");
|
|
818
|
+
}
|
|
819
|
+
if (!attachment.token) {
|
|
820
|
+
throw new Error("save_attachment requires token for Feishu sheet attachments");
|
|
821
|
+
}
|
|
822
|
+
await client.exportDriveResource({
|
|
823
|
+
token: attachment.token,
|
|
824
|
+
docType: "sheet",
|
|
825
|
+
fileExtension: extension,
|
|
826
|
+
outputPath,
|
|
827
|
+
subId: extension === "csv" ? subId ?? undefined : undefined,
|
|
828
|
+
identity: identity ?? undefined,
|
|
829
|
+
overwrite,
|
|
830
|
+
});
|
|
831
|
+
return savedAttachmentResult(attachment, outputPath, extension);
|
|
832
|
+
}
|
|
833
|
+
if (attachment.kind === "bitable" || attachment.kind === "base") {
|
|
834
|
+
const extension = requestedFormat ?? "base";
|
|
835
|
+
if (extension === "csv" && !subId) {
|
|
836
|
+
throw new Error("save_attachment requires input.subId when exporting a Feishu base as csv");
|
|
837
|
+
}
|
|
838
|
+
if (!attachment.token) {
|
|
839
|
+
throw new Error("save_attachment requires token for Feishu base attachments");
|
|
840
|
+
}
|
|
841
|
+
await client.exportDriveResource({
|
|
842
|
+
token: attachment.token,
|
|
843
|
+
docType: "bitable",
|
|
844
|
+
fileExtension: extension,
|
|
845
|
+
outputPath,
|
|
846
|
+
subId: extension === "csv" ? subId ?? undefined : undefined,
|
|
847
|
+
identity: identity ?? undefined,
|
|
848
|
+
overwrite,
|
|
849
|
+
});
|
|
850
|
+
return savedAttachmentResult(attachment, outputPath, extension);
|
|
851
|
+
}
|
|
852
|
+
if (attachment.token && attachment.kind === "drive_file") {
|
|
853
|
+
rejectConvertedOriginalDownload(requestedFormat, attachment.kind);
|
|
854
|
+
await client.downloadDriveFile({
|
|
855
|
+
fileToken: attachment.token,
|
|
856
|
+
outputPath,
|
|
857
|
+
identity: identity ?? undefined,
|
|
858
|
+
overwrite,
|
|
859
|
+
});
|
|
860
|
+
return savedAttachmentResult(attachment, outputPath, requestedFormat ?? "original");
|
|
861
|
+
}
|
|
862
|
+
throw new Error(`save_attachment does not support Feishu attachment kind: ${attachment.kind}`);
|
|
863
|
+
}
|
|
864
|
+
function rejectConvertedOriginalDownload(requestedFormat, kind) {
|
|
865
|
+
if (requestedFormat && requestedFormat !== "original") {
|
|
866
|
+
throw new Error(`save_attachment cannot convert Feishu ${kind} attachments; omit input.format or use "original"`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function resolveFeishuAttachment(source, input, client) {
|
|
870
|
+
const direct = normalizeAttachmentInput(input.attachment);
|
|
871
|
+
if (direct) {
|
|
872
|
+
return direct;
|
|
873
|
+
}
|
|
874
|
+
const messageId = asString(input.messageId);
|
|
875
|
+
const attachmentId = asString(input.attachmentId);
|
|
876
|
+
if (!messageId || !attachmentId) {
|
|
877
|
+
throw new Error("save_attachment requires either input.attachment or input.messageId and input.attachmentId");
|
|
878
|
+
}
|
|
879
|
+
const listed = await listFeishuMessageAttachments(source, { messageId }, client);
|
|
880
|
+
const attachments = Array.isArray(listed.attachments) ? listed.attachments : [];
|
|
881
|
+
const found = attachments
|
|
882
|
+
.map((item) => normalizeAttachmentInput(item))
|
|
883
|
+
.find((item) => item?.attachmentId === attachmentId);
|
|
884
|
+
if (!found) {
|
|
885
|
+
throw new Error(`save_attachment could not find attachmentId ${attachmentId} on message ${messageId}`);
|
|
886
|
+
}
|
|
887
|
+
return found;
|
|
888
|
+
}
|
|
889
|
+
function normalizeAttachmentInput(value) {
|
|
890
|
+
const raw = asRecord(value);
|
|
891
|
+
const kind = asString(raw.kind);
|
|
892
|
+
if (!kind) {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
const messageId = asString(raw.messageId) ?? asString(raw.message_id);
|
|
896
|
+
const fileKey = asString(raw.fileKey) ?? asString(raw.file_key);
|
|
897
|
+
const token = asString(raw.token);
|
|
898
|
+
const url = asString(raw.url);
|
|
899
|
+
const attachment = makeFeishuAttachment({
|
|
900
|
+
kind,
|
|
901
|
+
messageId,
|
|
902
|
+
fileKey,
|
|
903
|
+
resourceType: asString(raw.resourceType) === "image" ? "image" : asString(raw.resourceType) === "file" ? "file" : null,
|
|
904
|
+
token,
|
|
905
|
+
url,
|
|
906
|
+
name: asString(raw.name) ?? asString(raw.fileName) ?? asString(raw.file_name),
|
|
907
|
+
mimeType: asString(raw.mimeType) ?? asString(raw.mime_type),
|
|
908
|
+
size: asNumber(raw.size),
|
|
909
|
+
raw,
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
...attachment,
|
|
913
|
+
attachmentId: asString(raw.attachmentId) ?? asString(raw.attachment_id) ?? attachment.attachmentId,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function resolveAttachmentOutputPath(attachment, input) {
|
|
917
|
+
const outputPath = asString(input.outputPath);
|
|
918
|
+
if (outputPath) {
|
|
919
|
+
if (!(0, node_path_1.isAbsolute)(outputPath)) {
|
|
920
|
+
throw new Error("save_attachment requires input.outputPath to be absolute");
|
|
921
|
+
}
|
|
922
|
+
return outputPath;
|
|
923
|
+
}
|
|
924
|
+
const outputDir = asString(input.outputDir);
|
|
925
|
+
if (!outputDir) {
|
|
926
|
+
throw new Error("save_attachment requires input.outputPath or input.outputDir");
|
|
927
|
+
}
|
|
928
|
+
if (!(0, node_path_1.isAbsolute)(outputDir)) {
|
|
929
|
+
throw new Error("save_attachment requires input.outputDir to be absolute");
|
|
930
|
+
}
|
|
931
|
+
return (0, node_path_1.join)(outputDir, safeAttachmentFileName(attachment, input));
|
|
932
|
+
}
|
|
933
|
+
function safeAttachmentFileName(attachment, input) {
|
|
934
|
+
const explicit = asString(input.fileName);
|
|
935
|
+
const format = asString(input.format)?.toLowerCase();
|
|
936
|
+
const baseName = explicit
|
|
937
|
+
?? attachment.name
|
|
938
|
+
?? attachment.token
|
|
939
|
+
?? attachment.fileKey
|
|
940
|
+
?? attachment.attachmentId;
|
|
941
|
+
const safe = baseName.replace(/[\/\\:\0]/g, "_").trim() || attachment.attachmentId;
|
|
942
|
+
if ((0, node_path_1.extname)(safe)) {
|
|
943
|
+
return safe;
|
|
944
|
+
}
|
|
945
|
+
const extension = attachmentExtension(attachment, format);
|
|
946
|
+
return extension ? `${safe}.${extension}` : safe;
|
|
947
|
+
}
|
|
948
|
+
function attachmentExtension(attachment, format) {
|
|
949
|
+
if (format && format !== "original") {
|
|
950
|
+
return format === "markdown" ? "md" : format;
|
|
951
|
+
}
|
|
952
|
+
if (attachment.kind === "doc" || attachment.kind === "docx" || attachment.kind === "wiki") {
|
|
953
|
+
return "md";
|
|
954
|
+
}
|
|
955
|
+
if (attachment.kind === "sheet") {
|
|
956
|
+
return "xlsx";
|
|
957
|
+
}
|
|
958
|
+
if (attachment.kind === "bitable" || attachment.kind === "base") {
|
|
959
|
+
return "base";
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function savedAttachmentResult(attachment, outputPath, format) {
|
|
964
|
+
return {
|
|
965
|
+
attachmentId: attachment.attachmentId,
|
|
966
|
+
kind: attachment.kind,
|
|
967
|
+
path: outputPath,
|
|
968
|
+
format,
|
|
969
|
+
provider: "feishu",
|
|
970
|
+
source: {
|
|
971
|
+
messageId: attachment.messageId,
|
|
972
|
+
fileKey: attachment.fileKey,
|
|
973
|
+
token: attachment.token,
|
|
974
|
+
url: attachment.url,
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
}
|
|
596
978
|
async function listFeishuChats(source, input, client) {
|
|
597
979
|
const config = parseFeishuSourceConfig(source);
|
|
598
980
|
const raw = await withFeishuChatDiscoveryError("list_chats", () => client.listChats({
|
|
@@ -721,6 +1103,12 @@ function normalizeFeishuBotEvent(source, config, raw) {
|
|
|
721
1103
|
const senderId = asString(asRecord(sender.sender_id).open_id) ?? asString(sender.sender_id);
|
|
722
1104
|
const senderType = asString(sender.sender_type);
|
|
723
1105
|
const content = stringifyFeishuMessageContent(messageType, asString(message.content), message.mentions);
|
|
1106
|
+
const attachments = extractFeishuMessageAttachments({
|
|
1107
|
+
messageId,
|
|
1108
|
+
messageType,
|
|
1109
|
+
rawContent: asString(message.content),
|
|
1110
|
+
rawMessage: message,
|
|
1111
|
+
});
|
|
724
1112
|
const threadId = asString(message.thread_id) ?? asString(message.root_id);
|
|
725
1113
|
const parentId = asString(message.parent_id);
|
|
726
1114
|
return {
|
|
@@ -742,6 +1130,7 @@ function normalizeFeishuBotEvent(source, config, raw) {
|
|
|
742
1130
|
mentions,
|
|
743
1131
|
mentionOpenIds,
|
|
744
1132
|
content,
|
|
1133
|
+
attachments,
|
|
745
1134
|
threadId,
|
|
746
1135
|
parentId,
|
|
747
1136
|
},
|
|
@@ -945,8 +1334,10 @@ function normalizeFeishuMessage(raw) {
|
|
|
945
1334
|
}
|
|
946
1335
|
const sender = asRecord(message.sender);
|
|
947
1336
|
const messageType = asString(message.msg_type) ?? asString(message.message_type) ?? "unknown";
|
|
1337
|
+
const messageId = asString(message.message_id) ?? "";
|
|
1338
|
+
const rawContent = messageContentString(message);
|
|
948
1339
|
return {
|
|
949
|
-
messageId
|
|
1340
|
+
messageId,
|
|
950
1341
|
chatId: asString(message.chat_id),
|
|
951
1342
|
chatType: asString(message.chat_type),
|
|
952
1343
|
messageType,
|
|
@@ -954,10 +1345,16 @@ function normalizeFeishuMessage(raw) {
|
|
|
954
1345
|
senderType: asString(sender.sender_type) ?? asString(asRecord(sender.id).user_id) ?? null,
|
|
955
1346
|
mentions: extractMentionNames(message.mentions),
|
|
956
1347
|
mentionOpenIds: extractMentionOpenIds(message.mentions),
|
|
957
|
-
content: stringifyFeishuMessageContent(messageType,
|
|
1348
|
+
content: stringifyFeishuMessageContent(messageType, rawContent, message.mentions),
|
|
958
1349
|
createdAt: fromUnixMillisString(asString(message.create_time)),
|
|
959
1350
|
threadId: asString(message.thread_id) ?? asString(message.root_id),
|
|
960
1351
|
parentId: asString(message.parent_id),
|
|
1352
|
+
attachments: extractFeishuMessageAttachments({
|
|
1353
|
+
messageId,
|
|
1354
|
+
messageType,
|
|
1355
|
+
rawContent,
|
|
1356
|
+
rawMessage: message,
|
|
1357
|
+
}),
|
|
961
1358
|
raw: message,
|
|
962
1359
|
};
|
|
963
1360
|
}
|
|
@@ -976,6 +1373,146 @@ function normalizeFeishuMessages(raw) {
|
|
|
976
1373
|
.map((item) => normalizeFeishuMessage(item))
|
|
977
1374
|
.filter((item) => item !== null);
|
|
978
1375
|
}
|
|
1376
|
+
function extractFeishuMessageAttachments(input) {
|
|
1377
|
+
const parsedContent = parseJsonContent(input.rawContent);
|
|
1378
|
+
const attachments = [];
|
|
1379
|
+
const content = asRecord(parsedContent);
|
|
1380
|
+
if (input.messageType === "image") {
|
|
1381
|
+
const imageKey = asString(content.image_key);
|
|
1382
|
+
if (imageKey) {
|
|
1383
|
+
attachments.push(makeFeishuAttachment({
|
|
1384
|
+
kind: "image",
|
|
1385
|
+
messageId: input.messageId,
|
|
1386
|
+
fileKey: imageKey,
|
|
1387
|
+
resourceType: "image",
|
|
1388
|
+
raw: content,
|
|
1389
|
+
}));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (input.messageType === "file" || input.messageType === "audio" || input.messageType === "video" || input.messageType === "media") {
|
|
1393
|
+
const fileKey = asString(content.file_key);
|
|
1394
|
+
if (fileKey) {
|
|
1395
|
+
attachments.push(makeFeishuAttachment({
|
|
1396
|
+
kind: input.messageType,
|
|
1397
|
+
messageId: input.messageId,
|
|
1398
|
+
fileKey,
|
|
1399
|
+
resourceType: "file",
|
|
1400
|
+
name: asString(content.file_name) ?? asString(content.name),
|
|
1401
|
+
mimeType: asString(content.mime_type),
|
|
1402
|
+
size: asNumber(content.size),
|
|
1403
|
+
raw: content,
|
|
1404
|
+
}));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const url of extractUrls(parsedContent)) {
|
|
1408
|
+
const link = feishuAttachmentFromUrl(url, input.messageId);
|
|
1409
|
+
if (link) {
|
|
1410
|
+
attachments.push(link);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return dedupeFeishuAttachments(attachments);
|
|
1414
|
+
}
|
|
1415
|
+
function parseJsonContent(rawContent) {
|
|
1416
|
+
if (!rawContent) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
return JSON.parse(rawContent);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
return rawContent;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function makeFeishuAttachment(input) {
|
|
1427
|
+
const stable = [
|
|
1428
|
+
input.messageId ?? "",
|
|
1429
|
+
input.kind,
|
|
1430
|
+
input.fileKey ?? "",
|
|
1431
|
+
input.token ?? "",
|
|
1432
|
+
input.url ?? "",
|
|
1433
|
+
].join(":");
|
|
1434
|
+
return {
|
|
1435
|
+
attachmentId: `att_${(0, node_crypto_1.createHash)("sha1").update(stable).digest("hex").slice(0, 16)}`,
|
|
1436
|
+
provider: "feishu",
|
|
1437
|
+
kind: input.kind,
|
|
1438
|
+
messageId: input.messageId ?? null,
|
|
1439
|
+
fileKey: input.fileKey ?? null,
|
|
1440
|
+
resourceType: input.resourceType ?? null,
|
|
1441
|
+
token: input.token ?? null,
|
|
1442
|
+
url: input.url ?? null,
|
|
1443
|
+
name: input.name ?? null,
|
|
1444
|
+
mimeType: input.mimeType ?? null,
|
|
1445
|
+
size: input.size ?? null,
|
|
1446
|
+
raw: input.raw ?? {},
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function extractUrls(value) {
|
|
1450
|
+
const urls = new Set();
|
|
1451
|
+
const visit = (current) => {
|
|
1452
|
+
if (typeof current === "string") {
|
|
1453
|
+
const matches = current.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
|
|
1454
|
+
for (const match of matches) {
|
|
1455
|
+
urls.add(match.replace(/[),.;,。]+$/, ""));
|
|
1456
|
+
}
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (Array.isArray(current)) {
|
|
1460
|
+
for (const child of current) {
|
|
1461
|
+
visit(child);
|
|
1462
|
+
}
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (current && typeof current === "object") {
|
|
1466
|
+
for (const child of Object.values(current)) {
|
|
1467
|
+
visit(child);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
visit(value);
|
|
1472
|
+
return Array.from(urls).sort();
|
|
1473
|
+
}
|
|
1474
|
+
function feishuAttachmentFromUrl(url, messageId) {
|
|
1475
|
+
let parsed;
|
|
1476
|
+
try {
|
|
1477
|
+
parsed = new URL(url);
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1483
|
+
const patterns = [
|
|
1484
|
+
{ segment: "docx", kind: "docx" },
|
|
1485
|
+
{ segment: "doc", kind: "doc" },
|
|
1486
|
+
{ segment: "wiki", kind: "wiki" },
|
|
1487
|
+
{ segment: "sheets", kind: "sheet" },
|
|
1488
|
+
{ segment: "sheet", kind: "sheet" },
|
|
1489
|
+
{ segment: "base", kind: "bitable" },
|
|
1490
|
+
{ segment: "bitable", kind: "bitable" },
|
|
1491
|
+
{ segment: "file", kind: "drive_file" },
|
|
1492
|
+
];
|
|
1493
|
+
for (const pattern of patterns) {
|
|
1494
|
+
const index = parts.indexOf(pattern.segment);
|
|
1495
|
+
const token = index >= 0 ? parts[index + 1] : null;
|
|
1496
|
+
if (token) {
|
|
1497
|
+
return makeFeishuAttachment({
|
|
1498
|
+
kind: pattern.kind,
|
|
1499
|
+
messageId,
|
|
1500
|
+
token,
|
|
1501
|
+
url,
|
|
1502
|
+
name: token,
|
|
1503
|
+
raw: { url },
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
function dedupeFeishuAttachments(attachments) {
|
|
1510
|
+
const byId = new Map();
|
|
1511
|
+
for (const attachment of attachments) {
|
|
1512
|
+
byId.set(attachment.attachmentId, attachment);
|
|
1513
|
+
}
|
|
1514
|
+
return Array.from(byId.values());
|
|
1515
|
+
}
|
|
979
1516
|
function normalizeFeishuChat(raw) {
|
|
980
1517
|
const chat = firstFeishuChat(raw);
|
|
981
1518
|
if (!chat) {
|
|
@@ -1332,50 +1869,3 @@ function asStringArray(value) {
|
|
|
1332
1869
|
.map((item) => asString(item))
|
|
1333
1870
|
.filter((item) => Boolean(item));
|
|
1334
1871
|
}
|
|
1335
|
-
function ensureFeishuBotSchemaFile() {
|
|
1336
|
-
const body = JSON.stringify(FEISHU_BOT_OPENAPI_SCHEMA);
|
|
1337
|
-
(0, node_fs_1.writeFileSync)(FEISHU_BOT_SCHEMA_FILE, body, "utf8");
|
|
1338
|
-
return FEISHU_BOT_SCHEMA_FILE;
|
|
1339
|
-
}
|
|
1340
|
-
const FEISHU_BOT_OPENAPI_SCHEMA = {
|
|
1341
|
-
openapi: "3.0.3",
|
|
1342
|
-
info: {
|
|
1343
|
-
title: "Feishu Bot Info",
|
|
1344
|
-
version: "1.0.0",
|
|
1345
|
-
},
|
|
1346
|
-
servers: [
|
|
1347
|
-
{
|
|
1348
|
-
url: exports.FEISHU_OPENAPI_ENDPOINT,
|
|
1349
|
-
},
|
|
1350
|
-
],
|
|
1351
|
-
// Feishu's docs site does not currently expose this as a stable
|
|
1352
|
-
// machine-readable OpenAPI document to UXC, so keep the tiny operation
|
|
1353
|
-
// schema needed for bot self-identification close to the adapter.
|
|
1354
|
-
paths: {
|
|
1355
|
-
"/bot/v3/info": {
|
|
1356
|
-
get: {
|
|
1357
|
-
operationId: "get:/bot/v3/info",
|
|
1358
|
-
summary: "Get bot information for the current app",
|
|
1359
|
-
responses: {
|
|
1360
|
-
"200": {
|
|
1361
|
-
description: "OK",
|
|
1362
|
-
},
|
|
1363
|
-
},
|
|
1364
|
-
security: [
|
|
1365
|
-
{
|
|
1366
|
-
bearerAuth: [],
|
|
1367
|
-
},
|
|
1368
|
-
],
|
|
1369
|
-
},
|
|
1370
|
-
},
|
|
1371
|
-
},
|
|
1372
|
-
components: {
|
|
1373
|
-
securitySchemes: {
|
|
1374
|
-
bearerAuth: {
|
|
1375
|
-
type: "http",
|
|
1376
|
-
scheme: "bearer",
|
|
1377
|
-
bearerFormat: "tenant_access_token",
|
|
1378
|
-
},
|
|
1379
|
-
},
|
|
1380
|
-
},
|
|
1381
|
-
};
|