@lmcl/ailo-mcp-feishu 0.0.5 → 0.0.6
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/feishu-handler.js +127 -27
- package/dist/mcp-server.js +6 -2
- package/package.json +1 -1
- package/src/feishu-handler.ts +132 -33
- package/src/mcp-server.ts +6 -2
package/dist/feishu-handler.js
CHANGED
|
@@ -873,6 +873,67 @@ export class FeishuHandler {
|
|
|
873
873
|
});
|
|
874
874
|
wsClient.start({ eventDispatcher });
|
|
875
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* 根据文件名/扩展名推断飞书 im.file.create 的 file_type。
|
|
878
|
+
* 支持: opus | mp4 | pdf | doc | xls | ppt | stream
|
|
879
|
+
*/
|
|
880
|
+
inferFileType(fileName, mime) {
|
|
881
|
+
const ext = path.extname(fileName || "").toLowerCase().slice(1);
|
|
882
|
+
const m = (mime ?? "").toLowerCase();
|
|
883
|
+
if (["mp4", "mov", "avi", "mkv", "webm", "m4v"].includes(ext) || m.includes("video"))
|
|
884
|
+
return "mp4";
|
|
885
|
+
if (["opus", "mp3", "wav", "m4a", "aac", "ogg", "flac"].includes(ext) || m.includes("audio"))
|
|
886
|
+
return "opus";
|
|
887
|
+
if (ext === "pdf" || m.includes("pdf"))
|
|
888
|
+
return "pdf";
|
|
889
|
+
if (["doc", "docx"].includes(ext) || m.includes("msword") || m.includes("document"))
|
|
890
|
+
return "doc";
|
|
891
|
+
if (["xls", "xlsx"].includes(ext) || m.includes("spreadsheet") || m.includes("excel"))
|
|
892
|
+
return "xls";
|
|
893
|
+
if (["ppt", "pptx"].includes(ext) || m.includes("presentation") || m.includes("powerpoint"))
|
|
894
|
+
return "ppt";
|
|
895
|
+
return "stream";
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* 上传文件到飞书 IM,获取 file_key。
|
|
899
|
+
* 支持 file_path 或 base64,用于发送文件/音频/视频消息。
|
|
900
|
+
* 飞书 API: POST /open-apis/im/v1/files (multipart)
|
|
901
|
+
*/
|
|
902
|
+
async uploadFileToFeishu(opts) {
|
|
903
|
+
let fileData;
|
|
904
|
+
if (opts.filePath) {
|
|
905
|
+
if (!fs.existsSync(opts.filePath)) {
|
|
906
|
+
throw new Error(`文件不存在: ${opts.filePath}`);
|
|
907
|
+
}
|
|
908
|
+
fileData = fs.readFileSync(opts.filePath);
|
|
909
|
+
}
|
|
910
|
+
else if (opts.base64) {
|
|
911
|
+
fileData = Buffer.from(opts.base64, "base64");
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
throw new Error("文件上传失败:未提供 file_path 或 base64");
|
|
915
|
+
}
|
|
916
|
+
const fileType = opts.fileType ?? this.inferFileType(opts.fileName, opts.mime);
|
|
917
|
+
let res;
|
|
918
|
+
try {
|
|
919
|
+
res = (await this.client.im.file.create({
|
|
920
|
+
data: {
|
|
921
|
+
file_type: fileType,
|
|
922
|
+
file_name: opts.fileName,
|
|
923
|
+
file: fileData,
|
|
924
|
+
...(opts.duration != null && opts.duration > 0 ? { duration: opts.duration } : {}),
|
|
925
|
+
},
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
const err = e;
|
|
930
|
+
const detail = err?.response?.data ? JSON.stringify(err.response.data) : err?.message;
|
|
931
|
+
throw new Error(`飞书文件上传失败 (${err?.response?.status ?? "unknown"}): ${detail}`);
|
|
932
|
+
}
|
|
933
|
+
if (res?.file_key)
|
|
934
|
+
return res.file_key;
|
|
935
|
+
throw new Error(`飞书文件上传失败(无 file_key): ${JSON.stringify(res)}`);
|
|
936
|
+
}
|
|
876
937
|
/**
|
|
877
938
|
* 上传图片到飞书,获取 image_key。
|
|
878
939
|
* 支持三种来源(优先级:file_path > base64 > url):
|
|
@@ -918,12 +979,23 @@ export class FeishuHandler {
|
|
|
918
979
|
}
|
|
919
980
|
async sendText(chatId, text, attachments) {
|
|
920
981
|
const trimmed = (text ?? "").trim();
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
982
|
+
const allAttachments = attachments ?? [];
|
|
983
|
+
const imageAttachments = allAttachments.filter((a) => (a.type ?? "").toLowerCase() === "image");
|
|
984
|
+
const fileAttachments = allAttachments.filter((a) => {
|
|
985
|
+
const t = (a.type ?? "").toLowerCase();
|
|
986
|
+
if (t === "image")
|
|
987
|
+
return false;
|
|
988
|
+
if (["file", "audio", "video"].includes(t))
|
|
989
|
+
return true;
|
|
990
|
+
// 未指定 type 但有 path/base64 时,视为通用文件
|
|
991
|
+
return !!(a.file_path || a.base64);
|
|
992
|
+
});
|
|
993
|
+
// 无内容且无任何附件时跳过
|
|
994
|
+
if (!trimmed && imageAttachments.length === 0 && fileAttachments.length === 0) {
|
|
924
995
|
return;
|
|
925
996
|
}
|
|
926
|
-
|
|
997
|
+
const receiveIdType = chatId.startsWith("ou_") ? "open_id" : "chat_id";
|
|
998
|
+
// 1. 上传图片获取 image_key
|
|
927
999
|
const imageKeys = [];
|
|
928
1000
|
for (const att of imageAttachments) {
|
|
929
1001
|
if (att.file_path || att.base64) {
|
|
@@ -934,22 +1006,18 @@ export class FeishuHandler {
|
|
|
934
1006
|
});
|
|
935
1007
|
imageKeys.push(key);
|
|
936
1008
|
}
|
|
937
|
-
// url 暂不支持(需先下载再上传,可后续扩展)
|
|
938
1009
|
}
|
|
939
|
-
//
|
|
1010
|
+
// 2. 先发送文本+图片(post),再发送文件/音频/视频
|
|
940
1011
|
const contentRows = [];
|
|
941
1012
|
if (trimmed) {
|
|
942
1013
|
const { cleanText, atElements } = extractMentionElements(trimmed, this.mentionNameToId);
|
|
943
1014
|
const adapted = adaptMarkdownForFeishu(cleanText);
|
|
944
1015
|
const processed = convertMarkdownTablesToCodeBlock(adapted);
|
|
945
|
-
// 按段落拆分为多个 content row(飞书富文本是多段落结构)
|
|
946
1016
|
const paragraphs = processed.split(/\n{2,}/).filter((p) => p.trim());
|
|
947
1017
|
if (paragraphs.length === 0) {
|
|
948
|
-
// 无有效段落,退化为单行
|
|
949
1018
|
contentRows.push([{ tag: "md", text: processed }]);
|
|
950
1019
|
}
|
|
951
1020
|
else {
|
|
952
|
-
// @mention 的 at 元素放在第一个段落的 row 中
|
|
953
1021
|
const firstRow = [];
|
|
954
1022
|
for (const at of atElements) {
|
|
955
1023
|
firstRow.push({ tag: "at", user_id: at.userId });
|
|
@@ -957,33 +1025,65 @@ export class FeishuHandler {
|
|
|
957
1025
|
}
|
|
958
1026
|
firstRow.push({ tag: "md", text: paragraphs[0].trim() });
|
|
959
1027
|
contentRows.push(firstRow);
|
|
960
|
-
// 后续段落各自独立一行
|
|
961
1028
|
for (let i = 1; i < paragraphs.length; i++) {
|
|
962
1029
|
contentRows.push([{ tag: "md", text: paragraphs[i].trim() }]);
|
|
963
1030
|
}
|
|
964
1031
|
}
|
|
965
1032
|
}
|
|
966
|
-
// 添加图片节点(每张图一行)
|
|
967
1033
|
for (const key of imageKeys) {
|
|
968
1034
|
contentRows.push([{ tag: "img", image_key: key }]);
|
|
969
1035
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1036
|
+
if (contentRows.length > 0) {
|
|
1037
|
+
await this.client.im.v1.message.create({
|
|
1038
|
+
params: { receive_id_type: receiveIdType },
|
|
1039
|
+
data: {
|
|
1040
|
+
receive_id: chatId,
|
|
1041
|
+
msg_type: "post",
|
|
1042
|
+
content: JSON.stringify({
|
|
1043
|
+
zh_cn: {
|
|
1044
|
+
content: contentRows,
|
|
1045
|
+
},
|
|
1046
|
+
}),
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
// 3. 上传文件/音频/视频并发送对应消息
|
|
1051
|
+
for (const att of fileAttachments) {
|
|
1052
|
+
if (!att.file_path && !att.base64)
|
|
1053
|
+
continue;
|
|
1054
|
+
const fileName = att.name?.trim() ||
|
|
1055
|
+
(att.file_path ? path.basename(att.file_path) : `file_${Date.now()}`);
|
|
1056
|
+
const fileKey = await this.uploadFileToFeishu({
|
|
1057
|
+
filePath: att.file_path,
|
|
1058
|
+
base64: att.base64,
|
|
1059
|
+
fileName,
|
|
1060
|
+
mime: att.mime,
|
|
1061
|
+
duration: att.duration,
|
|
1062
|
+
});
|
|
1063
|
+
const msgType = (att.type ?? "file").toLowerCase();
|
|
1064
|
+
let content;
|
|
1065
|
+
if (msgType === "audio") {
|
|
1066
|
+
content = JSON.stringify({ file_key: fileKey, duration: att.duration ?? 0 });
|
|
1067
|
+
}
|
|
1068
|
+
else if (msgType === "video") {
|
|
1069
|
+
content = JSON.stringify({
|
|
1070
|
+
file_key: fileKey,
|
|
1071
|
+
file_name: fileName,
|
|
1072
|
+
duration: att.duration ?? 0,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
content = JSON.stringify({ file_key: fileKey, file_name: fileName });
|
|
1077
|
+
}
|
|
1078
|
+
await this.client.im.v1.message.create({
|
|
1079
|
+
params: { receive_id_type: receiveIdType },
|
|
1080
|
+
data: {
|
|
1081
|
+
receive_id: chatId,
|
|
1082
|
+
msg_type: msgType === "video" ? "media" : msgType,
|
|
1083
|
+
content,
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
973
1086
|
}
|
|
974
|
-
const receiveIdType = chatId.startsWith("ou_") ? "open_id" : "chat_id";
|
|
975
|
-
await this.client.im.v1.message.create({
|
|
976
|
-
params: { receive_id_type: receiveIdType },
|
|
977
|
-
data: {
|
|
978
|
-
receive_id: chatId,
|
|
979
|
-
msg_type: "post",
|
|
980
|
-
content: JSON.stringify({
|
|
981
|
-
zh_cn: {
|
|
982
|
-
content: contentRows,
|
|
983
|
-
},
|
|
984
|
-
}),
|
|
985
|
-
},
|
|
986
|
-
});
|
|
987
1087
|
}
|
|
988
1088
|
/** 处理来自 MCP 工具 feishu action=set_nickname 的命令 */
|
|
989
1089
|
async onCommand(command, params) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
const attachmentSchema = z.object({
|
|
7
|
-
type: z.string().optional(),
|
|
7
|
+
type: z.string().optional().describe("image|file|audio|video"),
|
|
8
8
|
path: z.string().optional(),
|
|
9
9
|
base64: z.string().optional(),
|
|
10
10
|
mime: z.string().optional(),
|
|
11
|
+
name: z.string().optional().describe("文件名,用于 file/audio/video"),
|
|
12
|
+
duration: z.number().optional().describe("音视频时长(毫秒),用于 audio/video"),
|
|
11
13
|
});
|
|
12
14
|
const feishuSchema = {
|
|
13
15
|
action: z.enum(["send", "read_doc", "set_nickname"]).describe("send=发消息; read_doc=读文档; set_nickname=设置备注"),
|
|
@@ -21,7 +23,7 @@ const feishuSchema = {
|
|
|
21
23
|
export function createFeishuMcpServer(handler) {
|
|
22
24
|
const server = new McpServer({ name: "feishu", version: "0.1.0" });
|
|
23
25
|
server.registerTool("feishu", {
|
|
24
|
-
description: "飞书操作。action=send 发消息(需 chat_id,text;attachments
|
|
26
|
+
description: "飞书操作。action=send 发消息(需 chat_id,text;attachments 可选):type=image 图片;type=file 文件;type=audio 音频;type=video 视频。附件用 path 或 base64,可选 name/duration。read_doc 读文档(需 url);set_nickname 设置备注(需 sender_id,nickname)。chat_id: ou_xxx 私聊/oc_xxx 群聊。",
|
|
25
27
|
inputSchema: feishuSchema,
|
|
26
28
|
}, async (args) => {
|
|
27
29
|
const { action } = args;
|
|
@@ -31,6 +33,8 @@ export function createFeishuMcpServer(handler) {
|
|
|
31
33
|
file_path: a.path,
|
|
32
34
|
base64: a.base64,
|
|
33
35
|
mime: a.mime,
|
|
36
|
+
name: a.name,
|
|
37
|
+
duration: a.duration,
|
|
34
38
|
}));
|
|
35
39
|
await handler.sendText(args.chat_id, args.text ?? "", atts);
|
|
36
40
|
return { content: [{ type: "text", text: `已发送到 ${args.chat_id}` }], isError: false };
|
package/package.json
CHANGED
package/src/feishu-handler.ts
CHANGED
|
@@ -1009,6 +1009,66 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1009
1009
|
wsClient.start({ eventDispatcher });
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* 根据文件名/扩展名推断飞书 im.file.create 的 file_type。
|
|
1014
|
+
* 支持: opus | mp4 | pdf | doc | xls | ppt | stream
|
|
1015
|
+
*/
|
|
1016
|
+
private inferFileType(fileName: string, mime?: string): "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream" {
|
|
1017
|
+
const ext = path.extname(fileName || "").toLowerCase().slice(1);
|
|
1018
|
+
const m = (mime ?? "").toLowerCase();
|
|
1019
|
+
if (["mp4", "mov", "avi", "mkv", "webm", "m4v"].includes(ext) || m.includes("video")) return "mp4";
|
|
1020
|
+
if (["opus", "mp3", "wav", "m4a", "aac", "ogg", "flac"].includes(ext) || m.includes("audio")) return "opus";
|
|
1021
|
+
if (ext === "pdf" || m.includes("pdf")) return "pdf";
|
|
1022
|
+
if (["doc", "docx"].includes(ext) || m.includes("msword") || m.includes("document")) return "doc";
|
|
1023
|
+
if (["xls", "xlsx"].includes(ext) || m.includes("spreadsheet") || m.includes("excel")) return "xls";
|
|
1024
|
+
if (["ppt", "pptx"].includes(ext) || m.includes("presentation") || m.includes("powerpoint")) return "ppt";
|
|
1025
|
+
return "stream";
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* 上传文件到飞书 IM,获取 file_key。
|
|
1030
|
+
* 支持 file_path 或 base64,用于发送文件/音频/视频消息。
|
|
1031
|
+
* 飞书 API: POST /open-apis/im/v1/files (multipart)
|
|
1032
|
+
*/
|
|
1033
|
+
private async uploadFileToFeishu(opts: {
|
|
1034
|
+
filePath?: string;
|
|
1035
|
+
base64?: string;
|
|
1036
|
+
fileName: string;
|
|
1037
|
+
fileType?: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
|
1038
|
+
mime?: string;
|
|
1039
|
+
duration?: number;
|
|
1040
|
+
}): Promise<string> {
|
|
1041
|
+
let fileData: Buffer;
|
|
1042
|
+
if (opts.filePath) {
|
|
1043
|
+
if (!fs.existsSync(opts.filePath)) {
|
|
1044
|
+
throw new Error(`文件不存在: ${opts.filePath}`);
|
|
1045
|
+
}
|
|
1046
|
+
fileData = fs.readFileSync(opts.filePath);
|
|
1047
|
+
} else if (opts.base64) {
|
|
1048
|
+
fileData = Buffer.from(opts.base64, "base64");
|
|
1049
|
+
} else {
|
|
1050
|
+
throw new Error("文件上传失败:未提供 file_path 或 base64");
|
|
1051
|
+
}
|
|
1052
|
+
const fileType = opts.fileType ?? this.inferFileType(opts.fileName, opts.mime);
|
|
1053
|
+
let res: { file_key?: string };
|
|
1054
|
+
try {
|
|
1055
|
+
res = (await this.client.im.file.create({
|
|
1056
|
+
data: {
|
|
1057
|
+
file_type: fileType,
|
|
1058
|
+
file_name: opts.fileName,
|
|
1059
|
+
file: fileData,
|
|
1060
|
+
...(opts.duration != null && opts.duration > 0 ? { duration: opts.duration } : {}),
|
|
1061
|
+
},
|
|
1062
|
+
})) as { file_key?: string };
|
|
1063
|
+
} catch (e: unknown) {
|
|
1064
|
+
const err = e as { response?: { data?: unknown; status?: number }; message?: string };
|
|
1065
|
+
const detail = err?.response?.data ? JSON.stringify(err.response.data) : err?.message;
|
|
1066
|
+
throw new Error(`飞书文件上传失败 (${err?.response?.status ?? "unknown"}): ${detail}`);
|
|
1067
|
+
}
|
|
1068
|
+
if (res?.file_key) return res.file_key;
|
|
1069
|
+
throw new Error(`飞书文件上传失败(无 file_key): ${JSON.stringify(res)}`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1012
1072
|
/**
|
|
1013
1073
|
* 上传图片到飞书,获取 image_key。
|
|
1014
1074
|
* 支持三种来源(优先级:file_path > base64 > url):
|
|
@@ -1060,18 +1120,35 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1060
1120
|
async sendText(
|
|
1061
1121
|
chatId: string,
|
|
1062
1122
|
text: string,
|
|
1063
|
-
attachments?: Array<{
|
|
1123
|
+
attachments?: Array<{
|
|
1124
|
+
type?: string;
|
|
1125
|
+
base64?: string;
|
|
1126
|
+
url?: string;
|
|
1127
|
+
mime?: string;
|
|
1128
|
+
file_path?: string;
|
|
1129
|
+
name?: string;
|
|
1130
|
+
duration?: number;
|
|
1131
|
+
}>
|
|
1064
1132
|
): Promise<void> {
|
|
1065
1133
|
const trimmed = (text ?? "").trim();
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1134
|
+
const allAttachments = attachments ?? [];
|
|
1135
|
+
const imageAttachments = allAttachments.filter((a) => (a.type ?? "").toLowerCase() === "image");
|
|
1136
|
+
const fileAttachments = allAttachments.filter((a) => {
|
|
1137
|
+
const t = (a.type ?? "").toLowerCase();
|
|
1138
|
+
if (t === "image") return false;
|
|
1139
|
+
if (["file", "audio", "video"].includes(t)) return true;
|
|
1140
|
+
// 未指定 type 但有 path/base64 时,视为通用文件
|
|
1141
|
+
return !!(a.file_path || a.base64);
|
|
1142
|
+
});
|
|
1068
1143
|
|
|
1069
|
-
//
|
|
1070
|
-
if (!trimmed && imageAttachments.length === 0) {
|
|
1144
|
+
// 无内容且无任何附件时跳过
|
|
1145
|
+
if (!trimmed && imageAttachments.length === 0 && fileAttachments.length === 0) {
|
|
1071
1146
|
return;
|
|
1072
1147
|
}
|
|
1073
1148
|
|
|
1074
|
-
|
|
1149
|
+
const receiveIdType = chatId.startsWith("ou_") ? "open_id" : "chat_id";
|
|
1150
|
+
|
|
1151
|
+
// 1. 上传图片获取 image_key
|
|
1075
1152
|
const imageKeys: string[] = [];
|
|
1076
1153
|
for (const att of imageAttachments) {
|
|
1077
1154
|
if (att.file_path || att.base64) {
|
|
@@ -1082,12 +1159,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1082
1159
|
});
|
|
1083
1160
|
imageKeys.push(key);
|
|
1084
1161
|
}
|
|
1085
|
-
// url 暂不支持(需先下载再上传,可后续扩展)
|
|
1086
1162
|
}
|
|
1087
1163
|
|
|
1088
|
-
//
|
|
1164
|
+
// 2. 先发送文本+图片(post),再发送文件/音频/视频
|
|
1089
1165
|
const contentRows: Array<Record<string, string>[]> = [];
|
|
1090
|
-
|
|
1091
1166
|
if (trimmed) {
|
|
1092
1167
|
const { cleanText, atElements } = extractMentionElements(
|
|
1093
1168
|
trimmed,
|
|
@@ -1095,14 +1170,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1095
1170
|
);
|
|
1096
1171
|
const adapted = adaptMarkdownForFeishu(cleanText);
|
|
1097
1172
|
const processed = convertMarkdownTablesToCodeBlock(adapted);
|
|
1098
|
-
|
|
1099
|
-
// 按段落拆分为多个 content row(飞书富文本是多段落结构)
|
|
1100
1173
|
const paragraphs = processed.split(/\n{2,}/).filter((p) => p.trim());
|
|
1101
1174
|
if (paragraphs.length === 0) {
|
|
1102
|
-
// 无有效段落,退化为单行
|
|
1103
1175
|
contentRows.push([{ tag: "md", text: processed }]);
|
|
1104
1176
|
} else {
|
|
1105
|
-
// @mention 的 at 元素放在第一个段落的 row 中
|
|
1106
1177
|
const firstRow: Record<string, string>[] = [];
|
|
1107
1178
|
for (const at of atElements) {
|
|
1108
1179
|
firstRow.push({ tag: "at", user_id: at.userId });
|
|
@@ -1110,36 +1181,64 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1110
1181
|
}
|
|
1111
1182
|
firstRow.push({ tag: "md", text: paragraphs[0].trim() });
|
|
1112
1183
|
contentRows.push(firstRow);
|
|
1113
|
-
// 后续段落各自独立一行
|
|
1114
1184
|
for (let i = 1; i < paragraphs.length; i++) {
|
|
1115
1185
|
contentRows.push([{ tag: "md", text: paragraphs[i].trim() }]);
|
|
1116
1186
|
}
|
|
1117
1187
|
}
|
|
1118
1188
|
}
|
|
1119
|
-
|
|
1120
|
-
// 添加图片节点(每张图一行)
|
|
1121
1189
|
for (const key of imageKeys) {
|
|
1122
1190
|
contentRows.push([{ tag: "img", image_key: key }]);
|
|
1123
1191
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1192
|
+
if (contentRows.length > 0) {
|
|
1193
|
+
await this.client.im.v1.message.create({
|
|
1194
|
+
params: { receive_id_type: receiveIdType },
|
|
1195
|
+
data: {
|
|
1196
|
+
receive_id: chatId,
|
|
1197
|
+
msg_type: "post",
|
|
1198
|
+
content: JSON.stringify({
|
|
1199
|
+
zh_cn: {
|
|
1200
|
+
content: contentRows,
|
|
1201
|
+
},
|
|
1202
|
+
}),
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1128
1205
|
}
|
|
1129
1206
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1207
|
+
// 3. 上传文件/音频/视频并发送对应消息
|
|
1208
|
+
for (const att of fileAttachments) {
|
|
1209
|
+
if (!att.file_path && !att.base64) continue;
|
|
1210
|
+
const fileName =
|
|
1211
|
+
att.name?.trim() ||
|
|
1212
|
+
(att.file_path ? path.basename(att.file_path) : `file_${Date.now()}`);
|
|
1213
|
+
const fileKey = await this.uploadFileToFeishu({
|
|
1214
|
+
filePath: att.file_path,
|
|
1215
|
+
base64: att.base64,
|
|
1216
|
+
fileName,
|
|
1217
|
+
mime: att.mime,
|
|
1218
|
+
duration: att.duration,
|
|
1219
|
+
});
|
|
1220
|
+
const msgType = (att.type ?? "file").toLowerCase();
|
|
1221
|
+
let content: string;
|
|
1222
|
+
if (msgType === "audio") {
|
|
1223
|
+
content = JSON.stringify({ file_key: fileKey, duration: att.duration ?? 0 });
|
|
1224
|
+
} else if (msgType === "video") {
|
|
1225
|
+
content = JSON.stringify({
|
|
1226
|
+
file_key: fileKey,
|
|
1227
|
+
file_name: fileName,
|
|
1228
|
+
duration: att.duration ?? 0,
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
content = JSON.stringify({ file_key: fileKey, file_name: fileName });
|
|
1232
|
+
}
|
|
1233
|
+
await this.client.im.v1.message.create({
|
|
1234
|
+
params: { receive_id_type: receiveIdType },
|
|
1235
|
+
data: {
|
|
1236
|
+
receive_id: chatId,
|
|
1237
|
+
msg_type: msgType === "video" ? "media" : msgType,
|
|
1238
|
+
content,
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1143
1242
|
}
|
|
1144
1243
|
|
|
1145
1244
|
/** 处理来自 MCP 工具 feishu action=set_nickname 的命令 */
|
package/src/mcp-server.ts
CHANGED
|
@@ -6,10 +6,12 @@ import { z } from "zod";
|
|
|
6
6
|
import type { FeishuHandler } from "./feishu-handler.js";
|
|
7
7
|
|
|
8
8
|
const attachmentSchema = z.object({
|
|
9
|
-
type: z.string().optional(),
|
|
9
|
+
type: z.string().optional().describe("image|file|audio|video"),
|
|
10
10
|
path: z.string().optional(),
|
|
11
11
|
base64: z.string().optional(),
|
|
12
12
|
mime: z.string().optional(),
|
|
13
|
+
name: z.string().optional().describe("文件名,用于 file/audio/video"),
|
|
14
|
+
duration: z.number().optional().describe("音视频时长(毫秒),用于 audio/video"),
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
const feishuSchema = {
|
|
@@ -28,7 +30,7 @@ export function createFeishuMcpServer(handler: FeishuHandler): McpServer {
|
|
|
28
30
|
server.registerTool(
|
|
29
31
|
"feishu",
|
|
30
32
|
{
|
|
31
|
-
description: "飞书操作。action=send 发消息(需 chat_id,text;attachments
|
|
33
|
+
description: "飞书操作。action=send 发消息(需 chat_id,text;attachments 可选):type=image 图片;type=file 文件;type=audio 音频;type=video 视频。附件用 path 或 base64,可选 name/duration。read_doc 读文档(需 url);set_nickname 设置备注(需 sender_id,nickname)。chat_id: ou_xxx 私聊/oc_xxx 群聊。",
|
|
32
34
|
inputSchema: feishuSchema,
|
|
33
35
|
},
|
|
34
36
|
async (args) => {
|
|
@@ -39,6 +41,8 @@ export function createFeishuMcpServer(handler: FeishuHandler): McpServer {
|
|
|
39
41
|
file_path: a.path,
|
|
40
42
|
base64: a.base64,
|
|
41
43
|
mime: a.mime,
|
|
44
|
+
name: a.name,
|
|
45
|
+
duration: a.duration,
|
|
42
46
|
}));
|
|
43
47
|
await handler.sendText(args.chat_id!, args.text ?? "", atts);
|
|
44
48
|
return { content: [{ type: "text" as const, text: `已发送到 ${args.chat_id}` }], isError: false };
|