opencode-feishu 0.3.6 → 0.4.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/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import fs, { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
+ import https, { request } from 'https';
5
+ import { text } from 'stream/consumers';
6
+ import { HttpsProxyAgent } from 'https-proxy-agent';
4
7
  import crypto2 from 'crypto';
5
8
  import url from 'url';
6
9
  import http from 'http';
7
- import https from 'https';
8
10
  import http2 from 'http2';
9
11
  import util2 from 'util';
10
12
  import zlib from 'zlib';
@@ -12,7 +14,6 @@ import stream3, { Readable } from 'stream';
12
14
  import { EventEmitter } from 'events';
13
15
  import qs from 'querystring';
14
16
  import WebSocket from 'ws';
15
- import { HttpsProxyAgent } from 'https-proxy-agent';
16
17
 
17
18
  var __create = Object.create;
18
19
  var __defProp = Object.defineProperty;
@@ -18949,9 +18950,9 @@ var trackStream = (stream4, chunkSize, onProgress, onFinish) => {
18949
18950
  // node_modules/axios/lib/adapters/fetch.js
18950
18951
  var DEFAULT_CHUNK_SIZE = 64 * 1024;
18951
18952
  var { isFunction: isFunction2 } = utils_default;
18952
- var globalFetchAPI = (({ Request, Response }) => ({
18953
+ var globalFetchAPI = (({ Request, Response: Response2 }) => ({
18953
18954
  Request,
18954
- Response
18955
+ Response: Response2
18955
18956
  }))(utils_default.global);
18956
18957
  var {
18957
18958
  ReadableStream: ReadableStream2,
@@ -18968,10 +18969,10 @@ var factory = (env) => {
18968
18969
  env = utils_default.merge.call({
18969
18970
  skipUndefined: true
18970
18971
  }, globalFetchAPI, env);
18971
- const { fetch: envFetch, Request, Response } = env;
18972
+ const { fetch: envFetch, Request, Response: Response2 } = env;
18972
18973
  const isFetchSupported = envFetch ? isFunction2(envFetch) : typeof fetch === "function";
18973
18974
  const isRequestSupported = isFunction2(Request);
18974
- const isResponseSupported = isFunction2(Response);
18975
+ const isResponseSupported = isFunction2(Response2);
18975
18976
  if (!isFetchSupported) {
18976
18977
  return false;
18977
18978
  }
@@ -18989,7 +18990,7 @@ var factory = (env) => {
18989
18990
  }).headers.has("Content-Type");
18990
18991
  return duplexAccessed && !hasContentType;
18991
18992
  });
18992
- const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils_default.isReadableStream(new Response("").body));
18993
+ const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils_default.isReadableStream(new Response2("").body));
18993
18994
  const resolvers = {
18994
18995
  stream: supportsResponseStream && ((res) => res.body)
18995
18996
  };
@@ -19100,7 +19101,7 @@ var factory = (env) => {
19100
19101
  responseContentLength,
19101
19102
  progressEventReducer(asyncDecorator(onDownloadProgress), true)
19102
19103
  ) || [];
19103
- response = new Response(
19104
+ response = new Response2(
19104
19105
  trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
19105
19106
  flush && flush();
19106
19107
  unsubscribe && unsubscribe();
@@ -19138,10 +19139,10 @@ var factory = (env) => {
19138
19139
  var seedCache = /* @__PURE__ */ new Map();
19139
19140
  var getFetch = (config) => {
19140
19141
  let env = config && config.env || {};
19141
- const { fetch: fetch2, Request, Response } = env;
19142
+ const { fetch: fetch2, Request, Response: Response2 } = env;
19142
19143
  const seeds = [
19143
19144
  Request,
19144
- Response,
19145
+ Response2,
19145
19146
  fetch2
19146
19147
  ];
19147
19148
  let len = seeds.length, i = len, seed, target, map = seedCache;
@@ -98760,6 +98761,256 @@ function isDuplicate(messageId) {
98760
98761
  return false;
98761
98762
  }
98762
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
+
98763
99014
  // src/feishu/group-filter.ts
98764
99015
  function isBotMentioned(mentions, botOpenId) {
98765
99016
  return mentions.some((m) => m.id?.open_id === botOpenId);
@@ -98795,22 +99046,19 @@ function startFeishuGateway(options) {
98795
99046
  const messageId = message.message_id;
98796
99047
  if (isDuplicate(messageId)) return;
98797
99048
  const messageType = message.message_type ?? "text";
99049
+ const rawContent = message.content ?? "";
98798
99050
  log("info", "\u98DE\u4E66\u6D88\u606F\u5143\u4FE1\u606F", {
98799
99051
  chatId,
98800
99052
  messageId: messageId ?? "",
98801
99053
  messageType,
98802
- hasContent: !!message.content
99054
+ hasContent: !!rawContent
98803
99055
  });
98804
- if (messageType !== "text" || !message.content) return;
98805
- let text;
98806
- try {
98807
- const parsed = JSON.parse(message.content);
98808
- text = (parsed.text ?? "").trim();
98809
- } catch {
98810
- return;
99056
+ if (!rawContent) return;
99057
+ let text2 = describeMessageType(messageType, rawContent);
99058
+ if (messageType === "text") {
99059
+ text2 = text2.replace(/@_user_\d+\s*/g, "").trim();
98811
99060
  }
98812
- text = text.replace(/@_user_\d+\s*/g, "").trim();
98813
- if (!text) return;
99061
+ if (!text2) return;
98814
99062
  const chatType = message.chat_type === "group" ? "group" : "p2p";
98815
99063
  let shouldReply = true;
98816
99064
  if (chatType === "group") {
@@ -98828,7 +99076,8 @@ function startFeishuGateway(options) {
98828
99076
  chatId: String(chatId),
98829
99077
  messageId: messageId ?? "",
98830
99078
  messageType,
98831
- content: text,
99079
+ content: text2,
99080
+ rawContent,
98832
99081
  chatType,
98833
99082
  senderId,
98834
99083
  rootId,
@@ -98840,7 +99089,7 @@ function startFeishuGateway(options) {
98840
99089
  messageId: messageId ?? "",
98841
99090
  chatType,
98842
99091
  shouldReply,
98843
- textPreview: text.slice(0, 80)
99092
+ textPreview: text2.slice(0, 80)
98844
99093
  });
98845
99094
  await onMessage(ctx);
98846
99095
  } catch (err) {
@@ -98894,7 +99143,7 @@ function startFeishuGateway(options) {
98894
99143
  }
98895
99144
 
98896
99145
  // src/feishu/sender.ts
98897
- async function sendTextMessage(client, chatId, text) {
99146
+ async function sendTextMessage(client, chatId, text2) {
98898
99147
  if (!chatId?.trim()) {
98899
99148
  return { ok: false, error: "No chat_id provided" };
98900
99149
  }
@@ -98904,7 +99153,7 @@ async function sendTextMessage(client, chatId, text) {
98904
99153
  data: {
98905
99154
  receive_id: chatId.trim(),
98906
99155
  msg_type: "text",
98907
- content: JSON.stringify({ text })
99156
+ content: JSON.stringify({ text: text2 })
98908
99157
  }
98909
99158
  });
98910
99159
  return { ok: true, messageId: res?.data?.message_id ?? "" };
@@ -98912,13 +99161,13 @@ async function sendTextMessage(client, chatId, text) {
98912
99161
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
98913
99162
  }
98914
99163
  }
98915
- async function updateMessage(client, messageId, text) {
99164
+ async function updateMessage(client, messageId, text2) {
98916
99165
  try {
98917
99166
  await client.im.message.update({
98918
99167
  path: { message_id: messageId },
98919
99168
  data: {
98920
99169
  msg_type: "text",
98921
- content: JSON.stringify({ text })
99170
+ content: JSON.stringify({ text: text2 })
98922
99171
  }
98923
99172
  });
98924
99173
  return { ok: true, messageId };
@@ -98944,14 +99193,19 @@ async function handleEvent(event) {
98944
99193
  if (!sessionId) break;
98945
99194
  const payload = pendingBySession.get(sessionId);
98946
99195
  if (!payload) break;
98947
- const added = extractPartText(part);
98948
- if (added) {
98949
- payload.textBuffer += added;
98950
- try {
98951
- await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
98952
- } 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;
98953
99203
  }
98954
99204
  }
99205
+ if (payload.textBuffer) {
99206
+ const res = await updateMessage(payload.feishuClient, payload.placeholderId, payload.textBuffer.trim());
99207
+ if (!res.ok) ;
99208
+ }
98955
99209
  break;
98956
99210
  }
98957
99211
  case "session.error": {
@@ -98961,9 +99215,8 @@ async function handleEvent(event) {
98961
99215
  const payload = pendingBySession.get(sessionId);
98962
99216
  if (!payload) break;
98963
99217
  const errMsg = props.error?.message ?? String(props.error);
98964
- try {
98965
- await updateMessage(payload.feishuClient, payload.placeholderId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
98966
- } catch {
99218
+ const updateRes = await updateMessage(payload.feishuClient, payload.placeholderId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
99219
+ if (!updateRes.ok) {
98967
99220
  await sendTextMessage(payload.feishuClient, payload.chatId, `\u274C \u4F1A\u8BDD\u9519\u8BEF: ${errMsg}`);
98968
99221
  }
98969
99222
  break;
@@ -99015,26 +99268,21 @@ async function getOrCreateSession(client, sessionKey, directory) {
99015
99268
 
99016
99269
  // src/handler/chat.ts
99017
99270
  async function handleChat(ctx, deps) {
99018
- const { content, chatId, chatType, senderId, createTime, shouldReply } = ctx;
99019
- if (!content.trim()) return;
99271
+ const { content, chatId, chatType, senderId, shouldReply, messageType, rawContent, messageId } = ctx;
99272
+ if (!content.trim() && messageType === "text") return;
99020
99273
  const { config, client, feishuClient, log, directory } = deps;
99021
99274
  const query = directory ? { directory } : void 0;
99022
99275
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
99023
99276
  const session = await getOrCreateSession(client, sessionKey, directory);
99024
- const timeStr = createTime ? new Date(Number(createTime)).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) : "";
99025
- let promptContent = content;
99026
- if (chatType === "group" && senderId) {
99027
- promptContent = timeStr ? `[${timeStr}] [${senderId}]: ${content}` : `[${senderId}]: ${content}`;
99028
- } else if (timeStr) {
99029
- promptContent = `[${timeStr}] ${content}`;
99030
- }
99277
+ const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
99278
+ if (!parts.length) return;
99031
99279
  if (!shouldReply) {
99032
99280
  try {
99033
99281
  await client.session.prompt({
99034
99282
  path: { id: session.id },
99035
99283
  query,
99036
99284
  body: {
99037
- parts: [{ type: "text", text: promptContent }],
99285
+ parts,
99038
99286
  noReply: true
99039
99287
  }
99040
99288
  });
@@ -99055,11 +99303,16 @@ async function handleChat(ctx, deps) {
99055
99303
  if (done) return;
99056
99304
  try {
99057
99305
  const res = await sendTextMessage(feishuClient, chatId, "\u6B63\u5728\u601D\u8003\u2026");
99306
+ if (done) return;
99058
99307
  if (res.ok && res.messageId) {
99059
99308
  placeholderId = res.messageId;
99060
99309
  registerPending(session.id, { chatId, placeholderId, feishuClient });
99061
99310
  }
99062
- } 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
+ });
99063
99316
  }
99064
99317
  }, thinkingDelay) : null;
99065
99318
  try {
@@ -99067,7 +99320,7 @@ async function handleChat(ctx, deps) {
99067
99320
  path: { id: session.id },
99068
99321
  query,
99069
99322
  body: {
99070
- parts: [{ type: "text", text: promptContent }]
99323
+ parts
99071
99324
  }
99072
99325
  });
99073
99326
  const start = Date.now();
@@ -99076,17 +99329,11 @@ async function handleChat(ctx, deps) {
99076
99329
  while (Date.now() - start < timeout) {
99077
99330
  await new Promise((r) => setTimeout(r, pollInterval));
99078
99331
  const { data: messages } = await client.session.messages({ path: { id: session.id }, query });
99079
- const text = extractLastAssistantText(messages ?? []);
99080
- if (text && text !== lastText) {
99081
- lastText = text;
99332
+ const text2 = extractLastAssistantText(messages ?? []);
99333
+ if (text2 && text2 !== lastText) {
99334
+ lastText = text2;
99082
99335
  sameCount = 0;
99083
- if (placeholderId) {
99084
- try {
99085
- await updateMessage(feishuClient, placeholderId, text);
99086
- } catch {
99087
- }
99088
- }
99089
- } else if (text && text.length > 0) {
99336
+ } else if (text2 && text2.length > 0) {
99090
99337
  sameCount++;
99091
99338
  if (sameCount >= stablePolls) break;
99092
99339
  }
@@ -99106,15 +99353,28 @@ async function handleChat(ctx, deps) {
99106
99353
  unregisterPending(session.id);
99107
99354
  }
99108
99355
  }
99109
- async function replyOrUpdate(feishuClient, chatId, placeholderId, text) {
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
+ }
99370
+ async function replyOrUpdate(feishuClient, chatId, placeholderId, text2) {
99110
99371
  if (placeholderId) {
99111
- try {
99112
- await updateMessage(feishuClient, placeholderId, text);
99113
- } catch {
99114
- await sendTextMessage(feishuClient, chatId, text);
99372
+ const res = await updateMessage(feishuClient, placeholderId, text2);
99373
+ if (!res.ok) {
99374
+ await sendTextMessage(feishuClient, chatId, text2);
99115
99375
  }
99116
99376
  } else {
99117
- await sendTextMessage(feishuClient, chatId, text);
99377
+ await sendTextMessage(feishuClient, chatId, text2);
99118
99378
  }
99119
99379
  }
99120
99380
  function extractLastAssistantText(messages) {
@@ -99126,7 +99386,8 @@ function extractLastAssistantText(messages) {
99126
99386
  // src/feishu/history.ts
99127
99387
  var DEFAULT_PAGE_SIZE = 50;
99128
99388
  async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options) {
99129
- const { maxMessages, log } = options;
99389
+ const { maxMessages, log, directory } = options;
99390
+ const query = directory ? { directory } : void 0;
99130
99391
  log("info", "\u5F00\u59CB\u6444\u5165\u7FA4\u804A\u5386\u53F2\u4E0A\u4E0B\u6587", { chatId, maxMessages });
99131
99392
  const messages = await fetchRecentMessages(feishuClient, chatId, maxMessages, log);
99132
99393
  if (!messages.length) {
@@ -99134,10 +99395,11 @@ async function ingestGroupHistory(feishuClient, opencodeClient, chatId, options)
99134
99395
  return;
99135
99396
  }
99136
99397
  const sessionKey = buildSessionKey("group", chatId);
99137
- const session = await getOrCreateSession(opencodeClient, sessionKey);
99398
+ const session = await getOrCreateSession(opencodeClient, sessionKey, directory);
99138
99399
  const contextText = formatHistoryAsContext(messages);
99139
99400
  await opencodeClient.session.prompt({
99140
99401
  path: { id: session.id },
99402
+ query,
99141
99403
  body: {
99142
99404
  parts: [{ type: "text", text: contextText }],
99143
99405
  noReply: true
@@ -99163,19 +99425,15 @@ async function fetchRecentMessages(client, chatId, maxMessages, log) {
99163
99425
  if (!items || items.length === 0) break;
99164
99426
  for (const item of items) {
99165
99427
  if (item.deleted) continue;
99166
- if (item.msg_type !== "text" || !item.body?.content) continue;
99167
- let text;
99168
- try {
99169
- const parsed = JSON.parse(item.body.content);
99170
- text = (parsed.text ?? "").trim();
99171
- } catch {
99172
- continue;
99173
- }
99174
- if (!text) continue;
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);
99432
+ if (!text2) continue;
99175
99433
  result.push({
99176
99434
  senderType: item.sender?.sender_type ?? "unknown",
99177
99435
  senderId: item.sender?.id ?? "",
99178
- content: text,
99436
+ content: text2,
99179
99437
  createTime: item.create_time ?? ""
99180
99438
  });
99181
99439
  if (result.length >= maxMessages) break;
@@ -99217,7 +99475,8 @@ var DEFAULT_CONFIG = {
99217
99475
  maxHistoryMessages: 200,
99218
99476
  pollInterval: 1e3,
99219
99477
  stablePolls: 3,
99220
- dedupTtl: 10 * 60 * 1e3
99478
+ dedupTtl: 10 * 60 * 1e3,
99479
+ directory: ""
99221
99480
  };
99222
99481
  var FeishuPlugin = async (ctx) => {
99223
99482
  const { client } = ctx;
@@ -99250,6 +99509,11 @@ var FeishuPlugin = async (ctx) => {
99250
99509
  } catch (parseErr) {
99251
99510
  throw new Error(`\u98DE\u4E66\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF\uFF1A${configPath} \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON (${parseErr})`);
99252
99511
  }
99512
+ if (feishuRaw.directory !== void 0 && typeof feishuRaw.directory !== "string") {
99513
+ throw new Error(
99514
+ `\u98DE\u4E66\u914D\u7F6E\u9519\u8BEF\uFF1A${configPath} \u4E2D\u7684 'directory' \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`
99515
+ );
99516
+ }
99253
99517
  if (!feishuRaw.appId || !feishuRaw.appSecret) {
99254
99518
  throw new Error(
99255
99519
  `\u98DE\u4E66\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A${configPath} \u4E2D\u5FC5\u987B\u5305\u542B appId \u548C appSecret`
@@ -99264,7 +99528,8 @@ var FeishuPlugin = async (ctx) => {
99264
99528
  maxHistoryMessages: feishuRaw.maxHistoryMessages ?? DEFAULT_CONFIG.maxHistoryMessages,
99265
99529
  pollInterval: feishuRaw.pollInterval ?? DEFAULT_CONFIG.pollInterval,
99266
99530
  stablePolls: feishuRaw.stablePolls ?? DEFAULT_CONFIG.stablePolls,
99267
- dedupTtl: feishuRaw.dedupTtl ?? DEFAULT_CONFIG.dedupTtl
99531
+ dedupTtl: feishuRaw.dedupTtl ?? DEFAULT_CONFIG.dedupTtl,
99532
+ directory: feishuRaw.directory ?? ctx.directory ?? DEFAULT_CONFIG.directory
99268
99533
  };
99269
99534
  initDedup(resolvedConfig.dedupTtl);
99270
99535
  const botOpenId = await fetchBotOpenId(resolvedConfig.appId, resolvedConfig.appSecret, log);
@@ -99278,14 +99543,15 @@ var FeishuPlugin = async (ctx) => {
99278
99543
  client,
99279
99544
  feishuClient: gateway.client,
99280
99545
  log,
99281
- directory: ctx.directory
99546
+ directory: resolvedConfig.directory
99282
99547
  });
99283
99548
  },
99284
99549
  onBotAdded: (chatId) => {
99285
99550
  if (!gateway) return;
99286
99551
  ingestGroupHistory(gateway.client, client, chatId, {
99287
99552
  maxMessages: resolvedConfig.maxHistoryMessages,
99288
- log
99553
+ log,
99554
+ directory: resolvedConfig.directory
99289
99555
  }).catch((err) => {
99290
99556
  log("error", "\u7FA4\u804A\u5386\u53F2\u6444\u5165\u5931\u8D25", {
99291
99557
  chatId,
@@ -99330,8 +99596,32 @@ function resolveEnvPlaceholders(obj) {
99330
99596
  }
99331
99597
  return obj;
99332
99598
  }
99599
+ function proxyFetch(url2, init) {
99600
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY || "";
99601
+ if (!proxyUrl) return fetch(url2, init);
99602
+ const parsed = new URL(url2);
99603
+ return new Promise((resolve, reject) => {
99604
+ const req = request(
99605
+ {
99606
+ hostname: parsed.hostname,
99607
+ path: parsed.pathname + parsed.search,
99608
+ method: init?.method ?? "GET",
99609
+ headers: init?.headers,
99610
+ agent: new HttpsProxyAgent(proxyUrl)
99611
+ },
99612
+ (res) => {
99613
+ text(res).then(
99614
+ (body) => resolve(new Response(body, { status: res.statusCode ?? 0 }))
99615
+ ).catch(reject);
99616
+ }
99617
+ );
99618
+ req.on("error", reject);
99619
+ if (init?.body) req.write(init.body);
99620
+ req.end();
99621
+ });
99622
+ }
99333
99623
  async function fetchBotOpenId(appId, appSecret, log) {
99334
- const tokenRes = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
99624
+ const tokenRes = await proxyFetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
99335
99625
  method: "POST",
99336
99626
  headers: { "Content-Type": "application/json" },
99337
99627
  body: JSON.stringify({ app_id: appId, app_secret: appSecret })
@@ -99341,7 +99631,7 @@ async function fetchBotOpenId(appId, appSecret, log) {
99341
99631
  if (!token) {
99342
99632
  throw new Error("\u83B7\u53D6 tenant_access_token \u5931\u8D25\uFF0C\u65E0\u6CD5\u542F\u52A8\u7FA4\u804A @\u63D0\u53CA\u68C0\u6D4B");
99343
99633
  }
99344
- const botRes = await fetch("https://open.feishu.cn/open-apis/bot/v3/info", {
99634
+ const botRes = await proxyFetch("https://open.feishu.cn/open-apis/bot/v3/info", {
99345
99635
  method: "GET",
99346
99636
  headers: { Authorization: `Bearer ${token}` }
99347
99637
  });