opencode-feishu 0.3.7 → 0.4.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 NeverMore93
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -98761,6 +98761,256 @@ function isDuplicate(messageId) {
98761
98761
  return false;
98762
98762
  }
98763
98763
 
98764
+ // src/feishu/resource.ts
98765
+ var MAX_RESOURCE_SIZE = 10 * 1024 * 1024;
98766
+ async function downloadMessageResource(client, messageId, fileKey, type, log) {
98767
+ try {
98768
+ const res = await client.im.messageResource.get({
98769
+ path: { message_id: messageId, file_key: fileKey },
98770
+ params: { type }
98771
+ });
98772
+ if (!res) {
98773
+ log("warn", "\u8D44\u6E90\u4E0B\u8F7D\u8FD4\u56DE\u7A7A\u6570\u636E", { messageId, fileKey, type });
98774
+ return null;
98775
+ }
98776
+ const stream4 = res.getReadableStream();
98777
+ const chunks = [];
98778
+ let totalSize = 0;
98779
+ for await (const chunk of stream4) {
98780
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
98781
+ totalSize += buf.length;
98782
+ if (totalSize > MAX_RESOURCE_SIZE) {
98783
+ log("warn", "\u8D44\u6E90\u8FC7\u5927\uFF0C\u8DF3\u8FC7\u4E0B\u8F7D", { messageId, fileKey, totalSize });
98784
+ stream4.destroy();
98785
+ return null;
98786
+ }
98787
+ chunks.push(buf);
98788
+ }
98789
+ const buffer = Buffer.concat(chunks);
98790
+ const headers = res.headers;
98791
+ const contentType = headers?.["content-type"] ?? guessMimeByType(type);
98792
+ const base64 = buffer.toString("base64");
98793
+ const dataUrl = `data:${contentType};base64,${base64}`;
98794
+ return { dataUrl, mime: contentType };
98795
+ } catch (err) {
98796
+ log("warn", "\u8D44\u6E90\u4E0B\u8F7D\u5931\u8D25", {
98797
+ messageId,
98798
+ fileKey,
98799
+ type,
98800
+ error: err instanceof Error ? err.message : String(err)
98801
+ });
98802
+ return null;
98803
+ }
98804
+ }
98805
+ function guessMimeByType(type) {
98806
+ return type === "image" ? "image/png" : "application/octet-stream";
98807
+ }
98808
+ function guessMimeByFilename(filename) {
98809
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
98810
+ const map = {
98811
+ png: "image/png",
98812
+ jpg: "image/jpeg",
98813
+ jpeg: "image/jpeg",
98814
+ gif: "image/gif",
98815
+ webp: "image/webp",
98816
+ svg: "image/svg+xml",
98817
+ bmp: "image/bmp",
98818
+ pdf: "application/pdf",
98819
+ doc: "application/msword",
98820
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
98821
+ xls: "application/vnd.ms-excel",
98822
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
98823
+ ppt: "application/vnd.ms-powerpoint",
98824
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
98825
+ txt: "text/plain",
98826
+ csv: "text/csv",
98827
+ json: "application/json",
98828
+ xml: "application/xml",
98829
+ zip: "application/zip",
98830
+ mp3: "audio/mpeg",
98831
+ wav: "audio/wav",
98832
+ ogg: "audio/ogg",
98833
+ opus: "audio/opus",
98834
+ mp4: "video/mp4",
98835
+ webm: "video/webm",
98836
+ mov: "video/quicktime"
98837
+ };
98838
+ return map[ext] ?? "application/octet-stream";
98839
+ }
98840
+
98841
+ // src/feishu/content-extractor.ts
98842
+ async function extractParts(feishuClient, messageId, messageType, rawContent, log) {
98843
+ try {
98844
+ switch (messageType) {
98845
+ case "text":
98846
+ return extractText(rawContent);
98847
+ case "image":
98848
+ return await extractImage(feishuClient, messageId, rawContent, log);
98849
+ case "post":
98850
+ return extractPost(rawContent);
98851
+ case "file":
98852
+ return await extractFile(feishuClient, messageId, rawContent, log);
98853
+ case "audio":
98854
+ return await extractAudio(feishuClient, messageId, rawContent, log);
98855
+ case "media":
98856
+ return extractMediaFallback();
98857
+ case "sticker":
98858
+ return [{ type: "text", text: "[\u8868\u60C5\u5305]" }];
98859
+ case "interactive":
98860
+ return extractInteractive(rawContent);
98861
+ case "share_chat":
98862
+ return extractShareChat(rawContent);
98863
+ case "share_user":
98864
+ return [{ type: "text", text: "[\u5206\u4EAB\u4E86\u4E00\u4E2A\u7528\u6237\u540D\u7247]" }];
98865
+ case "merge_forward":
98866
+ return [{ type: "text", text: "[\u5408\u5E76\u8F6C\u53D1\u6D88\u606F]" }];
98867
+ default:
98868
+ return [{ type: "text", text: `[\u4E0D\u652F\u6301\u7684\u6D88\u606F\u7C7B\u578B: ${messageType}]` }];
98869
+ }
98870
+ } catch (err) {
98871
+ log("warn", "\u6D88\u606F\u5185\u5BB9\u63D0\u53D6\u5931\u8D25", {
98872
+ messageId,
98873
+ messageType,
98874
+ error: err instanceof Error ? err.message : String(err)
98875
+ });
98876
+ return [{ type: "text", text: `[\u6D88\u606F\u5185\u5BB9\u63D0\u53D6\u5931\u8D25: ${messageType}]` }];
98877
+ }
98878
+ }
98879
+ function describeMessageType(messageType, rawContent) {
98880
+ switch (messageType) {
98881
+ case "text": {
98882
+ try {
98883
+ const parsed = JSON.parse(rawContent);
98884
+ return (parsed.text ?? "").trim();
98885
+ } catch {
98886
+ return "";
98887
+ }
98888
+ }
98889
+ case "image":
98890
+ return "[\u56FE\u7247]";
98891
+ case "post":
98892
+ return extractPostText(rawContent);
98893
+ case "file": {
98894
+ try {
98895
+ const parsed = JSON.parse(rawContent);
98896
+ return `[\u6587\u4EF6: ${parsed.file_name ?? "\u672A\u77E5\u6587\u4EF6"}]`;
98897
+ } catch {
98898
+ return "[\u6587\u4EF6]";
98899
+ }
98900
+ }
98901
+ case "audio":
98902
+ return "[\u8BED\u97F3\u6D88\u606F]";
98903
+ case "media":
98904
+ return "[\u89C6\u9891\u6D88\u606F]";
98905
+ case "sticker":
98906
+ return "[\u8868\u60C5\u5305]";
98907
+ case "interactive":
98908
+ return "[\u5361\u7247\u6D88\u606F]";
98909
+ case "share_chat":
98910
+ return "[\u7FA4\u5206\u4EAB]";
98911
+ case "share_user":
98912
+ return "[\u7528\u6237\u540D\u7247]";
98913
+ case "merge_forward":
98914
+ return "[\u5408\u5E76\u8F6C\u53D1]";
98915
+ default:
98916
+ return `[${messageType}]`;
98917
+ }
98918
+ }
98919
+ function extractText(rawContent) {
98920
+ const parsed = JSON.parse(rawContent);
98921
+ const text2 = (parsed.text ?? "").trim();
98922
+ if (!text2) return [];
98923
+ return [{ type: "text", text: text2 }];
98924
+ }
98925
+ async function extractImage(client, messageId, rawContent, log) {
98926
+ const parsed = JSON.parse(rawContent);
98927
+ const imageKey = parsed.image_key;
98928
+ if (!imageKey) return [{ type: "text", text: "[\u56FE\u7247: \u65E0\u6CD5\u83B7\u53D6]" }];
98929
+ const resource = await downloadMessageResource(client, messageId, imageKey, "image", log);
98930
+ if (!resource) return [{ type: "text", text: "[\u56FE\u7247: \u4E0B\u8F7D\u5931\u8D25]" }];
98931
+ return [{ type: "file", mime: resource.mime, url: resource.dataUrl }];
98932
+ }
98933
+ function extractPost(rawContent) {
98934
+ const text2 = extractPostText(rawContent);
98935
+ if (!text2) return [];
98936
+ return [{ type: "text", text: text2 }];
98937
+ }
98938
+ function extractPostText(rawContent) {
98939
+ try {
98940
+ const parsed = JSON.parse(rawContent);
98941
+ const lines = [];
98942
+ if (parsed.title) lines.push(parsed.title);
98943
+ if (Array.isArray(parsed.content)) {
98944
+ for (const paragraph of parsed.content) {
98945
+ if (!Array.isArray(paragraph)) continue;
98946
+ const segments = [];
98947
+ for (const element of paragraph) {
98948
+ if (element.tag === "text" && element.text) {
98949
+ segments.push(element.text);
98950
+ } else if (element.tag === "a" && element.text) {
98951
+ segments.push(element.href ? `${element.text}(${element.href})` : element.text);
98952
+ } else if (element.tag === "at" && element.text) {
98953
+ segments.push(element.text);
98954
+ } else if (element.tag === "img") {
98955
+ segments.push("[\u56FE\u7247]");
98956
+ }
98957
+ }
98958
+ if (segments.length) lines.push(segments.join(""));
98959
+ }
98960
+ }
98961
+ return lines.join("\n").trim();
98962
+ } catch {
98963
+ return "";
98964
+ }
98965
+ }
98966
+ async function extractFile(client, messageId, rawContent, log) {
98967
+ const parsed = JSON.parse(rawContent);
98968
+ const fileKey = parsed.file_key;
98969
+ const fileName = parsed.file_name ?? "\u672A\u77E5\u6587\u4EF6";
98970
+ if (!fileKey) return [{ type: "text", text: `[\u6587\u4EF6: ${fileName}]` }];
98971
+ const mime = guessMimeByFilename(fileName);
98972
+ const resource = await downloadMessageResource(client, messageId, fileKey, "file", log);
98973
+ if (!resource) return [{ type: "text", text: `[\u6587\u4EF6\u4E0B\u8F7D\u5931\u8D25: ${fileName}]` }];
98974
+ return [{ type: "file", mime: resource.mime || mime, url: resource.dataUrl, filename: fileName }];
98975
+ }
98976
+ async function extractAudio(client, messageId, rawContent, log) {
98977
+ const parsed = JSON.parse(rawContent);
98978
+ const fileKey = parsed.file_key;
98979
+ if (!fileKey) return [{ type: "text", text: "[\u8BED\u97F3: \u65E0\u6CD5\u83B7\u53D6]" }];
98980
+ const resource = await downloadMessageResource(client, messageId, fileKey, "file", log);
98981
+ if (!resource) return [{ type: "text", text: "[\u8BED\u97F3: \u4E0B\u8F7D\u5931\u8D25]" }];
98982
+ return [{ type: "file", mime: resource.mime || "audio/opus", url: resource.dataUrl }];
98983
+ }
98984
+ function extractMediaFallback() {
98985
+ return [{ type: "text", text: "[\u89C6\u9891\u6D88\u606F]" }];
98986
+ }
98987
+ function extractInteractive(rawContent) {
98988
+ try {
98989
+ const parsed = JSON.parse(rawContent);
98990
+ const texts = [];
98991
+ if (parsed.header?.title?.content) texts.push(parsed.header.title.content);
98992
+ if (Array.isArray(parsed.elements)) {
98993
+ for (const el of parsed.elements) {
98994
+ if (el.tag === "div" && el.text?.content) texts.push(el.text.content);
98995
+ else if (el.tag === "markdown" && el.content) texts.push(el.content);
98996
+ }
98997
+ }
98998
+ const text2 = texts.join("\n").trim();
98999
+ return text2 ? [{ type: "text", text: `[\u5361\u7247\u6D88\u606F]
99000
+ ${text2}` }] : [{ type: "text", text: "[\u5361\u7247\u6D88\u606F]" }];
99001
+ } catch {
99002
+ return [{ type: "text", text: "[\u5361\u7247\u6D88\u606F]" }];
99003
+ }
99004
+ }
99005
+ function extractShareChat(rawContent) {
99006
+ try {
99007
+ const parsed = JSON.parse(rawContent);
99008
+ return [{ type: "text", text: `[\u5206\u4EAB\u4E86\u4E00\u4E2A\u7FA4\u804A${parsed.chat_id ? `: ${parsed.chat_id}` : ""}]` }];
99009
+ } catch {
99010
+ return [{ type: "text", text: "[\u7FA4\u5206\u4EAB]" }];
99011
+ }
99012
+ }
99013
+
98764
99014
  // src/feishu/group-filter.ts
98765
99015
  function isBotMentioned(mentions, botOpenId) {
98766
99016
  return mentions.some((m) => m.id?.open_id === botOpenId);
@@ -98796,21 +99046,18 @@ function startFeishuGateway(options) {
98796
99046
  const messageId = message.message_id;
98797
99047
  if (isDuplicate(messageId)) return;
98798
99048
  const messageType = message.message_type ?? "text";
99049
+ const rawContent = message.content ?? "";
98799
99050
  log("info", "\u98DE\u4E66\u6D88\u606F\u5143\u4FE1\u606F", {
98800
99051
  chatId,
98801
99052
  messageId: messageId ?? "",
98802
99053
  messageType,
98803
- hasContent: !!message.content
99054
+ hasContent: !!rawContent
98804
99055
  });
98805
- if (messageType !== "text" || !message.content) return;
98806
- let text2;
98807
- try {
98808
- const parsed = JSON.parse(message.content);
98809
- text2 = (parsed.text ?? "").trim();
98810
- } catch {
98811
- return;
99056
+ if (!rawContent) return;
99057
+ let text2 = describeMessageType(messageType, rawContent);
99058
+ if (messageType === "text") {
99059
+ text2 = text2.replace(/@_user_\d+\s*/g, "").trim();
98812
99060
  }
98813
- text2 = text2.replace(/@_user_\d+\s*/g, "").trim();
98814
99061
  if (!text2) return;
98815
99062
  const chatType = message.chat_type === "group" ? "group" : "p2p";
98816
99063
  let shouldReply = true;
@@ -98830,6 +99077,7 @@ function startFeishuGateway(options) {
98830
99077
  messageId: messageId ?? "",
98831
99078
  messageType,
98832
99079
  content: text2,
99080
+ rawContent,
98833
99081
  chatType,
98834
99082
  senderId,
98835
99083
  rootId,
@@ -98945,14 +99193,19 @@ async function handleEvent(event) {
98945
99193
  if (!sessionId) break;
98946
99194
  const payload = pendingBySession.get(sessionId);
98947
99195
  if (!payload) break;
98948
- const added = extractPartText(part);
98949
- if (added) {
98950
- payload.textBuffer += added;
98951
- try {
98952
- await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
98953
- } catch {
99196
+ const delta = event.properties.delta;
99197
+ if (delta) {
99198
+ payload.textBuffer += delta;
99199
+ } else {
99200
+ const fullText = extractPartText(part);
99201
+ if (fullText) {
99202
+ payload.textBuffer = fullText;
98954
99203
  }
98955
99204
  }
99205
+ if (payload.textBuffer) {
99206
+ const res = await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
99207
+ if (!res.ok) ;
99208
+ }
98956
99209
  break;
98957
99210
  }
98958
99211
  case "session.error": {
@@ -98962,9 +99215,8 @@ async function handleEvent(event) {
98962
99215
  const payload = pendingBySession.get(sessionId);
98963
99216
  if (!payload) break;
98964
99217
  const errMsg = props.error?.message ?? String(props.error);
98965
- try {
98966
- await updateMessage(payload.feishuClient, payload.placeholderId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
98967
- } catch {
99218
+ const updateRes = await updateMessage(payload.feishuClient, payload.placeholderId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
99219
+ if (!updateRes.ok) {
98968
99220
  await sendTextMessage(payload.feishuClient, payload.chatId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
98969
99221
  }
98970
99222
  break;
@@ -99016,26 +99268,21 @@ async function getOrCreateSession(client, sessionKey, directory) {
99016
99268
 
99017
99269
  // src/handler/chat.ts
99018
99270
  async function handleChat(ctx, deps) {
99019
- const { content, chatId, chatType, senderId, createTime, shouldReply } = ctx;
99020
- if (!content.trim()) return;
99271
+ const { content, chatId, chatType, senderId, shouldReply, messageType, rawContent, messageId } = ctx;
99272
+ if (!content.trim() && messageType === "text") return;
99021
99273
  const { config, client, feishuClient, log, directory } = deps;
99022
99274
  const query = directory ? { directory } : void 0;
99023
99275
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
99024
99276
  const session = await getOrCreateSession(client, sessionKey, directory);
99025
- const timeStr = createTime ? new Date(Number(createTime)).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) : "";
99026
- let promptContent = content;
99027
- if (chatType === "group" && senderId) {
99028
- promptContent = timeStr ? `[${timeStr}] [${senderId}]: ${content}` : `[${senderId}]: ${content}`;
99029
- } else if (timeStr) {
99030
- promptContent = `[${timeStr}] ${content}`;
99031
- }
99277
+ const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
99278
+ if (!parts.length) return;
99032
99279
  if (!shouldReply) {
99033
99280
  try {
99034
99281
  await client.session.prompt({
99035
99282
  path: { id: session.id },
99036
99283
  query,
99037
99284
  body: {
99038
- parts: [{ type: "text", text: promptContent }],
99285
+ parts,
99039
99286
  noReply: true
99040
99287
  }
99041
99288
  });
@@ -99056,11 +99303,16 @@ async function handleChat(ctx, deps) {
99056
99303
  if (done) return;
99057
99304
  try {
99058
99305
  const res = await sendTextMessage(feishuClient, chatId, "\u6B63\u5728\u601D\u8003\u2026");
99306
+ if (done) return;
99059
99307
  if (res.ok && res.messageId) {
99060
99308
  placeholderId = res.messageId;
99061
99309
  registerPending(session.id, { chatId, placeholderId, feishuClient });
99062
99310
  }
99063
- } catch {
99311
+ } catch (err) {
99312
+ log("warn", "\u53D1\u9001\u5360\u4F4D\u6D88\u606F\u5931\u8D25", {
99313
+ chatId,
99314
+ error: err instanceof Error ? err.message : String(err)
99315
+ });
99064
99316
  }
99065
99317
  }, thinkingDelay) : null;
99066
99318
  try {
@@ -99068,7 +99320,7 @@ async function handleChat(ctx, deps) {
99068
99320
  path: { id: session.id },
99069
99321
  query,
99070
99322
  body: {
99071
- parts: [{ type: "text", text: promptContent }]
99323
+ parts
99072
99324
  }
99073
99325
  });
99074
99326
  const start = Date.now();
@@ -99081,12 +99333,6 @@ async function handleChat(ctx, deps) {
99081
99333
  if (text2 && text2 !== lastText) {
99082
99334
  lastText = text2;
99083
99335
  sameCount = 0;
99084
- if (placeholderId) {
99085
- try {
99086
- await updateMessage(feishuClient, placeholderId, text2);
99087
- } catch {
99088
- }
99089
- }
99090
99336
  } else if (text2 && text2.length > 0) {
99091
99337
  sameCount++;
99092
99338
  if (sameCount >= stablePolls) break;
@@ -99107,11 +99353,24 @@ async function handleChat(ctx, deps) {
99107
99353
  unregisterPending(session.id);
99108
99354
  }
99109
99355
  }
99356
+ async function buildPromptParts(feishuClient, messageId, messageType, rawContent, textContent, chatType, senderId, log) {
99357
+ if (messageType === "text") {
99358
+ let promptText = textContent;
99359
+ if (chatType === "group" && senderId) {
99360
+ promptText = `[${senderId}]: ${textContent}`;
99361
+ }
99362
+ return [{ type: "text", text: promptText }];
99363
+ }
99364
+ const parts = await extractParts(feishuClient, messageId, messageType, rawContent, log);
99365
+ if (chatType === "group" && senderId && parts.length > 0) {
99366
+ return [{ type: "text", text: `[${senderId}]:` }, ...parts];
99367
+ }
99368
+ return parts;
99369
+ }
99110
99370
  async function replyOrUpdate(feishuClient, chatId, placeholderId, text2) {
99111
99371
  if (placeholderId) {
99112
- try {
99113
- await updateMessage(feishuClient, placeholderId, text2);
99114
- } catch {
99372
+ const res = await updateMessage(feishuClient, placeholderId, text2);
99373
+ if (!res.ok) {
99115
99374
  await sendTextMessage(feishuClient, chatId, text2);
99116
99375
  }
99117
99376
  } else {
@@ -99127,7 +99386,8 @@ function extractLastAssistantText(messages) {
99127
99386
  // src/feishu/history.ts
99128
99387
  var DEFAULT_PAGE_SIZE = 50;
99129
99388
  async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options) {
99130
- const { maxMessages, log } = options;
99389
+ const { maxMessages, log, directory } = options;
99390
+ const query = directory ? { directory } : void 0;
99131
99391
  log("info", "\u5F00\u59CB\u6444\u5165\u7FA4\u804A\u5386\u53F2\u4E0A\u4E0B\u6587", { chatId, maxMessages });
99132
99392
  const messages = await fetchRecentMessages(feishuClient, chatId, maxMessages, log);
99133
99393
  if (!messages.length) {
@@ -99135,10 +99395,11 @@ async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options)
99135
99395
  return;
99136
99396
  }
99137
99397
  const sessionKey = buildSessionKey("group", chatId);
99138
- const session = await getOrCreateSession(opencodeClient, sessionKey);
99398
+ const session = await getOrCreateSession(opencodeClient, sessionKey, directory);
99139
99399
  const contextText = formatHistoryAsContext(messages);
99140
99400
  await opencodeClient.session.prompt({
99141
99401
  path: { id: session.id },
99402
+ query,
99142
99403
  body: {
99143
99404
  parts: [{ type: "text", text: contextText }],
99144
99405
  noReply: true
@@ -99164,14 +99425,10 @@ async function fetchRecentMessages(client, chatId, maxMessages, log) {
99164
99425
  if (!items || items.length === 0) break;
99165
99426
  for (const item of items) {
99166
99427
  if (item.deleted) continue;
99167
- if (item.msg_type !== "text" || !item.body?.content) continue;
99168
- let text2;
99169
- try {
99170
- const parsed = JSON.parse(item.body.content);
99171
- text2 = (parsed.text ?? "").trim();
99172
- } catch {
99173
- continue;
99174
- }
99428
+ if (!item.body?.content) continue;
99429
+ const msgType = item.msg_type ?? "text";
99430
+ const rawContent = item.body.content;
99431
+ const text2 = describeMessageType(msgType, rawContent);
99175
99432
  if (!text2) continue;
99176
99433
  result.push({
99177
99434
  senderType: item.sender?.sender_type ?? "unknown",
@@ -99218,7 +99475,8 @@ var DEFAULT_CONFIG = {
99218
99475
  maxHistoryMessages: 200,
99219
99476
  pollInterval: 1e3,
99220
99477
  stablePolls: 3,
99221
- dedupTtl: 10 * 60 * 1e3
99478
+ dedupTtl: 10 * 60 * 1e3,
99479
+ directory: ""
99222
99480
  };
99223
99481
  var FeishuPlugin = async (ctx) => {
99224
99482
  const { client } = ctx;
@@ -99251,6 +99509,12 @@ var FeishuPlugin = async (ctx) => {
99251
99509
  } catch (parseErr) {
99252
99510
  throw new Error(`\u98DE\u4E66\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF\uFF1A${configPath} \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON (${parseErr})`);
99253
99511
  }
99512
+ if (feishuRaw.directory !== void 0 && typeof feishuRaw.directory !== "string") {
99513
+ log("warn", `\u98DE\u4E66\u914D\u7F6E\u8B66\u544A\uFF1A${configPath} \u4E2D\u7684 'directory' \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u5DF2\u5FFD\u7565`, {
99514
+ actualType: typeof feishuRaw.directory
99515
+ });
99516
+ feishuRaw.directory = void 0;
99517
+ }
99254
99518
  if (!feishuRaw.appId || !feishuRaw.appSecret) {
99255
99519
  throw new Error(
99256
99520
  `\u98DE\u4E66\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A${configPath} \u4E2D\u5FC5\u987B\u5305\u542B appId \u548C appSecret`
@@ -99265,7 +99529,8 @@ var FeishuPlugin = async (ctx) => {
99265
99529
  maxHistoryMessages: feishuRaw.maxHistoryMessages ?? DEFAULT_CONFIG.maxHistoryMessages,
99266
99530
  pollInterval: feishuRaw.pollInterval ?? DEFAULT_CONFIG.pollInterval,
99267
99531
  stablePolls: feishuRaw.stablePolls ?? DEFAULT_CONFIG.stablePolls,
99268
- dedupTtl: feishuRaw.dedupTtl ?? DEFAULT_CONFIG.dedupTtl
99532
+ dedupTtl: feishuRaw.dedupTtl ?? DEFAULT_CONFIG.dedupTtl,
99533
+ directory: expandDirectoryPath(feishuRaw.directory ?? ctx.directory ?? DEFAULT_CONFIG.directory)
99269
99534
  };
99270
99535
  initDedup(resolvedConfig.dedupTtl);
99271
99536
  const botOpenId = await fetchBotOpenId(resolvedConfig.appId, resolvedConfig.appSecret, log);
@@ -99279,14 +99544,15 @@ var FeishuPlugin = async (ctx) => {
99279
99544
  client,
99280
99545
  feishuClient: gateway.client,
99281
99546
  log,
99282
- directory: ctx.directory
99547
+ directory: resolvedConfig.directory
99283
99548
  });
99284
99549
  },
99285
99550
  onBotAdded: (chatId) => {
99286
99551
  if (!gateway) return;
99287
99552
  ingestGroupHistory(gateway.client, client, chatId, {
99288
99553
  maxMessages: resolvedConfig.maxHistoryMessages,
99289
- log
99554
+ log,
99555
+ directory: resolvedConfig.directory
99290
99556
  }).catch((err) => {
99291
99557
  log("error", "\u7FA4\u804A\u5386\u53F2\u6444\u5165\u5931\u8D25", {
99292
99558
  chatId,
@@ -99308,6 +99574,20 @@ var FeishuPlugin = async (ctx) => {
99308
99574
  };
99309
99575
  return hooks;
99310
99576
  };
99577
+ function expandDirectoryPath(dir) {
99578
+ if (!dir) return dir;
99579
+ if (dir.startsWith("~")) {
99580
+ dir = join(homedir(), dir.slice(1));
99581
+ }
99582
+ dir = dir.replace(/\$\{(\w+)\}/g, (_match, name) => {
99583
+ const val = process.env[name];
99584
+ if (val === void 0) {
99585
+ throw new Error(`\u73AF\u5883\u53D8\u91CF ${name} \u672A\u8BBE\u7F6E\uFF08directory \u5F15\u7528\u4E86 \${${name}}\uFF09`);
99586
+ }
99587
+ return val;
99588
+ });
99589
+ return dir;
99590
+ }
99311
99591
  function resolveEnvPlaceholders(obj) {
99312
99592
  if (typeof obj === "string") {
99313
99593
  if (!obj.includes("${")) return obj;