@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.
@@ -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 imageAttachments = (attachments ?? []).filter((a) => (a.type ?? "").toLowerCase() === "image");
922
- // 无内容且无图片时跳过
923
- if (!trimmed && imageAttachments.length === 0) {
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
- // 上传图片获取 image_key(失败时抛出错误,同步传播回 LLM tool result)
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
- // 构建 post content 行
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
- if (contentRows.length === 0) {
972
- return;
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) {
@@ -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 可选,图片用 type=image+path 或 base64);read_doc 读文档(需 url);set_nickname 设置备注(需 sender_id,nickname)。chat_id: ou_xxx 私聊/oc_xxx 群聊。",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmcl/ailo-mcp-feishu",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Ailo 飞书/Lark 通道 MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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<{ type?: string; base64?: string; url?: string; mime?: string; file_path?: string }>
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 imageAttachments =
1067
- (attachments ?? []).filter((a) => (a.type ?? "").toLowerCase() === "image");
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
- // 上传图片获取 image_key(失败时抛出错误,同步传播回 LLM tool result)
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
- // 构建 post content 行
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
- if (contentRows.length === 0) {
1127
- return;
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
- const receiveIdType = chatId.startsWith("ou_") ? "open_id" : "chat_id";
1131
- await this.client.im.v1.message.create({
1132
- params: { receive_id_type: receiveIdType },
1133
- data: {
1134
- receive_id: chatId,
1135
- msg_type: "post",
1136
- content: JSON.stringify({
1137
- zh_cn: {
1138
- content: contentRows,
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 可选,图片用 type=image+path 或 base64);read_doc 读文档(需 url);set_nickname 设置备注(需 sender_id,nickname)。chat_id: ou_xxx 私聊/oc_xxx 群聊。",
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 };