@modelzen/feishu-codex-bridge 0.1.5 → 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 +217 -13
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2764,6 +2764,27 @@ function connLabel(state) {
2764
2764
  return state;
2765
2765
  }
2766
2766
  }
2767
+ function scopeStatusText(i) {
2768
+ if (i.missingScopes === void 0) return "\u672A\u80FD\u81EA\u52A8\u68C0\u67E5\uFF08\u51ED\u8BC1\u5931\u6548\u6216\u7F51\u7EDC\u95EE\u9898\uFF09";
2769
+ if (i.missingScopes.length === 0) return "\u5FC5\u9700\u6743\u9650\u9F50\u5168";
2770
+ return `\u7F3A\u5931 ${i.missingScopes.length} \u9879\uFF1A${i.missingScopes.join(" ")}`;
2771
+ }
2772
+ function scopeDiagnosis(i) {
2773
+ if (i.missingScopes === void 0) {
2774
+ return [
2775
+ md("- \u98DE\u4E66\u6743\u9650\uFF1A\u26A0\uFE0F \u65E0\u6CD5\u81EA\u52A8\u68C0\u67E5\uFF08\u51ED\u8BC1\u5931\u6548\u6216\u7F51\u7EDC\u4E0D\u901A\uFF09"),
2776
+ actions([linkButton("\u{1F511} \u53BB\u6743\u9650\u9875\u6838\u5BF9", i.scopeGrantUrl)])
2777
+ ];
2778
+ }
2779
+ if (i.missingScopes.length === 0) {
2780
+ return [md("- \u98DE\u4E66\u6743\u9650\uFF1A\u2705 \u5FC5\u9700\u6743\u9650\u5DF2\u5168\u90E8\u5F00\u901A")];
2781
+ }
2782
+ return [
2783
+ md(`- \u98DE\u4E66\u6743\u9650\uFF1A\u274C \u7F3A ${i.missingScopes.length} \u9879 \u2014\u2014 \u5F00\u901A\u524D\u76F8\u5173\u529F\u80FD\uFF08\u6536\u53D1\u6D88\u606F / \u5361\u7247 / \u5EFA\u7FA4\u7B49\uFF09\u4E0D\u53EF\u7528`),
2784
+ note(`\u5F85\u5F00\u901A\uFF1A${i.missingScopes.join("\u3000")}`),
2785
+ actions([linkButton("\u{1F511} \u4E00\u952E\u53BB\u5F00\u901A\u8FD9\u4E9B\u6743\u9650", i.scopeGrantUrl)])
2786
+ ];
2787
+ }
2767
2788
  function codexDiagnosePrompt(i) {
2768
2789
  return [
2769
2790
  "\u6211\u5728\u7528 feishu-codex-bridge\uFF08\u98DE\u4E66 \u2194 \u672C\u5730 Codex \u6865\u63A5\uFF09\u9047\u5230\u95EE\u9898\uFF0C\u8BF7\u5E2E\u6211\u5B9A\u4F4D\u539F\u56E0\u5E76\u7ED9\u51FA\u4FEE\u590D\u6B65\u9AA4\u3002",
@@ -2778,6 +2799,7 @@ function codexDiagnosePrompt(i) {
2778
2799
  "\u3010\u8FD0\u884C\u5FEB\u7167\u3011",
2779
2800
  `- codex \u53EF\u7528\uFF1A${i.codexOk ? "\u662F" : "\u5426"}`,
2780
2801
  `- \u98DE\u4E66\u957F\u8FDE\u63A5\uFF1A${i.conn}`,
2802
+ `- \u98DE\u4E66\u6743\u9650\uFF1A${scopeStatusText(i)}`,
2781
2803
  "",
2782
2804
  "\u3010\u8BF7\u4F60\u505A\u7684\u4E8B\u3011",
2783
2805
  "1. \u8BFB\u53D6\u5E76\u5206\u6790\u65E5\u5FD7\uFF0C\u627E\u51FA\u6700\u8FD1\u7684\u62A5\u9519\u6216\u5F02\u5E38\u5806\u6808\uFF1A",
@@ -2794,6 +2816,7 @@ function codexDiagnosePrompt(i) {
2794
2816
  }
2795
2817
  function buildDoctorCard(i) {
2796
2818
  const prompt = codexDiagnosePrompt(i);
2819
+ const hasProblem = !i.codexOk || i.missingScopes !== void 0 && i.missingScopes.length > 0;
2797
2820
  return card(
2798
2821
  [
2799
2822
  md("**\u521D\u6B65\u8BCA\u65AD**"),
@@ -2801,6 +2824,7 @@ function buildDoctorCard(i) {
2801
2824
  `- Codex\uFF1A${i.codexOk ? `\u2705 \u53EF\u7528${i.codexVer ? `\uFF08${i.codexVer}\uFF09` : ""}` : "\u274C \u4E0D\u53EF\u7528\uFF08\u68C0\u67E5 CODEX_BIN / PATH\uFF09"}`
2802
2825
  ),
2803
2826
  md(`- \u98DE\u4E66\u957F\u8FDE\u63A5\uFF1A${connLabel(i.conn)}`),
2827
+ ...scopeDiagnosis(i),
2804
2828
  note(`bridge v${i.bridgeVer}\u3000\xB7\u3000Node ${i.node}\u3000\xB7\u3000${i.platform}`),
2805
2829
  hr(),
2806
2830
  md("**\u65E5\u5FD7\u8DEF\u5F84**"),
@@ -2815,7 +2839,7 @@ function buildDoctorCard(i) {
2815
2839
  linkButton("\u{1F41E} \u63D0 Issue", `${REPO}/issues`)
2816
2840
  ])
2817
2841
  ],
2818
- { header: { title: "\u{1FA7A} \u8BCA\u65AD", template: i.codexOk ? "blue" : "orange" } }
2842
+ { header: { title: "\u{1FA7A} \u8BCA\u65AD", template: hasProblem ? "orange" : "blue" } }
2819
2843
  );
2820
2844
  }
2821
2845
  function buildNewProjectFormCard(opts = {}) {
@@ -3504,6 +3528,156 @@ async function handleDmConsole(channel, cfg, msg) {
3504
3528
  });
3505
3529
  }
3506
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
+
3507
3681
  // src/bot/comments.ts
3508
3682
  var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
3509
3683
  var REPLY_MAX_CHARS = 2e3;
@@ -3898,19 +4072,34 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3898
4072
  async function handleTurn(msg, text, sessionKey, flat, project) {
3899
4073
  const existing = active.get(sessionKey);
3900
4074
  if (existing) {
3901
- if (getPendingPolicy(cfg) === "steer" && existing.run && existing.thread) {
3902
- 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();
3903
4083
  if (tid) {
3904
4084
  try {
3905
- await existing.thread.steer({ text }, tid);
3906
- log.info("intake", "steer", { tid });
4085
+ await cur.thread.steer({ text, images }, tid);
4086
+ log.info("intake", "steer", { tid, images: images?.length ?? 0 });
3907
4087
  return;
3908
4088
  } catch (err) {
3909
4089
  log.warn("intake", "steer-failed", { err: String(err) });
3910
4090
  }
3911
4091
  }
3912
4092
  }
3913
- 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 });
3914
4103
  log.info("intake", "queued", { depth: existing.queue.length });
3915
4104
  return;
3916
4105
  }
@@ -3919,6 +4108,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3919
4108
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
3920
4109
  const reaction = runReaction(msg.messageId, !sema.hasFree());
3921
4110
  try {
4111
+ const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
3922
4112
  let thread = await resolveThread(sessionKey, msg.chatId);
3923
4113
  if (!thread) {
3924
4114
  const cwd = project?.cwd ?? fallbackCwd;
@@ -3943,6 +4133,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3943
4133
  flat,
3944
4134
  thread,
3945
4135
  firstText: text,
4136
+ images,
3946
4137
  knownThreadId: sessionKey,
3947
4138
  requesterOpenId: msg.senderId
3948
4139
  },
@@ -3995,7 +4186,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
3995
4186
  return;
3996
4187
  }
3997
4188
  const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
3998
- 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 });
3999
4191
  await launchRun(
4000
4192
  {
4001
4193
  chatId: msg.chatId,
@@ -4003,6 +4195,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4003
4195
  replyInThread: true,
4004
4196
  thread,
4005
4197
  firstText,
4198
+ images,
4006
4199
  model,
4007
4200
  effort,
4008
4201
  cwd,
@@ -4202,6 +4395,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4202
4395
  }).on(DM.doctor, async ({ evt }) => {
4203
4396
  if (!dmAdmin(evt.operator?.openId)) return;
4204
4397
  const codexBin = resolveCodexBin();
4398
+ const app = cfg.accounts.app;
4399
+ const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
4400
+ const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
4401
+ const missingScopes = scopeCheck?.missingScopes;
4205
4402
  const info = {
4206
4403
  codexOk: await backend.isAvailable().catch(() => false),
4207
4404
  codexVer: codexBin ? codexVersion(codexBin) : null,
@@ -4211,7 +4408,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4211
4408
  platform: `${process.platform}-${process.arch}`,
4212
4409
  logStdout: serviceStdoutPath(),
4213
4410
  logStderr: serviceStderrPath(),
4214
- configFile: paths.configFile
4411
+ configFile: paths.configFile,
4412
+ missingScopes,
4413
+ // 缺失时预选缺失项(精准开通);查不到/全开通时预选全部必需 scope 供核对。
4414
+ scopeGrantUrl: buildScopeGrantUrl(
4415
+ app.id,
4416
+ app.tenant,
4417
+ missingScopes && missingScopes.length ? missingScopes : void 0
4418
+ )
4215
4419
  };
4216
4420
  await sendManagedCard(channel, evt.chatId, buildDoctorCard(info), evt.messageId).catch(
4217
4421
  (err) => log.fail("console", err, { cmd: "doctor" })
@@ -4397,14 +4601,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4397
4601
  };
4398
4602
  let curCardKey;
4399
4603
  try {
4400
- let turnText = opts.firstText;
4604
+ let turnInput = { text: opts.firstText, images: opts.images };
4401
4605
  let replyTo = opts.replyTo;
4402
4606
  let replyInThread = opts.flat ? false : opts.replyInThread ?? Boolean(opts.knownThreadId);
4403
4607
  for (; ; ) {
4404
4608
  const rec = topicThreadId ? await getSession(topicThreadId) : void 0;
4405
4609
  const turnModel = rec?.model ?? opts.model;
4406
4610
  const turnEffort = rec?.effort ?? opts.effort;
4407
- const run = opts.thread.runStreamed({ text: turnText }, { model: turnModel, effort: turnEffort });
4611
+ const run = opts.thread.runStreamed(turnInput, { model: turnModel, effort: turnEffort });
4408
4612
  state.run = run;
4409
4613
  const render = new RunRender();
4410
4614
  render.showTools = getShowToolCalls(cfg);
@@ -4491,7 +4695,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4491
4695
  log.info("card", "final", { terminal: render.terminal() });
4492
4696
  if (killed) break;
4493
4697
  if (state.queue.length === 0) break;
4494
- turnText = state.queue.shift();
4698
+ turnInput = state.queue.shift();
4495
4699
  }
4496
4700
  } catch (err) {
4497
4701
  log.fail("intake", err);
@@ -4855,7 +5059,7 @@ async function runUpdate(opts = {}) {
4855
5059
  }
4856
5060
 
4857
5061
  // src/cli/commands/bot.ts
4858
- import { rm as rm3 } from "fs/promises";
5062
+ import { rm as rm4 } from "fs/promises";
4859
5063
  async function runBotInit(name) {
4860
5064
  if (!ensureCodex()) {
4861
5065
  process.exitCode = 1;
@@ -4909,7 +5113,7 @@ async function runBotRm(name) {
4909
5113
  }
4910
5114
  const after = await removeBot(bot2.appId);
4911
5115
  await removeSecret(secretKeyForApp(bot2.appId));
4912
- await rm3(botDir(bot2.appId), { recursive: true, force: true });
5116
+ await rm4(botDir(bot2.appId), { recursive: true, force: true });
4913
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`);
4914
5118
  if (after.bots.length === 0) {
4915
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.5",
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": {