@modelzen/feishu-codex-bridge 0.3.4 → 0.3.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/README.md CHANGED
@@ -165,6 +165,7 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
165
165
  - **💬 单会话群**:整群就是**一条连续会话**(全程**免 @**、消息按序排队、无 `/resume`)。适合**个人单线深入**、像私聊一样直接聊。
166
166
  - **干活**:在项目群里 **@机器人** 描述需求;机器人在该群绑定的目录里跑 Codex,流式卡片回结果。
167
167
  - **话题 = 会话**:对某条消息开话题后,话题内可**免 @** 连续对话,是一条连贯的 Codex 会话。
168
+ - **发图 / 发附件**:直接在消息里**发图片**,Codex 能看到(多模态读图);**发文件附件**(日志 / PDF / 代码等),桥会把它下载到本地并把**绝对路径**告诉 Codex,让它用工具直接打开分析。⚠️ 附件落在桥的全局临时目录(`~/.feishu-codex-bridge/inbound`,1h 后自动清),**只有「完全访问」档**能读到——「项目内只读 / 读写」档的沙箱把读取锁在项目目录内,读不到该目录。单文件上限 50MB、单条消息最多 9 个;合并转发里的附件飞书官方不支持取,故不支持。
168
169
  - **文档评论 @机器人**:在飞书文档评论里 @ 它就回(前提:已开通文档评论权限 + 订阅 `drive.notice.comment_add_v1`,且机器人对该文档有访问权限)。只支持 doc/docx/sheet/file;评论框不渲染 markdown,回复为纯文本,超长会截断。
169
170
  - **终止**:卡片上的 **⏹** 随时终止当前轮;卡死超过 watchdog 阈值(默认 120s)自动中止并回收进程。
170
171
  - **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog / 管理员)、用量、诊断、重连,全在私聊菜单里。
package/dist/cli.js CHANGED
@@ -68,7 +68,10 @@ var paths = {
68
68
  * passes lark-cli's AssertSecurePath audit.
69
69
  */
70
70
  secretsGetterScript: join(appDir, "secrets-getter"),
71
- mediaDir: join(appDir, "media")
71
+ mediaDir: join(appDir, "media"),
72
+ /** Inbound file attachments downloaded from chat, handed to codex by absolute
73
+ * path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
74
+ inboundDir: join(appDir, "inbound")
72
75
  };
73
76
 
74
77
  // src/config/bots.ts
@@ -5404,7 +5407,7 @@ async function collectInboundImages(channel, msg) {
5404
5407
  return [];
5405
5408
  }
5406
5409
  if (refs.length === 0) return [];
5407
- await pruneOldMedia();
5410
+ await pruneOldMedia(paths.mediaDir);
5408
5411
  try {
5409
5412
  await mkdir11(paths.mediaDir, { recursive: true });
5410
5413
  } catch {
@@ -5507,16 +5510,16 @@ function readHeader(headers, name) {
5507
5510
  function safeName(fileKey) {
5508
5511
  return fileKey.replace(/[^a-zA-Z0-9_-]/g, "").slice(-40) || "img";
5509
5512
  }
5510
- async function pruneOldMedia() {
5513
+ async function pruneOldMedia(dir) {
5511
5514
  let entries;
5512
5515
  try {
5513
- entries = await readdir2(paths.mediaDir);
5516
+ entries = await readdir2(dir);
5514
5517
  } catch {
5515
5518
  return;
5516
5519
  }
5517
5520
  const cutoff = Date.now() - MEDIA_TTL_MS;
5518
5521
  for (const name of entries) {
5519
- const file = join14(paths.mediaDir, name);
5522
+ const file = join14(dir, name);
5520
5523
  try {
5521
5524
  const st = await stat3(file);
5522
5525
  if (st.mtimeMs < cutoff) await rm4(file, { force: true });
@@ -5524,6 +5527,277 @@ async function pruneOldMedia() {
5524
5527
  }
5525
5528
  }
5526
5529
  }
5530
+ var MAX_FILES = 9;
5531
+ var MAX_FILE_BYTES = 50 * 1024 * 1024;
5532
+ function messageHasFiles(msg) {
5533
+ return (msg.resources ?? []).some((r) => r.type === "file");
5534
+ }
5535
+ async function collectInboundFiles(channel, msg) {
5536
+ const refs = [];
5537
+ const seen = /* @__PURE__ */ new Set();
5538
+ for (const r of msg.resources ?? []) {
5539
+ if (r.type === "file" && r.fileKey && !seen.has(r.fileKey)) {
5540
+ seen.add(r.fileKey);
5541
+ refs.push({ messageId: msg.messageId, fileKey: r.fileKey, fileName: r.fileName });
5542
+ }
5543
+ }
5544
+ if (refs.length === 0) return [];
5545
+ await pruneOldMedia(paths.inboundDir);
5546
+ try {
5547
+ await mkdir11(paths.inboundDir, { recursive: true });
5548
+ } catch {
5549
+ }
5550
+ const out = [];
5551
+ for (const ref of refs.slice(0, MAX_FILES)) {
5552
+ const f = await downloadOneFile(channel, ref);
5553
+ if (f) out.push(f);
5554
+ }
5555
+ log.info("intake", "files", { found: refs.length, downloaded: out.length });
5556
+ return out;
5557
+ }
5558
+ async function downloadOneFile(channel, ref) {
5559
+ try {
5560
+ const res = await channel.rawClient.im.v1.messageResource.get({
5561
+ path: { message_id: ref.messageId, file_key: ref.fileKey },
5562
+ params: { type: "file" }
5563
+ });
5564
+ const declared = Number(readHeader(res.headers, "content-length"));
5565
+ if (Number.isFinite(declared) && declared > MAX_FILE_BYTES) {
5566
+ log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: declared });
5567
+ return void 0;
5568
+ }
5569
+ const name = cleanFileName(ref.fileName) || "attachment";
5570
+ const onDisk = `${safeName(ref.fileKey)}-${name}`;
5571
+ const file = join14(paths.inboundDir, onDisk);
5572
+ await res.writeFile(file);
5573
+ try {
5574
+ const st = await stat3(file);
5575
+ if (st.size > MAX_FILE_BYTES) {
5576
+ await rm4(file, { force: true });
5577
+ log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: st.size });
5578
+ return void 0;
5579
+ }
5580
+ } catch {
5581
+ }
5582
+ return { path: file, name };
5583
+ } catch (err) {
5584
+ log.warn("intake", "file-download-failed", { fileKey: ref.fileKey.slice(0, 24), err: String(err) });
5585
+ return void 0;
5586
+ }
5587
+ }
5588
+ function cleanFileName(name) {
5589
+ if (!name) return "";
5590
+ const base = name.split(/[/\\]/).pop() ?? name;
5591
+ const cleaned = base.replace(/[\x00-\x1f<>:"|?*]/g, "_").replace(/\s+/g, " ").trim().slice(0, 100);
5592
+ return cleaned === "." || cleaned === ".." ? "" : cleaned;
5593
+ }
5594
+ function stripFileTokens(text) {
5595
+ return text.replace(/<file\b[^<]*\/>/g, "").replace(/[ \t]+\n/g, "\n").trim();
5596
+ }
5597
+ function weaveFileManifest(text, files) {
5598
+ const stripped = stripFileTokens(text);
5599
+ if (files.length === 0) return stripped;
5600
+ const lines = files.map((f) => `- ${f.name} \u2192 ${f.path}`).join("\n");
5601
+ const head = stripped ? `${stripped}
5602
+
5603
+ ` : "";
5604
+ return `${head}[\u7528\u6237\u4E0A\u4F20\u4E86 ${files.length} \u4E2A\u9644\u4EF6\uFF0C\u5DF2\u4FDD\u5B58\u5230\u672C\u5730\uFF0C\u53EF\u7528 shell / \u8BFB\u53D6\u5DE5\u5177\u6309\u4E0B\u9762\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76F4\u63A5\u6253\u5F00\uFF1A
5605
+ ${lines}
5606
+ ]`;
5607
+ }
5608
+
5609
+ // src/bot/context-weave.ts
5610
+ var QUOTE_MAX = 800;
5611
+ var LINE_MAX = 280;
5612
+ var THREAD_WEAVE_MAX = 20;
5613
+ var THREAD_PAGE_SIZE = 50;
5614
+ async function fetchQuotedMessage(channel, messageId) {
5615
+ try {
5616
+ const res = await channel.rawClient.im.v1.message.get({ path: { message_id: messageId } });
5617
+ const items = res.data?.items ?? [];
5618
+ const item = items[0];
5619
+ if (!item || item.deleted) return void 0;
5620
+ const cm = toContextMessage(item);
5621
+ return cm.text.trim() ? cm : void 0;
5622
+ } catch (err) {
5623
+ log.warn("intake", "quote-fetch-failed", { messageId, err: String(err) });
5624
+ return void 0;
5625
+ }
5626
+ }
5627
+ async function fetchThreadContext(channel, threadId, opts = {}) {
5628
+ const limit = opts.limit ?? THREAD_WEAVE_MAX;
5629
+ const since = opts.sinceTime ?? 0;
5630
+ try {
5631
+ const res = await channel.rawClient.im.v1.message.list({
5632
+ params: {
5633
+ container_id_type: "thread",
5634
+ container_id: threadId,
5635
+ sort_type: "ByCreateTimeDesc",
5636
+ page_size: THREAD_PAGE_SIZE
5637
+ }
5638
+ });
5639
+ const items = res.data?.items ?? [];
5640
+ const picked = items.filter((it) => !it.deleted).map(toContextMessage).filter(
5641
+ (m) => m.fromUser && // drop the bot's own replies, other apps, system notices
5642
+ m.messageId !== opts.excludeMessageId && // drop the triggering @ message
5643
+ (since === 0 || m.createTime > since) && // delta only for existing sessions
5644
+ m.text.trim().length > 0
5645
+ );
5646
+ picked.sort((a, b) => a.createTime - b.createTime);
5647
+ const out = picked.slice(-limit);
5648
+ if (picked.length > out.length) {
5649
+ log.info("intake", "thread-context-truncated", { threadId, kept: out.length, total: picked.length });
5650
+ }
5651
+ return out;
5652
+ } catch (err) {
5653
+ log.warn("intake", "thread-context-failed", { threadId, err: String(err) });
5654
+ return [];
5655
+ }
5656
+ }
5657
+ function toContextMessage(item) {
5658
+ const id = item.sender?.id ?? "";
5659
+ const name = item.sender?.sender_name || (id ? `\u7528\u6237${id.slice(-4)}` : "\u67D0\u4EBA");
5660
+ return {
5661
+ messageId: item.message_id ?? "",
5662
+ senderName: name,
5663
+ text: extractMessageText(item.msg_type, item.body?.content, item.mentions),
5664
+ fromUser: item.sender?.sender_type === "user",
5665
+ createTime: Number(item.create_time) || 0
5666
+ };
5667
+ }
5668
+ function extractMessageText(msgType, content, mentions) {
5669
+ if (!content) return placeholderFor(msgType);
5670
+ let parsed;
5671
+ try {
5672
+ parsed = JSON.parse(content);
5673
+ } catch {
5674
+ return placeholderFor(msgType);
5675
+ }
5676
+ switch (msgType) {
5677
+ case "text":
5678
+ return replaceMentions(parsed?.text ?? "", mentions);
5679
+ case "post":
5680
+ return replaceMentions(extractPostText(parsed), mentions);
5681
+ case "image":
5682
+ return "[\u56FE\u7247]";
5683
+ case "audio":
5684
+ return "[\u8BED\u97F3]";
5685
+ case "media":
5686
+ return "[\u89C6\u9891]";
5687
+ case "file": {
5688
+ const name = parsed?.file_name;
5689
+ return name ? `[\u6587\u4EF6\uFF1A${name}]` : "[\u6587\u4EF6]";
5690
+ }
5691
+ case "sticker":
5692
+ return "[\u8868\u60C5]";
5693
+ case "interactive":
5694
+ return "[\u5361\u7247\u6D88\u606F]";
5695
+ case "share_chat":
5696
+ return "[\u5206\u4EAB\u7FA4\u540D\u7247]";
5697
+ case "share_user":
5698
+ return "[\u5206\u4EAB\u4E2A\u4EBA\u540D\u7247]";
5699
+ case "merge_forward":
5700
+ case "forward":
5701
+ return "[\u5408\u5E76\u8F6C\u53D1\u6D88\u606F]";
5702
+ default:
5703
+ return placeholderFor(msgType);
5704
+ }
5705
+ }
5706
+ function placeholderFor(msgType) {
5707
+ return msgType ? `[${msgType} \u6D88\u606F]` : "[\u6D88\u606F]";
5708
+ }
5709
+ function extractPostText(parsed) {
5710
+ if (!parsed || typeof parsed !== "object") return "";
5711
+ const obj = parsed;
5712
+ let title = obj.title;
5713
+ let blocks = obj.content;
5714
+ if (!Array.isArray(blocks)) {
5715
+ for (const v of Object.values(obj)) {
5716
+ if (v && typeof v === "object" && Array.isArray(v.content)) {
5717
+ title = v.title;
5718
+ blocks = v.content;
5719
+ break;
5720
+ }
5721
+ }
5722
+ }
5723
+ const parts = [];
5724
+ if (typeof title === "string" && title.trim()) parts.push(title.trim());
5725
+ if (Array.isArray(blocks)) {
5726
+ for (const line of blocks) {
5727
+ if (!Array.isArray(line)) continue;
5728
+ const lineText = line.map(nodeToText).join("");
5729
+ if (lineText) parts.push(lineText);
5730
+ }
5731
+ }
5732
+ return parts.join("\n");
5733
+ }
5734
+ function nodeToText(node) {
5735
+ if (!node || typeof node !== "object") return "";
5736
+ const n = node;
5737
+ switch (n.tag) {
5738
+ case "text":
5739
+ return typeof n.text === "string" ? n.text : "";
5740
+ case "a":
5741
+ return typeof n.text === "string" ? n.text : typeof n.href === "string" ? n.href : "";
5742
+ case "at": {
5743
+ const name = typeof n.user_name === "string" ? n.user_name : "";
5744
+ return name ? `@${name}` : "@\u67D0\u4EBA";
5745
+ }
5746
+ case "img":
5747
+ return "[\u56FE\u7247]";
5748
+ case "media":
5749
+ return "[\u89C6\u9891]";
5750
+ case "emotion":
5751
+ return "[\u8868\u60C5]";
5752
+ default:
5753
+ return typeof n.text === "string" ? n.text : "";
5754
+ }
5755
+ }
5756
+ function replaceMentions(text, mentions) {
5757
+ if (!text || !mentions?.length) return text;
5758
+ let out = text;
5759
+ for (const m of mentions) {
5760
+ if (!m.key) continue;
5761
+ out = out.split(m.key).join(m.name ? `@${m.name}` : "@\u67D0\u4EBA");
5762
+ }
5763
+ return out;
5764
+ }
5765
+ function sanitizeContext(s, maxLen, oneLine2) {
5766
+ if (!s) return "";
5767
+ let out = s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\r\n?/g, "\n");
5768
+ out = oneLine2 ? out.replace(/\s+/g, " ") : out.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
5769
+ out = out.trim();
5770
+ return out.length > maxLen ? `${out.slice(0, maxLen)}\u2026` : out;
5771
+ }
5772
+ function weaveQuote(text, quoted) {
5773
+ if (!quoted) return text;
5774
+ const who = sanitizeContext(quoted.senderName, 40, true) || "\u67D0\u4EBA";
5775
+ const body = sanitizeContext(quoted.text, QUOTE_MAX, true);
5776
+ if (!body) return text;
5777
+ const block = `[\u7528\u6237\u5F15\u7528\u4E86\u4E00\u6761\u6D88\u606F\uFF08\u6765\u81EA ${who}\uFF09\uFF1A
5778
+ ${body}
5779
+ ]`;
5780
+ const base = text.trim();
5781
+ return base ? `${block}
5782
+
5783
+ ${base}` : block;
5784
+ }
5785
+ function weaveThreadHistory(text, msgs) {
5786
+ if (msgs.length === 0) return text;
5787
+ const lines = msgs.map((m) => {
5788
+ const who = sanitizeContext(m.senderName, 40, true) || "\u67D0\u4EBA";
5789
+ const body = sanitizeContext(m.text, LINE_MAX, true);
5790
+ return body ? `${who}\uFF1A${body}` : "";
5791
+ }).filter((l) => l.length > 0);
5792
+ if (lines.length === 0) return text;
5793
+ const block = `[\u8BDD\u9898\u4E2D\u5728\u6B64\u4E4B\u524D\u5DF2\u6709\u7684\u6D88\u606F\uFF08\u6309\u65F6\u95F4\u5148\u540E\u6392\u5217\uFF0C\u4F9B\u4F60\u7406\u89E3\u4E0A\u4E0B\u6587\uFF09\uFF1A
5794
+ ${lines.join("\n")}
5795
+ ]`;
5796
+ const base = text.trim();
5797
+ return base ? `${block}
5798
+
5799
+ ${base}` : block;
5800
+ }
5527
5801
 
5528
5802
  // src/bot/comments.ts
5529
5803
  var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
@@ -6010,20 +6284,36 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6010
6284
  const perm = turnPerm(project, senderId);
6011
6285
  return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
6012
6286
  }
6287
+ async function ingestContext(msg, text) {
6288
+ let body = text;
6289
+ if (messageHasFiles(msg)) {
6290
+ const files = await collectInboundFiles(channel, msg);
6291
+ body = weaveFileManifest(text, files);
6292
+ if (!body.trim()) {
6293
+ body = "\u7528\u6237\u53D1\u6765\u4E00\u4E2A\u9644\u4EF6\uFF0C\u4F46\u6865\u6CA1\u80FD\u4E0B\u8F7D\u5B83\uFF08\u53EF\u80FD\u8D85\u8FC7 50MB \u4E0A\u9650\u6216\u88AB\u98DE\u4E66\u62D2\u7EDD\uFF09\u3002\u8BF7\u544A\u8BC9\u7528\u6237\u9644\u4EF6\u6CA1\u8BFB\u5230\uFF0C\u53EF\u4EE5\u91CD\u53D1\uFF0C\u6216\u6539\u4E3A\u7C98\u8D34\u6587\u672C / \u53D1\u56FE\u7247\u3002";
6294
+ }
6295
+ }
6296
+ if (msg.replyToMessageId) {
6297
+ const quoted = await fetchQuotedMessage(channel, msg.replyToMessageId);
6298
+ body = weaveQuote(body, quoted);
6299
+ }
6300
+ return body;
6301
+ }
6013
6302
  async function handleTurn(msg, text, sessionKey, flat, project, perm) {
6014
6303
  const existing = active.get(sessionKey);
6015
6304
  if (existing) {
6016
6305
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6306
+ const woven = await ingestContext(msg, text);
6017
6307
  const cur = active.get(sessionKey);
6018
6308
  if (!cur) {
6019
- startReservedRun(msg, text, sessionKey, flat, project, perm, images);
6309
+ startReservedRun(msg, woven, sessionKey, flat, project, perm, images, true, text);
6020
6310
  return;
6021
6311
  }
6022
6312
  if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
6023
6313
  const tid = cur.run.turnId();
6024
6314
  if (tid) {
6025
6315
  try {
6026
- await cur.thread.steer({ text, images }, tid);
6316
+ await cur.thread.steer({ text: woven, images }, tid);
6027
6317
  log.info("intake", "steer", { tid, images: images?.length ?? 0 });
6028
6318
  return;
6029
6319
  } catch (err) {
@@ -6031,13 +6321,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6031
6321
  }
6032
6322
  }
6033
6323
  }
6034
- cur.queue.push({ text, images });
6324
+ cur.queue.push({ text: woven, images });
6035
6325
  log.info("intake", "queued", { depth: cur.queue.length });
6036
6326
  return;
6037
6327
  }
6038
6328
  startReservedRun(msg, text, sessionKey, flat, project, perm);
6039
6329
  }
6040
- function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
6330
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
6041
6331
  const existing = active.get(sessionKey);
6042
6332
  if (existing) {
6043
6333
  existing.queue.push({ text, images: preloadedImages });
@@ -6050,7 +6340,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6050
6340
  const reaction = runReaction(msg.messageId, !sema.hasFree());
6051
6341
  try {
6052
6342
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
6053
- let thread = await resolveThread(sessionKey, msg.chatId, { mode: perm.mode, network: perm.network });
6343
+ let firstText = preIngested ? text : await ingestContext(msg, text);
6344
+ const { thread: resolved, recreated } = await resolveThread(sessionKey, msg.chatId, {
6345
+ mode: perm.mode,
6346
+ network: perm.network
6347
+ });
6348
+ let thread = resolved;
6349
+ const neverSeen = !thread;
6350
+ const codexEmpty = neverSeen || recreated;
6351
+ const prior = neverSeen ? void 0 : await getSession(sessionKey);
6054
6352
  if (!thread) {
6055
6353
  const cwd = project?.cwd ?? fallbackCwd;
6056
6354
  thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
@@ -6060,11 +6358,23 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6060
6358
  chatId: msg.chatId,
6061
6359
  cwd,
6062
6360
  codexThreadId: thread.codexThreadId,
6063
- summary: text.slice(0, 80),
6361
+ // `text` is already file-woven when preIngested; use the raw
6362
+ // `summaryText` (handleTurn's original) so the session label isn't
6363
+ // manifest boilerplate + a temp path.
6364
+ summary: stripFileTokens(summaryText2 ?? text).slice(0, 80),
6365
+ lastSeenAt: msg.createTime,
6064
6366
  createdAt: Date.now(),
6065
6367
  updatedAt: Date.now()
6066
6368
  });
6067
6369
  }
6370
+ if (msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6371
+ const history = await fetchThreadContext(channel, msg.threadId, {
6372
+ sinceTime: codexEmpty ? 0 : prior?.lastSeenAt ?? 0,
6373
+ excludeMessageId: msg.messageId
6374
+ });
6375
+ firstText = weaveThreadHistory(firstText, history);
6376
+ }
6377
+ if (!neverSeen) void patchSession(sessionKey, { lastSeenAt: msg.createTime }).catch(() => void 0);
6068
6378
  reserved.thread = thread;
6069
6379
  await launchRun(
6070
6380
  {
@@ -6073,7 +6383,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6073
6383
  replyInThread: !flat,
6074
6384
  flat,
6075
6385
  thread,
6076
- firstText: text,
6386
+ firstText,
6077
6387
  images,
6078
6388
  knownThreadId: sessionKey,
6079
6389
  requesterOpenId: msg.senderId
@@ -6090,9 +6400,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6090
6400
  }
6091
6401
  async function resolveThread(threadId, chatId, perm) {
6092
6402
  const live = sessions.get(threadId);
6093
- if (live) return live;
6403
+ if (live) return { thread: live, recreated: false };
6094
6404
  const rec = await getSession(threadId);
6095
- if (!rec) return void 0;
6405
+ if (!rec) return { thread: void 0, recreated: false };
6096
6406
  try {
6097
6407
  const resumed = await backend.resumeThread({
6098
6408
  cwd: rec.cwd,
@@ -6103,7 +6413,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6103
6413
  network: perm?.network
6104
6414
  });
6105
6415
  sessions.set(threadId, resumed);
6106
- return resumed;
6416
+ return { thread: resumed, recreated: false };
6107
6417
  } catch (err) {
6108
6418
  log.fail("agent", err, { phase: "resume-on-turn", threadId });
6109
6419
  const project = await getProjectByChatId(chatId);
@@ -6116,7 +6426,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6116
6426
  network: perm?.network ?? project?.network
6117
6427
  });
6118
6428
  sessions.set(threadId, fresh);
6119
- return fresh;
6429
+ await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
6430
+ return { thread: fresh, recreated: true };
6120
6431
  }
6121
6432
  }
6122
6433
  async function evictLiveSessionsForChat(chatId) {
@@ -6147,8 +6458,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6147
6458
  await channel.send(msg.chatId, { markdown: `\u274C \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId }).catch(() => void 0);
6148
6459
  return;
6149
6460
  }
6150
- const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6151
6461
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6462
+ const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6152
6463
  log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
6153
6464
  await launchRun(
6154
6465
  {
@@ -6161,7 +6472,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6161
6472
  model,
6162
6473
  effort,
6163
6474
  cwd,
6164
- summary: text.slice(0, 80) || "(\u7A7A)",
6475
+ summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
6165
6476
  requesterOpenId: msg.senderId,
6166
6477
  roleSuffix: perm.roleSuffix
6167
6478
  },
package/dist/index.d.ts CHANGED
@@ -42,6 +42,9 @@ declare const paths: {
42
42
  */
43
43
  secretsGetterScript: string;
44
44
  mediaDir: string;
45
+ /** Inbound file attachments downloaded from chat, handed to codex by absolute
46
+ * path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
47
+ inboundDir: string;
45
48
  };
46
49
 
47
50
  export { log, newTraceId, paths, withTrace };
package/dist/index.js CHANGED
@@ -47,7 +47,10 @@ var paths = {
47
47
  * passes lark-cli's AssertSecurePath audit.
48
48
  */
49
49
  secretsGetterScript: join(appDir, "secrets-getter"),
50
- mediaDir: join(appDir, "media")
50
+ mediaDir: join(appDir, "media"),
51
+ /** Inbound file attachments downloaded from chat, handed to codex by absolute
52
+ * path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
53
+ inboundDir: join(appDir, "inbound")
51
54
  };
52
55
 
53
56
  // src/core/logger.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {