@modelzen/feishu-codex-bridge 0.1.7 → 0.1.8

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 +317 -32
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -538,7 +538,7 @@ async function spawnExecProvider(pc, ref) {
538
538
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
539
539
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
540
540
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
541
- return new Promise((resolve5, reject) => {
541
+ return new Promise((resolve6, reject) => {
542
542
  const env = {};
543
543
  if (pc.passEnv) for (const k of pc.passEnv) {
544
544
  const v = process.env[k];
@@ -583,7 +583,7 @@ async function spawnExecProvider(pc, ref) {
583
583
  try {
584
584
  const parsed = JSON.parse(stdout);
585
585
  const value = parsed.values?.[ref.id];
586
- if (typeof value === "string") return resolve5(value);
586
+ if (typeof value === "string") return resolve6(value);
587
587
  const err = parsed.errors?.[ref.id]?.message;
588
588
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
589
589
  } catch (err) {
@@ -680,6 +680,30 @@ var COMMENT_SCOPES = [
680
680
  "wiki:wiki:readonly"
681
681
  ];
682
682
  var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES];
683
+ var SCOPE_LABELS = {
684
+ "im:message.group_at_msg:readonly": "\u63A5\u6536\u7FA4\u91CC @\u673A\u5668\u4EBA \u7684\u6D88\u606F",
685
+ "im:message.group_msg": "\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\uFF08\u514D@\uFF09",
686
+ "im:message.p2p_msg:readonly": "\u63A5\u6536\u79C1\u804A\u6D88\u606F\uFF08\u7BA1\u7406\u53F0\uFF09",
687
+ "im:message:send_as_bot": "\u53D1\u9001\u6D88\u606F / \u5361\u7247",
688
+ "im:message.pins:write_only": "\u7F6E\u9876\u6D88\u606F\u5230\u7FA4 Pin",
689
+ "im:message.reactions:write_only": "\u6D88\u606F\u8868\u60C5\u56DE\u590D\uFF08\u8FD0\u884C\u72B6\u6001\uFF09",
690
+ "im:resource": "\u56FE\u7247 / \u6587\u4EF6\u4E0A\u4F20\u4E0E\u4E0B\u8F7D",
691
+ "im:chat:create": "\u521B\u5EFA\u9879\u76EE\u7FA4",
692
+ "im:chat:update": "\u8F6C\u79FB\u7FA4\u4E3B\uFF08\u89E3\u7ED1\u65F6\uFF09",
693
+ "im:chat.managers:write_only": "\u8BBE\u7F6E\u7FA4\u7BA1\u7406\u5458",
694
+ "im:chat.announcement:read": "\u8BFB\u53D6\u7FA4\u516C\u544A",
695
+ "im:chat.announcement:write_only": "\u7F16\u8F91\u7FA4\u516C\u544A",
696
+ "im:chat.top_notice:write_only": "\u7F6E\u9876\u7FA4\u516C\u544A\u6A2A\u5E45",
697
+ "im:chat.tabs:write_only": "\u6DFB\u52A0\u7FA4\u6807\u7B7E\u9875",
698
+ "cardkit:card:write": "\u4EA4\u4E92\u6309\u94AE\u5361\u7247",
699
+ "docs:document.comment:read": "\u8BFB\u53D6\u6587\u6863\u8BC4\u8BBA",
700
+ "docs:document.comment:create": "\u53D1\u8868\u6587\u6863\u8BC4\u8BBA\u56DE\u590D",
701
+ "wiki:wiki:readonly": "\u8BFB\u53D6\u77E5\u8BC6\u5E93\u8282\u70B9"
702
+ };
703
+ function labelScope(scope) {
704
+ const label = SCOPE_LABELS[scope];
705
+ return label ? `${label}\uFF08${scope}\uFF09` : scope;
706
+ }
683
707
  var HOSTS = {
684
708
  feishu: "open.feishu.cn",
685
709
  lark: "open.larksuite.com"
@@ -1112,7 +1136,7 @@ var AsyncQueue = class {
1112
1136
  continue;
1113
1137
  }
1114
1138
  if (this.closed) return;
1115
- const next = await new Promise((resolve5) => this.waiters.push(resolve5));
1139
+ const next = await new Promise((resolve6) => this.waiters.push(resolve6));
1116
1140
  if (next.done) return;
1117
1141
  yield next.value;
1118
1142
  }
@@ -1163,8 +1187,8 @@ var AppServerClient = class {
1163
1187
  const id = ++this.nextId;
1164
1188
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1165
1189
  `;
1166
- return new Promise((resolve5, reject) => {
1167
- this.pending.set(id, { resolve: resolve5, reject });
1190
+ return new Promise((resolve6, reject) => {
1191
+ this.pending.set(id, { resolve: resolve6, reject });
1168
1192
  this.child.stdin.write(payload, (err) => {
1169
1193
  if (err) {
1170
1194
  this.pending.delete(id);
@@ -1188,14 +1212,14 @@ var AppServerClient = class {
1188
1212
  const child = this.child;
1189
1213
  if (!child || child.exitCode !== null) return;
1190
1214
  child.kill("SIGTERM");
1191
- await new Promise((resolve5) => {
1215
+ await new Promise((resolve6) => {
1192
1216
  const t = setTimeout(() => {
1193
1217
  if (child.exitCode === null) child.kill("SIGKILL");
1194
- resolve5();
1218
+ resolve6();
1195
1219
  }, graceMs);
1196
1220
  child.once("exit", () => {
1197
1221
  clearTimeout(t);
1198
- resolve5();
1222
+ resolve6();
1199
1223
  });
1200
1224
  });
1201
1225
  }
@@ -1312,14 +1336,28 @@ function mapItemComplete(item) {
1312
1336
  // src/agent/codex-appserver/backend.ts
1313
1337
  var APPROVAL_POLICY = "never";
1314
1338
  var SANDBOX = "danger-full-access";
1339
+ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1340
+ "\u4F60\u73B0\u5728\u901A\u8FC7\u300C\u98DE\u4E66\u6865\u300D\u4E0E\u7528\u6237\u5BF9\u8BDD\uFF1A\u4F60\u7684\u56DE\u590D\u4F1A\u88AB\u6E32\u67D3\u6210\u98DE\u4E66\u6D88\u606F\u3002\u8BF7\u9075\u5B88\u4E24\u6761\u8F93\u51FA\u7EA6\u5B9A\u3002",
1341
+ "",
1342
+ "1) \u56FE\u7247\uFF1A\u8981\u914D\u56FE\u65F6\uFF0C\u7528\u6807\u51C6 Markdown \u56FE\u7247\u8BED\u6CD5 ![\u8BF4\u660E](\u8DEF\u5F84) \u5F15\u7528\u4E00\u4E2A\u3010\u771F\u5B9E\u5B58\u5728\u3011\u7684\u56FE\u7247\uFF0C",
1343
+ "\u98DE\u4E66\u6865\u4F1A\u81EA\u52A8\u4E0A\u4F20\u5E76\u5728\u98DE\u4E66\u91CC\u6E32\u67D3\u3002\u8DEF\u5F84\u53EF\u4EE5\u662F\u76F8\u5BF9\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u7684\u76F8\u5BF9\u8DEF\u5F84\u3001\u5DE5\u4F5C\u76EE\u5F55\u5185\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C",
1344
+ "\u6216\u4E00\u4E2A http(s) \u56FE\u7247 URL\u3002\u7EDD\u4E0D\u8981\u7F16\u9020\u4E0D\u5B58\u5728\u7684\u56FE\u7247\u5360\u4F4D\uFF08\u4F8B\u5982\u5199 ![\u7BA1\u7406\u53F0\u622A\u56FE] \u5374\u6CA1\u6709\u5BF9\u5E94\u6587\u4EF6\uFF09\u2014\u2014",
1345
+ "\u6CA1\u6709\u771F\u5B9E\u56FE\u7247\u5C31\u4E0D\u8981\u5199\u56FE\u7247\u8BED\u6CD5\u3002",
1346
+ "",
1347
+ "2) \u5361\u7247\uFF1A\u4EC5\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u300C\u7528\u5361\u7247\u56DE\u590D / \u505A\u6210\u98DE\u4E66\u5361\u7247 / \u5361\u7247\u5F62\u5F0F\u5C55\u793A / changelog \u5361\u7247\u300D\u4E4B\u7C7B\u65F6\uFF0C",
1348
+ "\u628A\u8981\u5C55\u793A\u7684\u5185\u5BB9\u5305\u8FDB\u4E00\u4E2A ```feishu-card \u4EE3\u7801\u5757\uFF0C\u5757\u5185\u7528 Markdown \u4E66\u5199\uFF1A",
1349
+ "\u9996\u884C\u7528 `# \u6807\u9898` \u4F5C\u4E3A\u5361\u7247\u6807\u9898\u680F\uFF1B\u7528 `---` \u4F5C\u5206\u9694\u7EBF\uFF1B\u7528 `> \u6587\u5B57` \u4F5C\u7070\u8272\u6CE8\u811A\uFF1B",
1350
+ "`**\u7C97\u4F53**`\u3001\u5217\u8868\u3001\u94FE\u63A5\u7167\u5E38\u4F7F\u7528\uFF1B\u914D\u56FE\u540C\u6837\u7528 ![\u8BF4\u660E](\u771F\u5B9E\u8DEF\u5F84)\u3002",
1351
+ "\u4E0D\u8981\u624B\u5199\u98DE\u4E66\u5361\u7247\u7684 JSON\u3002\u666E\u901A\u95EE\u7B54\u6B63\u5E38\u56DE\u590D\u5373\u53EF\uFF0C\u53EA\u6709\u7528\u6237\u8981\u5361\u7247\u65F6\u624D\u7528 ```feishu-card \u4EE3\u7801\u5757\u3002"
1352
+ ].join("\n");
1315
1353
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1316
1354
  function withDeadline(p, ms, label) {
1317
- return new Promise((resolve5, reject) => {
1355
+ return new Promise((resolve6, reject) => {
1318
1356
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1319
1357
  p.then(
1320
1358
  (v) => {
1321
1359
  clearTimeout(t);
1322
- resolve5(v);
1360
+ resolve6(v);
1323
1361
  },
1324
1362
  (e) => {
1325
1363
  clearTimeout(t);
@@ -1359,11 +1397,11 @@ var CodexThread = class {
1359
1397
  if (self.model) params.model = self.model;
1360
1398
  if (self.effort) params.effort = self.effort;
1361
1399
  let startError;
1362
- const startFailed = new Promise((resolve5) => {
1400
+ const startFailed = new Promise((resolve6) => {
1363
1401
  self.client.request("turn/start", params).then(void 0, (err) => {
1364
1402
  startError = err instanceof Error ? err : new Error(String(err));
1365
1403
  log.fail("agent", startError, { phase: "turn/start" });
1366
- resolve5("start-failed");
1404
+ resolve6("start-failed");
1367
1405
  });
1368
1406
  });
1369
1407
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -1487,6 +1525,7 @@ var CodexAppServerBackend = class {
1487
1525
  cwd: opts.cwd,
1488
1526
  approvalPolicy: APPROVAL_POLICY,
1489
1527
  sandbox: SANDBOX,
1528
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1490
1529
  ...opts.model ? { model: opts.model } : {}
1491
1530
  });
1492
1531
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
@@ -1498,6 +1537,7 @@ var CodexAppServerBackend = class {
1498
1537
  cwd: opts.cwd,
1499
1538
  approvalPolicy: APPROVAL_POLICY,
1500
1539
  sandbox: SANDBOX,
1540
+ developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1501
1541
  ...opts.model ? { model: opts.model } : {}
1502
1542
  });
1503
1543
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
@@ -1928,6 +1968,15 @@ function card(elements, opts = {}) {
1928
1968
  function md(content) {
1929
1969
  return { tag: "markdown", content };
1930
1970
  }
1971
+ function image(imgKey, alt = "") {
1972
+ return {
1973
+ tag: "img",
1974
+ img_key: imgKey,
1975
+ alt: { tag: "plain_text", content: alt },
1976
+ mode: "fit_horizontal",
1977
+ preview: true
1978
+ };
1979
+ }
1931
1980
  function note(content) {
1932
1981
  return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
1933
1982
  }
@@ -2337,6 +2386,92 @@ function truncateTail(s, n) {
2337
2386
  return t.length > n ? `\u2026${t.slice(t.length - n)}` : t;
2338
2387
  }
2339
2388
 
2389
+ // src/card/markdown-render.ts
2390
+ var NO_IMAGES = /* @__PURE__ */ new Map();
2391
+ var IMG_RE = /!\[([^\]]*)\]\(\s*(<[^>]+>|[^)\s]+)(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
2392
+ var FENCE_RE = /```feishu-card[^\n]*\n([\s\S]*?)```/g;
2393
+ function cleanSrc(raw) {
2394
+ let s = raw.trim();
2395
+ if (s.startsWith("<") && s.endsWith(">")) s = s.slice(1, -1).trim();
2396
+ return s;
2397
+ }
2398
+ function extractCardFences(text) {
2399
+ const fences = [];
2400
+ const re = new RegExp(FENCE_RE.source, "g");
2401
+ const stripped = text.replace(re, (_full, inner) => {
2402
+ fences.push(inner.trim());
2403
+ return "";
2404
+ });
2405
+ return { fences, stripped };
2406
+ }
2407
+ function renderRichText(text, images = NO_IMAGES) {
2408
+ const body = extractCardFences(text).stripped;
2409
+ if (!body.includes("![")) {
2410
+ const t = body.trim();
2411
+ return t ? [md(t)] : [];
2412
+ }
2413
+ const els = [];
2414
+ let buf = "";
2415
+ const flush = () => {
2416
+ const t = buf.trim();
2417
+ if (t) els.push(md(t));
2418
+ buf = "";
2419
+ };
2420
+ const re = new RegExp(IMG_RE.source, "g");
2421
+ let last = 0;
2422
+ let m;
2423
+ while ((m = re.exec(body)) !== null) {
2424
+ buf += body.slice(last, m.index);
2425
+ const alt = m[1] ?? "";
2426
+ const src = cleanSrc(m[2] ?? "");
2427
+ const key = images.get(src);
2428
+ if (key) {
2429
+ flush();
2430
+ els.push(image(key, alt));
2431
+ } else {
2432
+ buf += m[0];
2433
+ }
2434
+ last = m.index + m[0].length;
2435
+ }
2436
+ buf += body.slice(last);
2437
+ flush();
2438
+ return els;
2439
+ }
2440
+ function buildCleanCard(fenceMarkdown, images = NO_IMAGES, template = "blue") {
2441
+ const lines = fenceMarkdown.split("\n");
2442
+ let start = 0;
2443
+ while (start < lines.length && lines[start]?.trim() === "") start++;
2444
+ const headingMatch = lines[start]?.match(/^#{1,6}\s+(.+?)\s*$/);
2445
+ const title = headingMatch ? headingMatch[1] : "";
2446
+ if (headingMatch) start++;
2447
+ const bodyMarkdown = lines.slice(start).join("\n").trim();
2448
+ const elements = renderCleanBody(bodyMarkdown, images);
2449
+ const body = elements.length > 0 ? elements : [md(title || "\xAD")];
2450
+ return card(body, {
2451
+ ...title ? { header: { title, template } } : {},
2452
+ summary: title || "\u5361\u7247"
2453
+ });
2454
+ }
2455
+ function renderCleanBody(bodyMarkdown, images) {
2456
+ const out = [];
2457
+ for (const raw of bodyMarkdown.split(/\n{2,}/)) {
2458
+ const block = raw.trim();
2459
+ if (!block) continue;
2460
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(block)) {
2461
+ out.push(hr());
2462
+ continue;
2463
+ }
2464
+ const blockLines = block.split("\n");
2465
+ if (blockLines.every((l) => l.trim() === "" || /^\s*>\s?/.test(l))) {
2466
+ const noteText = blockLines.map((l) => l.replace(/^\s*>\s?/, "")).join("\n").trim();
2467
+ if (noteText) out.push(note(noteText));
2468
+ continue;
2469
+ }
2470
+ out.push(...renderRichText(block, images));
2471
+ }
2472
+ return out;
2473
+ }
2474
+
2340
2475
  // src/card/tool-render.ts
2341
2476
  var HEADER_TITLE_MAX = 80;
2342
2477
  var OUTPUT_MAX = 1200;
@@ -2415,7 +2550,7 @@ function renderTerminal(state, rc) {
2415
2550
  })
2416
2551
  );
2417
2552
  }
2418
- if (answer) elements.push(md(answer));
2553
+ if (answer) elements.push(...renderRichText(answer, rc.images));
2419
2554
  if (state.terminal === "interrupted") {
2420
2555
  elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2421
2556
  } else if (state.terminal === "idle_timeout") {
@@ -2624,6 +2759,142 @@ var RunCardStream = class {
2624
2759
  }
2625
2760
  };
2626
2761
 
2762
+ // src/card/outbound-images.ts
2763
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
2764
+ import { extname, isAbsolute, resolve as resolve2, sep } from "path";
2765
+ var MAX_IMAGES = 9;
2766
+ var MAX_BYTES = 10 * 1024 * 1024;
2767
+ var DOWNLOAD_TIMEOUT_MS = 1e4;
2768
+ var ALLOWED_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "tif", "tiff", "bmp", "ico"]);
2769
+ var cache = /* @__PURE__ */ new Map();
2770
+ var IMG_RE2 = /!\[([^\]]*)\]\(\s*(<[^>]+>|[^)\s]+)(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
2771
+ function cleanSrc2(raw) {
2772
+ let s = raw.trim();
2773
+ if (s.startsWith("<") && s.endsWith(">")) s = s.slice(1, -1).trim();
2774
+ return s;
2775
+ }
2776
+ function imageSources(text) {
2777
+ const out = [];
2778
+ const seen = /* @__PURE__ */ new Set();
2779
+ const re = new RegExp(IMG_RE2.source, "g");
2780
+ let m;
2781
+ while ((m = re.exec(text)) !== null) {
2782
+ const src = cleanSrc2(m[2] ?? "");
2783
+ if (src && !seen.has(src)) {
2784
+ seen.add(src);
2785
+ out.push(src);
2786
+ }
2787
+ }
2788
+ return out;
2789
+ }
2790
+ async function uploadOutboundImages(channel, sources, cwd) {
2791
+ const picked = sources.slice(0, MAX_IMAGES);
2792
+ if (sources.length > picked.length) {
2793
+ log.warn("outbound", "image-cap", { skipped: sources.length - picked.length });
2794
+ }
2795
+ const results = await Promise.all(
2796
+ picked.map(async (src) => {
2797
+ try {
2798
+ return [src, await resolveAndUpload(channel, src, cwd)];
2799
+ } catch (err) {
2800
+ log.warn("outbound", "image-failed", { src: src.slice(0, 80), err: String(err) });
2801
+ return [src, void 0];
2802
+ }
2803
+ })
2804
+ );
2805
+ const out = /* @__PURE__ */ new Map();
2806
+ for (const [src, key] of results) if (key) out.set(src, key);
2807
+ if (out.size > 0) log.info("outbound", "images", { want: sources.length, uploaded: out.size });
2808
+ return out;
2809
+ }
2810
+ async function resolveAndUpload(channel, src, cwd) {
2811
+ const { buffer, cacheKey } = await loadSource(src, cwd);
2812
+ if (!buffer) return void 0;
2813
+ const hit = cache.get(cacheKey);
2814
+ if (hit) return hit;
2815
+ const key = await uploadBuffer(channel, buffer);
2816
+ if (key) cache.set(cacheKey, key);
2817
+ return key;
2818
+ }
2819
+ async function loadSource(src, cwd) {
2820
+ if (/^https?:\/\//i.test(src)) return loadRemote(src);
2821
+ return loadLocal(src, cwd);
2822
+ }
2823
+ async function loadLocal(src, cwd) {
2824
+ const cwdAbs = resolve2(cwd);
2825
+ const abs = isAbsolute(src) ? resolve2(src) : resolve2(cwdAbs, src);
2826
+ if (abs !== cwdAbs && !abs.startsWith(cwdAbs + sep)) {
2827
+ log.warn("outbound", "image-outside-cwd", { src: src.slice(0, 80) });
2828
+ return { cacheKey: `local:${abs}` };
2829
+ }
2830
+ const ext = extname(abs).slice(1).toLowerCase();
2831
+ if (!ALLOWED_EXT.has(ext)) {
2832
+ log.warn("outbound", "image-ext", { ext, src: src.slice(0, 80) });
2833
+ return { cacheKey: `local:${abs}` };
2834
+ }
2835
+ let size;
2836
+ let mtimeMs;
2837
+ try {
2838
+ const st = await stat2(abs);
2839
+ if (!st.isFile()) throw new Error("not a file");
2840
+ size = st.size;
2841
+ mtimeMs = st.mtimeMs;
2842
+ } catch {
2843
+ log.warn("outbound", "image-missing", { src: src.slice(0, 80) });
2844
+ return { cacheKey: `local:${abs}` };
2845
+ }
2846
+ if (size === 0 || size > MAX_BYTES) {
2847
+ log.warn("outbound", "image-size", { size, src: src.slice(0, 80) });
2848
+ return { cacheKey: `local:${abs}:${size}` };
2849
+ }
2850
+ const buffer = await readFile5(abs);
2851
+ return { buffer, cacheKey: `local:${abs}:${mtimeMs}:${size}` };
2852
+ }
2853
+ async function loadRemote(url) {
2854
+ const cacheKey = `url:${url}`;
2855
+ const ctrl = new AbortController();
2856
+ const timer = setTimeout(() => ctrl.abort(), DOWNLOAD_TIMEOUT_MS);
2857
+ try {
2858
+ const res = await fetch(url, { signal: ctrl.signal, redirect: "follow" });
2859
+ if (!res.ok) {
2860
+ log.warn("outbound", "image-http", { url: url.slice(0, 80), status: res.status });
2861
+ return { cacheKey };
2862
+ }
2863
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
2864
+ if (ct && !ct.startsWith("image/")) {
2865
+ log.warn("outbound", "image-ctype", { ct, url: url.slice(0, 80) });
2866
+ return { cacheKey };
2867
+ }
2868
+ const declared = Number(res.headers.get("content-length") ?? 0);
2869
+ if (declared > MAX_BYTES) {
2870
+ log.warn("outbound", "image-size", { declared, url: url.slice(0, 80) });
2871
+ return { cacheKey };
2872
+ }
2873
+ const ab = await res.arrayBuffer();
2874
+ if (ab.byteLength === 0 || ab.byteLength > MAX_BYTES) {
2875
+ log.warn("outbound", "image-size", { size: ab.byteLength, url: url.slice(0, 80) });
2876
+ return { cacheKey };
2877
+ }
2878
+ return { buffer: Buffer.from(ab), cacheKey };
2879
+ } catch (err) {
2880
+ log.warn("outbound", "image-fetch", { url: url.slice(0, 80), err: String(err) });
2881
+ return { cacheKey };
2882
+ } finally {
2883
+ clearTimeout(timer);
2884
+ }
2885
+ }
2886
+ async function uploadBuffer(channel, buffer) {
2887
+ const res = await channel.rawClient.im.v1.image.create({
2888
+ data: { image_type: "message", image: buffer }
2889
+ });
2890
+ const key = res?.image_key ?? res?.data?.image_key;
2891
+ if (!key) {
2892
+ log.warn("outbound", "image-no-key", { res: JSON.stringify(res).slice(0, 120) });
2893
+ return void 0;
2894
+ }
2895
+ return key;
2896
+ }
2897
+
2627
2898
  // src/card/dm-cards.ts
2628
2899
  function openChatUrl(chatId) {
2629
2900
  return `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
@@ -2780,8 +3051,9 @@ function scopeDiagnosis(i) {
2780
3051
  return [md("- \u98DE\u4E66\u6743\u9650\uFF1A\u2705 \u5FC5\u9700\u6743\u9650\u5DF2\u5168\u90E8\u5F00\u901A")];
2781
3052
  }
2782
3053
  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")}`),
3054
+ 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 / \u56FE\u7247 / \u5EFA\u7FA4\u7B49\uFF09\u4E0D\u53EF\u7528`),
3055
+ note(`\u5F85\u5F00\u901A\uFF1A
3056
+ ${i.missingScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
2785
3057
  actions([linkButton("\u{1F511} \u4E00\u952E\u53BB\u5F00\u901A\u8FD9\u4E9B\u6743\u9650", i.scopeGrantUrl)])
2786
3058
  ];
2787
3059
  }
@@ -2977,7 +3249,7 @@ function buildGroupSettingsCard(project) {
2977
3249
  // src/service/update.ts
2978
3250
  import { execFile, spawn as spawn5 } from "child_process";
2979
3251
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
2980
- import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
3252
+ import { dirname as dirname6, join as join8, resolve as resolve4 } from "path";
2981
3253
  import { fileURLToPath as fileURLToPath3 } from "url";
2982
3254
  import { promisify } from "util";
2983
3255
 
@@ -2986,7 +3258,7 @@ import { spawn as spawn4, spawnSync } from "child_process";
2986
3258
  import { existsSync as existsSync4 } from "fs";
2987
3259
  import { appendFile, mkdir as mkdir4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
2988
3260
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
2989
- import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
3261
+ import { dirname as dirname5, join as join7, resolve as resolve3 } from "path";
2990
3262
  import { fileURLToPath as fileURLToPath2 } from "url";
2991
3263
  var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
2992
3264
  function launchAgentPlistPath() {
@@ -3000,7 +3272,7 @@ function serviceStderrPath() {
3000
3272
  }
3001
3273
  function resolveCliBinPath() {
3002
3274
  const distDir = dirname5(fileURLToPath2(import.meta.url));
3003
- return resolve2(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3275
+ return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3004
3276
  }
3005
3277
  function escapeXml(value) {
3006
3278
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -3166,7 +3438,7 @@ function getServiceAdapter() {
3166
3438
  var execFileP = promisify(execFile);
3167
3439
  var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3168
3440
  function pkgRoot() {
3169
- return resolve3(dirname6(fileURLToPath3(import.meta.url)), "..");
3441
+ return resolve4(dirname6(fileURLToPath3(import.meta.url)), "..");
3170
3442
  }
3171
3443
  function pkgJson() {
3172
3444
  try {
@@ -3232,12 +3504,12 @@ async function restartDaemon() {
3232
3504
  }
3233
3505
 
3234
3506
  // src/project/registry.ts
3235
- import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3507
+ import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3236
3508
  import { dirname as dirname7 } from "path";
3237
3509
  var FILE_VERSION2 = 1;
3238
3510
  async function read() {
3239
3511
  try {
3240
- const text = await readFile5(paths.projectsFile, "utf8");
3512
+ const text = await readFile6(paths.projectsFile, "utf8");
3241
3513
  const parsed = JSON.parse(text);
3242
3514
  return Array.isArray(parsed.projects) ? parsed.projects : [];
3243
3515
  } catch (err) {
@@ -3292,7 +3564,7 @@ async function removeProject(name) {
3292
3564
  // src/project/lifecycle.ts
3293
3565
  import { mkdir as mkdir6 } from "fs/promises";
3294
3566
  import { existsSync as existsSync6 } from "fs";
3295
- import { isAbsolute, join as join9, resolve as resolve4 } from "path";
3567
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
3296
3568
 
3297
3569
  // src/project/git-info.ts
3298
3570
  import { execFile as execFile2 } from "child_process";
@@ -3430,7 +3702,7 @@ async function createProject(channel, input2) {
3430
3702
  let cwd;
3431
3703
  let blank;
3432
3704
  if (input2.existingPath) {
3433
- cwd = isAbsolute(input2.existingPath) ? input2.existingPath : resolve4(input2.existingPath);
3705
+ cwd = isAbsolute2(input2.existingPath) ? input2.existingPath : resolve5(input2.existingPath);
3434
3706
  if (!existsSync6(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
3435
3707
  blank = false;
3436
3708
  } else {
@@ -3468,12 +3740,12 @@ async function transferOwnership(channel, chatId, toOpenId) {
3468
3740
  }
3469
3741
 
3470
3742
  // src/bot/session-store.ts
3471
- import { mkdir as mkdir7, readFile as readFile6, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3743
+ import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3472
3744
  import { dirname as dirname8 } from "path";
3473
3745
  var FILE_VERSION3 = 1;
3474
3746
  async function read2() {
3475
3747
  try {
3476
- const text = await readFile6(paths.sessionsFile, "utf8");
3748
+ const text = await readFile7(paths.sessionsFile, "utf8");
3477
3749
  const parsed = JSON.parse(text);
3478
3750
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
3479
3751
  } catch (err) {
@@ -3529,9 +3801,9 @@ async function handleDmConsole(channel, cfg, msg) {
3529
3801
  }
3530
3802
 
3531
3803
  // src/bot/media.ts
3532
- import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat2 } from "fs/promises";
3804
+ import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat3 } from "fs/promises";
3533
3805
  import { join as join10 } from "path";
3534
- var MAX_IMAGES = 9;
3806
+ var MAX_IMAGES2 = 9;
3535
3807
  var MEDIA_TTL_MS = 60 * 6e4;
3536
3808
  var EXT_BY_CONTENT_TYPE = {
3537
3809
  "image/png": "png",
@@ -3564,7 +3836,7 @@ async function collectInboundImages(channel, msg) {
3564
3836
  }
3565
3837
  const out = [];
3566
3838
  let index = 0;
3567
- for (const ref of refs.slice(0, MAX_IMAGES)) {
3839
+ for (const ref of refs.slice(0, MAX_IMAGES2)) {
3568
3840
  const path = await downloadOne(channel, ref, index++);
3569
3841
  if (path) out.push(path);
3570
3842
  }
@@ -3671,7 +3943,7 @@ async function pruneOldMedia() {
3671
3943
  for (const name of entries) {
3672
3944
  const file = join10(paths.mediaDir, name);
3673
3945
  try {
3674
- const st = await stat2(file);
3946
+ const st = await stat3(file);
3675
3947
  if (st.mtimeMs < cutoff) await rm3(file, { force: true });
3676
3948
  } catch {
3677
3949
  }
@@ -4686,9 +4958,22 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4686
4958
  const finalMsgId = cardMsgId;
4687
4959
  await adoptThreadId(finalMsgId);
4688
4960
  rc.cardKey = finalMsgId;
4961
+ const answerText = finalMessageText(rc.rs);
4962
+ const { fences } = extractCardFences(answerText);
4963
+ const imgSources = imageSources(answerText);
4964
+ if (imgSources.length > 0) {
4965
+ rc.images = await uploadOutboundImages(channel, imgSources, opts.cwd ?? fallbackCwd);
4966
+ }
4689
4967
  await stream2.updateCard(channel, buildRunCard(rc));
4690
4968
  runsByCard.delete(cardMsgId);
4691
4969
  promoteCard(finalMsgId, rc);
4970
+ for (const fence of fences) {
4971
+ try {
4972
+ await sendManagedCard(channel, opts.chatId, buildCleanCard(fence, rc.images), finalMsgId, !opts.flat);
4973
+ } catch (err) {
4974
+ log.fail("card", err, { phase: "clean-card" });
4975
+ }
4976
+ }
4692
4977
  if (topicThreadId) await patchSession(topicThreadId, { updatedAt: Date.now() });
4693
4978
  replyTo = finalMsgId;
4694
4979
  replyInThread = !opts.flat;
@@ -5171,15 +5456,15 @@ async function secretsRemove(id) {
5171
5456
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
5172
5457
  }
5173
5458
  function readStdin() {
5174
- return new Promise((resolve5) => {
5459
+ return new Promise((resolve6) => {
5175
5460
  let data = "";
5176
5461
  if (process.stdin.isTTY) {
5177
- resolve5("");
5462
+ resolve6("");
5178
5463
  return;
5179
5464
  }
5180
5465
  process.stdin.setEncoding("utf8");
5181
5466
  process.stdin.on("data", (c) => data += c);
5182
- process.stdin.on("end", () => resolve5(data));
5467
+ process.stdin.on("end", () => resolve6(data));
5183
5468
  });
5184
5469
  }
5185
5470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {