@modelzen/feishu-codex-bridge 0.3.5 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +231 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5606,6 +5606,199 @@ ${lines}
5606
5606
  ]`;
5607
5607
  }
5608
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
+ }
5801
+
5609
5802
  // src/bot/comments.ts
5610
5803
  var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
5611
5804
  var REPLY_MAX_CHARS = 2e3;
@@ -6091,20 +6284,26 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6091
6284
  const perm = turnPerm(project, senderId);
6092
6285
  return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
6093
6286
  }
6094
- async function ingestFiles(msg, text) {
6095
- if (!messageHasFiles(msg)) return text;
6096
- const files = await collectInboundFiles(channel, msg);
6097
- const woven = weaveFileManifest(text, files);
6098
- if (!woven.trim()) {
6099
- return "\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";
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);
6100
6299
  }
6101
- return woven;
6300
+ return body;
6102
6301
  }
6103
6302
  async function handleTurn(msg, text, sessionKey, flat, project, perm) {
6104
6303
  const existing = active.get(sessionKey);
6105
6304
  if (existing) {
6106
6305
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6107
- const woven = await ingestFiles(msg, text);
6306
+ const woven = await ingestContext(msg, text);
6108
6307
  const cur = active.get(sessionKey);
6109
6308
  if (!cur) {
6110
6309
  startReservedRun(msg, woven, sessionKey, flat, project, perm, images, true, text);
@@ -6141,8 +6340,15 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6141
6340
  const reaction = runReaction(msg.messageId, !sema.hasFree());
6142
6341
  try {
6143
6342
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
6144
- const firstText = preIngested ? text : await ingestFiles(msg, text);
6145
- 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);
6146
6352
  if (!thread) {
6147
6353
  const cwd = project?.cwd ?? fallbackCwd;
6148
6354
  thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
@@ -6156,10 +6362,19 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6156
6362
  // `summaryText` (handleTurn's original) so the session label isn't
6157
6363
  // manifest boilerplate + a temp path.
6158
6364
  summary: stripFileTokens(summaryText2 ?? text).slice(0, 80),
6365
+ lastSeenAt: msg.createTime,
6159
6366
  createdAt: Date.now(),
6160
6367
  updatedAt: Date.now()
6161
6368
  });
6162
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);
6163
6378
  reserved.thread = thread;
6164
6379
  await launchRun(
6165
6380
  {
@@ -6185,9 +6400,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6185
6400
  }
6186
6401
  async function resolveThread(threadId, chatId, perm) {
6187
6402
  const live = sessions.get(threadId);
6188
- if (live) return live;
6403
+ if (live) return { thread: live, recreated: false };
6189
6404
  const rec = await getSession(threadId);
6190
- if (!rec) return void 0;
6405
+ if (!rec) return { thread: void 0, recreated: false };
6191
6406
  try {
6192
6407
  const resumed = await backend.resumeThread({
6193
6408
  cwd: rec.cwd,
@@ -6198,7 +6413,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6198
6413
  network: perm?.network
6199
6414
  });
6200
6415
  sessions.set(threadId, resumed);
6201
- return resumed;
6416
+ return { thread: resumed, recreated: false };
6202
6417
  } catch (err) {
6203
6418
  log.fail("agent", err, { phase: "resume-on-turn", threadId });
6204
6419
  const project = await getProjectByChatId(chatId);
@@ -6211,7 +6426,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6211
6426
  network: perm?.network ?? project?.network
6212
6427
  });
6213
6428
  sessions.set(threadId, fresh);
6214
- return fresh;
6429
+ await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
6430
+ return { thread: fresh, recreated: true };
6215
6431
  }
6216
6432
  }
6217
6433
  async function evictLiveSessionsForChat(chatId) {
@@ -6243,7 +6459,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6243
6459
  return;
6244
6460
  }
6245
6461
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6246
- const firstText = await ingestFiles(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6462
+ const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6247
6463
  log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
6248
6464
  await launchRun(
6249
6465
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.5",
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": {