@modelzen/feishu-codex-bridge 0.1.6 → 0.1.7

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 +180 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3528,6 +3528,156 @@ async function handleDmConsole(channel, cfg, msg) {
3528
3528
  });
3529
3529
  }
3530
3530
 
3531
+ // src/bot/media.ts
3532
+ import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat2 } from "fs/promises";
3533
+ import { join as join10 } from "path";
3534
+ var MAX_IMAGES = 9;
3535
+ var MEDIA_TTL_MS = 60 * 6e4;
3536
+ var EXT_BY_CONTENT_TYPE = {
3537
+ "image/png": "png",
3538
+ "image/jpeg": "jpg",
3539
+ "image/jpg": "jpg",
3540
+ "image/gif": "gif",
3541
+ "image/webp": "webp",
3542
+ "image/bmp": "bmp",
3543
+ "image/heic": "heic",
3544
+ "image/heif": "heif",
3545
+ "image/tiff": "tiff"
3546
+ };
3547
+ function messageHasImages(msg) {
3548
+ if ((msg.resources ?? []).some((r) => r.type === "image")) return true;
3549
+ return msg.rawContentType === "merge_forward";
3550
+ }
3551
+ async function collectInboundImages(channel, msg) {
3552
+ let refs;
3553
+ try {
3554
+ refs = await gatherRefs(channel, msg);
3555
+ } catch (err) {
3556
+ log.warn("intake", "image-gather-failed", { err: String(err) });
3557
+ return [];
3558
+ }
3559
+ if (refs.length === 0) return [];
3560
+ await pruneOldMedia();
3561
+ try {
3562
+ await mkdir8(paths.mediaDir, { recursive: true });
3563
+ } catch {
3564
+ }
3565
+ const out = [];
3566
+ let index = 0;
3567
+ for (const ref of refs.slice(0, MAX_IMAGES)) {
3568
+ const path = await downloadOne(channel, ref, index++);
3569
+ if (path) out.push(path);
3570
+ }
3571
+ log.info("intake", "images", { found: refs.length, downloaded: out.length });
3572
+ return out;
3573
+ }
3574
+ async function gatherRefs(channel, msg) {
3575
+ const refs = [];
3576
+ const seen = /* @__PURE__ */ new Set();
3577
+ const add = (messageId, fileKey) => {
3578
+ if (!fileKey || seen.has(fileKey)) return;
3579
+ seen.add(fileKey);
3580
+ refs.push({ messageId, fileKey });
3581
+ };
3582
+ for (const r of msg.resources ?? []) {
3583
+ if (r.type === "image") add(msg.messageId, r.fileKey);
3584
+ }
3585
+ if (msg.rawContentType === "merge_forward") {
3586
+ const items = await fetchSubMessages(channel, msg.messageId);
3587
+ for (const sub of items) {
3588
+ if (!sub.message_id || sub.message_id === msg.messageId) continue;
3589
+ for (const key of imageKeysFromContent(sub.msg_type, sub.body?.content)) {
3590
+ add(sub.message_id, key);
3591
+ }
3592
+ }
3593
+ }
3594
+ return refs;
3595
+ }
3596
+ async function fetchSubMessages(channel, messageId) {
3597
+ try {
3598
+ const res = await channel.rawClient.im.v1.message.get({ path: { message_id: messageId } });
3599
+ return res.data?.items ?? [];
3600
+ } catch (err) {
3601
+ log.warn("intake", "submessages-failed", { messageId, err: String(err) });
3602
+ return [];
3603
+ }
3604
+ }
3605
+ function imageKeysFromContent(msgType, content) {
3606
+ if (!content) return [];
3607
+ let parsed;
3608
+ try {
3609
+ parsed = JSON.parse(content);
3610
+ } catch {
3611
+ return [];
3612
+ }
3613
+ if (msgType === "image") {
3614
+ const key = parsed?.image_key;
3615
+ return key ? [key] : [];
3616
+ }
3617
+ const keys = [];
3618
+ walkForImageKeys(parsed, keys);
3619
+ return keys;
3620
+ }
3621
+ function walkForImageKeys(node, out) {
3622
+ if (!node || typeof node !== "object") return;
3623
+ if (Array.isArray(node)) {
3624
+ for (const child of node) walkForImageKeys(child, out);
3625
+ return;
3626
+ }
3627
+ const obj = node;
3628
+ if (obj.tag === "img" && typeof obj.image_key === "string") out.push(obj.image_key);
3629
+ for (const k of Object.keys(obj)) walkForImageKeys(obj[k], out);
3630
+ }
3631
+ async function downloadOne(channel, ref, index) {
3632
+ try {
3633
+ const res = await channel.rawClient.im.v1.messageResource.get({
3634
+ path: { message_id: ref.messageId, file_key: ref.fileKey },
3635
+ params: { type: "image" }
3636
+ });
3637
+ const ext = extFromHeaders(res.headers);
3638
+ const file = join10(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
3639
+ await res.writeFile(file);
3640
+ return file;
3641
+ } catch (err) {
3642
+ log.warn("intake", "image-download-failed", { fileKey: ref.fileKey.slice(0, 24), err: String(err) });
3643
+ return void 0;
3644
+ }
3645
+ }
3646
+ function extFromHeaders(headers) {
3647
+ const ct = readHeader(headers, "content-type");
3648
+ if (ct) {
3649
+ const base = ct.split(";")[0]?.trim().toLowerCase();
3650
+ if (base && EXT_BY_CONTENT_TYPE[base]) return EXT_BY_CONTENT_TYPE[base];
3651
+ }
3652
+ return "png";
3653
+ }
3654
+ function readHeader(headers, name) {
3655
+ if (!headers || typeof headers !== "object") return void 0;
3656
+ const h = headers;
3657
+ const raw = typeof h.get === "function" ? h.get(name) : h[name] ?? h[name.toLowerCase()];
3658
+ return typeof raw === "string" ? raw : Array.isArray(raw) ? String(raw[0]) : void 0;
3659
+ }
3660
+ function safeName(fileKey) {
3661
+ return fileKey.replace(/[^a-zA-Z0-9_-]/g, "").slice(-40) || "img";
3662
+ }
3663
+ async function pruneOldMedia() {
3664
+ let entries;
3665
+ try {
3666
+ entries = await readdir2(paths.mediaDir);
3667
+ } catch {
3668
+ return;
3669
+ }
3670
+ const cutoff = Date.now() - MEDIA_TTL_MS;
3671
+ for (const name of entries) {
3672
+ const file = join10(paths.mediaDir, name);
3673
+ try {
3674
+ const st = await stat2(file);
3675
+ if (st.mtimeMs < cutoff) await rm3(file, { force: true });
3676
+ } catch {
3677
+ }
3678
+ }
3679
+ }
3680
+
3531
3681
  // src/bot/comments.ts
3532
3682
  var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
3533
3683
  var REPLY_MAX_CHARS = 2e3;
@@ -3922,19 +4072,34 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3922
4072
  async function handleTurn(msg, text, sessionKey, flat, project) {
3923
4073
  const existing = active.get(sessionKey);
3924
4074
  if (existing) {
3925
- if (getPendingPolicy(cfg) === "steer" && existing.run && existing.thread) {
3926
- const tid = existing.run.turnId();
4075
+ const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
4076
+ const cur = active.get(sessionKey);
4077
+ if (!cur) {
4078
+ startReservedRun(msg, text, sessionKey, flat, project, images);
4079
+ return;
4080
+ }
4081
+ if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
4082
+ const tid = cur.run.turnId();
3927
4083
  if (tid) {
3928
4084
  try {
3929
- await existing.thread.steer({ text }, tid);
3930
- log.info("intake", "steer", { tid });
4085
+ await cur.thread.steer({ text, images }, tid);
4086
+ log.info("intake", "steer", { tid, images: images?.length ?? 0 });
3931
4087
  return;
3932
4088
  } catch (err) {
3933
4089
  log.warn("intake", "steer-failed", { err: String(err) });
3934
4090
  }
3935
4091
  }
3936
4092
  }
3937
- existing.queue.push(text);
4093
+ cur.queue.push({ text, images });
4094
+ log.info("intake", "queued", { depth: cur.queue.length });
4095
+ return;
4096
+ }
4097
+ startReservedRun(msg, text, sessionKey, flat, project);
4098
+ }
4099
+ function startReservedRun(msg, text, sessionKey, flat, project, preloadedImages) {
4100
+ const existing = active.get(sessionKey);
4101
+ if (existing) {
4102
+ existing.queue.push({ text, images: preloadedImages });
3938
4103
  log.info("intake", "queued", { depth: existing.queue.length });
3939
4104
  return;
3940
4105
  }
@@ -3943,6 +4108,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3943
4108
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
3944
4109
  const reaction = runReaction(msg.messageId, !sema.hasFree());
3945
4110
  try {
4111
+ const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
3946
4112
  let thread = await resolveThread(sessionKey, msg.chatId);
3947
4113
  if (!thread) {
3948
4114
  const cwd = project?.cwd ?? fallbackCwd;
@@ -3967,6 +4133,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3967
4133
  flat,
3968
4134
  thread,
3969
4135
  firstText: text,
4136
+ images,
3970
4137
  knownThreadId: sessionKey,
3971
4138
  requesterOpenId: msg.senderId
3972
4139
  },
@@ -4019,7 +4186,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4019
4186
  return;
4020
4187
  }
4021
4188
  const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
4022
- log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort });
4189
+ const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
4190
+ log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
4023
4191
  await launchRun(
4024
4192
  {
4025
4193
  chatId: msg.chatId,
@@ -4027,6 +4195,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4027
4195
  replyInThread: true,
4028
4196
  thread,
4029
4197
  firstText,
4198
+ images,
4030
4199
  model,
4031
4200
  effort,
4032
4201
  cwd,
@@ -4432,14 +4601,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4432
4601
  };
4433
4602
  let curCardKey;
4434
4603
  try {
4435
- let turnText = opts.firstText;
4604
+ let turnInput = { text: opts.firstText, images: opts.images };
4436
4605
  let replyTo = opts.replyTo;
4437
4606
  let replyInThread = opts.flat ? false : opts.replyInThread ?? Boolean(opts.knownThreadId);
4438
4607
  for (; ; ) {
4439
4608
  const rec = topicThreadId ? await getSession(topicThreadId) : void 0;
4440
4609
  const turnModel = rec?.model ?? opts.model;
4441
4610
  const turnEffort = rec?.effort ?? opts.effort;
4442
- const run = opts.thread.runStreamed({ text: turnText }, { model: turnModel, effort: turnEffort });
4611
+ const run = opts.thread.runStreamed(turnInput, { model: turnModel, effort: turnEffort });
4443
4612
  state.run = run;
4444
4613
  const render = new RunRender();
4445
4614
  render.showTools = getShowToolCalls(cfg);
@@ -4526,7 +4695,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4526
4695
  log.info("card", "final", { terminal: render.terminal() });
4527
4696
  if (killed) break;
4528
4697
  if (state.queue.length === 0) break;
4529
- turnText = state.queue.shift();
4698
+ turnInput = state.queue.shift();
4530
4699
  }
4531
4700
  } catch (err) {
4532
4701
  log.fail("intake", err);
@@ -4890,7 +5059,7 @@ async function runUpdate(opts = {}) {
4890
5059
  }
4891
5060
 
4892
5061
  // src/cli/commands/bot.ts
4893
- import { rm as rm3 } from "fs/promises";
5062
+ import { rm as rm4 } from "fs/promises";
4894
5063
  async function runBotInit(name) {
4895
5064
  if (!ensureCodex()) {
4896
5065
  process.exitCode = 1;
@@ -4944,7 +5113,7 @@ async function runBotRm(name) {
4944
5113
  }
4945
5114
  const after = await removeBot(bot2.appId);
4946
5115
  await removeSecret(secretKeyForApp(bot2.appId));
4947
- await rm3(botDir(bot2.appId), { recursive: true, force: true });
5116
+ await rm4(botDir(bot2.appId), { recursive: true, force: true });
4948
5117
  console.log(`\u2713 \u5DF2\u79FB\u9664\u673A\u5668\u4EBA\u300C${bot2.name}\u300D(${bot2.appId})\uFF1A\u6CE8\u518C\u8868 + \u5BC6\u94A5 + \u72B6\u6001\u76EE\u5F55(projects/sessions)\u3002`);
4949
5118
  if (after.bots.length === 0) {
4950
5119
  console.log(" \u5DF2\u65E0\u4EFB\u4F55\u673A\u5668\u4EBA\uFF0C`bot init` \u91CD\u65B0\u521B\u5EFA\u3002");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {