@modelzen/feishu-codex-bridge 0.3.8 → 0.3.9

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 +453 -113
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -600,9 +600,9 @@ async function spawnExecProvider(pc, ref) {
600
600
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
601
601
  return new Promise((resolve7, reject) => {
602
602
  const env = {};
603
- if (pc.passEnv) for (const k of pc.passEnv) {
604
- const v = process.env[k];
605
- if (v) env[k] = v;
603
+ if (pc.passEnv) for (const k2 of pc.passEnv) {
604
+ const v = process.env[k2];
605
+ if (v) env[k2] = v;
606
606
  }
607
607
  if (pc.env) Object.assign(env, pc.env);
608
608
  const child = spawnProcess(pc.command, pc.args ?? [], {
@@ -948,11 +948,11 @@ function emit(level, phase, event, fields = {}) {
948
948
  event,
949
949
  ...ctx
950
950
  };
951
- for (const [k, v] of Object.entries(fields)) {
952
- if (RESERVED_KEYS.has(k)) {
953
- entry[`_${k}`] = v;
951
+ for (const [k2, v] of Object.entries(fields)) {
952
+ if (RESERVED_KEYS.has(k2)) {
953
+ entry[`_${k2}`] = v;
954
954
  } else {
955
- entry[k] = v;
955
+ entry[k2] = v;
956
956
  }
957
957
  }
958
958
  const s = getStream();
@@ -1003,20 +1003,20 @@ function formatFields(fields) {
1003
1003
  const keys = Object.keys(fields);
1004
1004
  if (keys.length === 0) return "";
1005
1005
  const parts = [];
1006
- for (const k of keys) {
1007
- const v = fields[k];
1006
+ for (const k2 of keys) {
1007
+ const v = fields[k2];
1008
1008
  if (v === void 0 || v === null) continue;
1009
- if (k === "stack") continue;
1009
+ if (k2 === "stack") continue;
1010
1010
  if (typeof v === "string") {
1011
- parts.push(`${k}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
1011
+ parts.push(`${k2}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
1012
1012
  } else if (typeof v === "number" || typeof v === "boolean") {
1013
- parts.push(`${k}=${v}`);
1013
+ parts.push(`${k2}=${v}`);
1014
1014
  } else {
1015
1015
  try {
1016
1016
  const str = JSON.stringify(v);
1017
- parts.push(`${k}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
1017
+ parts.push(`${k2}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
1018
1018
  } catch {
1019
- parts.push(`${k}=?`);
1019
+ parts.push(`${k2}=?`);
1020
1020
  }
1021
1021
  }
1022
1022
  }
@@ -1380,6 +1380,14 @@ function mapNotification(n) {
1380
1380
  return mapItemStart(n.params.item);
1381
1381
  case "item/completed":
1382
1382
  return mapItemComplete(n.params.item);
1383
+ case "thread/tokenUsage/updated":
1384
+ return {
1385
+ type: "context_usage",
1386
+ usedTokens: n.params.tokenUsage.last.totalTokens,
1387
+ contextWindow: n.params.tokenUsage.modelContextWindow
1388
+ };
1389
+ case "thread/compacted":
1390
+ return { type: "context_compacted" };
1383
1391
  case "turn/completed":
1384
1392
  return { type: "done", turnId: n.params.turn.id };
1385
1393
  case "error":
@@ -1430,6 +1438,12 @@ function mapItemComplete(item) {
1430
1438
 
1431
1439
  // src/agent/codex-appserver/backend.ts
1432
1440
  var APPROVAL_POLICY = "never";
1441
+ var AUTO_COMPACT_OFF_LIMIT = 1e9;
1442
+ function withAutoCompact(params, autoCompact) {
1443
+ if (autoCompact !== false) return params;
1444
+ const config = params.config ?? {};
1445
+ return { ...params, config: { ...config, model_auto_compact_token_limit: AUTO_COMPACT_OFF_LIMIT } };
1446
+ }
1433
1447
  function sandboxParams(mode, network) {
1434
1448
  if ((mode ?? "full") === "full") return { sandbox: "danger-full-access" };
1435
1449
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -1467,6 +1481,7 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1467
1481
  "\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"
1468
1482
  ].join("\n");
1469
1483
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1484
+ var COMPACT_TIMEOUT_MS = 12e4;
1470
1485
  function withDeadline(p, ms, label) {
1471
1486
  return new Promise((resolve7, reject) => {
1472
1487
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
@@ -1548,6 +1563,40 @@ var CodexThread = class {
1548
1563
  async abort(turnId) {
1549
1564
  await this.client.request("turn/interrupt", { threadId: this.codexThreadId, turnId });
1550
1565
  }
1566
+ async compact() {
1567
+ let startError;
1568
+ const startFailed = new Promise((resolve7) => {
1569
+ this.client.request("thread/compact/start", { threadId: this.codexThreadId }).then(void 0, (err) => {
1570
+ startError = err instanceof Error ? err : new Error(String(err));
1571
+ log.fail("agent", startError, { phase: "thread/compact/start" });
1572
+ resolve7("start-failed");
1573
+ });
1574
+ });
1575
+ let timer;
1576
+ const timeout = new Promise((resolve7) => {
1577
+ timer = setTimeout(() => resolve7("timeout"), COMPACT_TIMEOUT_MS);
1578
+ });
1579
+ const stream2 = this.client.stream()[Symbol.asyncIterator]();
1580
+ let compacted = false;
1581
+ let usage = null;
1582
+ try {
1583
+ while (true) {
1584
+ const step = await Promise.race([stream2.next(), startFailed, timeout]);
1585
+ if (step === "start-failed") throw startError ?? new Error("thread/compact/start \u8BF7\u6C42\u5931\u8D25");
1586
+ if (step === "timeout") throw new Error(`\u538B\u7F29\u8D85\u65F6\uFF08codex \u672A\u5728 ${COMPACT_TIMEOUT_MS / 1e3}s \u5185\u5B8C\u6210\uFF09`);
1587
+ if (step.done) break;
1588
+ const ev = mapNotification(step.value);
1589
+ if (!ev) continue;
1590
+ if (ev.type === "context_usage") usage = { usedTokens: ev.usedTokens, contextWindow: ev.contextWindow };
1591
+ else if (ev.type === "context_compacted") compacted = true;
1592
+ else if (ev.type === "error" && !ev.willRetry) throw new Error(ev.message);
1593
+ else if (ev.type === "done") break;
1594
+ }
1595
+ } finally {
1596
+ if (timer) clearTimeout(timer);
1597
+ }
1598
+ return { compacted, usage };
1599
+ }
1551
1600
  async close() {
1552
1601
  await this.client.close();
1553
1602
  }
@@ -1636,7 +1685,7 @@ var CodexAppServerBackend = class {
1636
1685
  }
1637
1686
  }
1638
1687
  async startThread(opts) {
1639
- const sandbox = sandboxParams(opts.mode, opts.network);
1688
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1640
1689
  const client = await this.spawn(opts.cwd);
1641
1690
  const res = await client.request("thread/start", {
1642
1691
  cwd: opts.cwd,
@@ -1648,7 +1697,7 @@ var CodexAppServerBackend = class {
1648
1697
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1649
1698
  }
1650
1699
  async resumeThread(opts) {
1651
- const sandbox = sandboxParams(opts.mode, opts.network);
1700
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1652
1701
  const client = await this.spawn(opts.cwd);
1653
1702
  const res = await client.request("thread/resume", {
1654
1703
  threadId: opts.codexThreadId,
@@ -1821,39 +1870,53 @@ function stampRenderToken(card2) {
1821
1870
  }
1822
1871
  }
1823
1872
  }
1824
- for (const k of Object.keys(obj)) visit(obj[k]);
1873
+ for (const k2 of Object.keys(obj)) visit(obj[k2]);
1825
1874
  };
1826
1875
  visit(card2);
1827
1876
  }
1877
+ function isCardIdNotReady(err) {
1878
+ const data = err?.response?.data;
1879
+ return data?.code === 230099 || /11310|cardid is invalid/i.test(data?.msg ?? "");
1880
+ }
1828
1881
  async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1829
1882
  stampRenderToken(card2);
1830
- const created = await channel.rawClient.cardkit.v1.card.create({
1831
- data: { type: "card_json", data: JSON.stringify(card2) }
1832
- });
1833
- const cardId = created.data?.card_id;
1834
- if (!cardId) {
1835
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1836
- }
1837
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1838
- let messageId;
1839
- if (replyTo) {
1840
- const sent = await channel.rawClient.im.v1.message.reply({
1841
- path: { message_id: replyTo },
1842
- data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1843
- });
1844
- messageId = sent.data?.message_id;
1845
- } else {
1846
- const sent = await channel.rawClient.im.v1.message.create({
1847
- params: { receive_id_type: receiveIdType },
1848
- data: { receive_id: to, msg_type: "interactive", content }
1849
- });
1850
- messageId = sent.data?.message_id;
1851
- }
1852
- if (!messageId) {
1853
- throw new Error("send card-by-reference returned no message_id");
1883
+ const data = JSON.stringify(card2);
1884
+ const attempt = async () => {
1885
+ const created = await channel.rawClient.cardkit.v1.card.create({ data: { type: "card_json", data } });
1886
+ const cardId = created.data?.card_id;
1887
+ if (!cardId) {
1888
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1889
+ }
1890
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1891
+ let messageId;
1892
+ if (replyTo) {
1893
+ const sent = await channel.rawClient.im.v1.message.reply({
1894
+ path: { message_id: replyTo },
1895
+ data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1896
+ });
1897
+ messageId = sent.data?.message_id;
1898
+ } else {
1899
+ const sent = await channel.rawClient.im.v1.message.create({
1900
+ params: { receive_id_type: receiveIdType },
1901
+ data: { receive_id: to, msg_type: "interactive", content }
1902
+ });
1903
+ messageId = sent.data?.message_id;
1904
+ }
1905
+ if (!messageId) {
1906
+ throw new Error("send card-by-reference returned no message_id");
1907
+ }
1908
+ byMessageId.set(messageId, { cardId, sequence: 0 });
1909
+ return { messageId, cardId };
1910
+ };
1911
+ for (let i = 0; ; i++) {
1912
+ try {
1913
+ return await attempt();
1914
+ } catch (err) {
1915
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
1916
+ log.fail("card", err, { phase: "managed-send", attempt: i, retry: true });
1917
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
1918
+ }
1854
1919
  }
1855
- byMessageId.set(messageId, { cardId, sequence: 0 });
1856
- return { messageId, cardId };
1857
1920
  }
1858
1921
  async function updateManagedCard(channel, messageId, card2) {
1859
1922
  const entry = byMessageId.get(messageId);
@@ -1983,6 +2046,10 @@ function reduce(state, evt) {
1983
2046
  });
1984
2047
  return { ...state, blocks };
1985
2048
  }
2049
+ case "context_usage":
2050
+ return { ...state, usage: { used: evt.usedTokens, window: evt.contextWindow } };
2051
+ // context_compacted is surfaced as a standalone notice by the run loop, not
2052
+ // folded into the card — fall through to the no-op default.
1986
2053
  case "error":
1987
2054
  return { ...state, terminal: "error", errorMsg: evt.message, footer: null };
1988
2055
  case "done":
@@ -2102,6 +2169,9 @@ function image(imgKey, alt = "") {
2102
2169
  function note(content) {
2103
2170
  return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
2104
2171
  }
2172
+ function colorNote(content, color) {
2173
+ return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: color } };
2174
+ }
2105
2175
  function hr() {
2106
2176
  return { tag: "hr" };
2107
2177
  }
@@ -2320,7 +2390,12 @@ function talkLine(noMention, tail) {
2320
2390
  function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2321
2391
  const elements = [];
2322
2392
  if (scope === "single") {
2323
- const lines = [talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"), "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"];
2393
+ const lines = [
2394
+ talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"),
2395
+ "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6",
2396
+ "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4",
2397
+ "\xB7 `/compact` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09"
2398
+ ];
2324
2399
  if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2325
2400
  lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2326
2401
  elements.push(md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"), hr(), md(lines.join("\n")));
@@ -2331,6 +2406,8 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2331
2406
  md(
2332
2407
  `${talkLine(noMention, "\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD")}
2333
2408
  \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2409
+ \xB7 \`/context\` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4
2410
+ \xB7 \`/compact\` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09
2334
2411
  \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2335
2412
  ),
2336
2413
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
@@ -2365,7 +2442,9 @@ function buildWelcomeCard(kind, docUrl, noMention = true) {
2365
2442
  "\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB\uFF08\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF09\n\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09"
2366
2443
  ),
2367
2444
  md("\u{1F9F5} **\u8BDD\u9898\u5185**"),
2368
- md("\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"),
2445
+ md(
2446
+ "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/context` \xB7 `/compact` \u2192 \u770B / \u538B\u7F29\u4E0A\u4E0B\u6587"
2447
+ ),
2369
2448
  note("\u4EFB\u610F\u573A\u666F\u53D1 `/help` \u770B\u5F53\u524D\u53EF\u7528\u547D\u4EE4\u3002")
2370
2449
  );
2371
2450
  }
@@ -2635,7 +2714,80 @@ function escapeInline2(s) {
2635
2714
  return s.replace(/\s+/g, " ").trim();
2636
2715
  }
2637
2716
 
2717
+ // src/card/context-gauge.ts
2718
+ var CTX_WARN = 0.7;
2719
+ var CTX_HIGH = 0.85;
2720
+ var CTX_CRIT = 0.95;
2721
+ function ctxTier(frac) {
2722
+ if (frac >= CTX_CRIT) return { level: 3, color: "red", dot: "\u{1F534}", advice: "\u5F3A\u70C8\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2723
+ if (frac >= CTX_HIGH) return { level: 2, color: "orange", dot: "\u{1F7E0}", advice: "\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2724
+ if (frac >= CTX_WARN) return { level: 1, color: "yellow", dot: "\u{1F7E1}", advice: "\u53EF\u8003\u8651 `/compact` \u538B\u7F29" };
2725
+ return { level: 0, color: "green", dot: "\u{1F7E2}", advice: "" };
2726
+ }
2727
+ function ctxPercent(used, window) {
2728
+ if (!window || window <= 0) return null;
2729
+ return Math.max(0, Math.min(100, Math.round(used / window * 100)));
2730
+ }
2731
+ function k(n) {
2732
+ return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(Math.max(0, Math.round(n)));
2733
+ }
2734
+ function runCardGauge(used, window) {
2735
+ const pct = ctxPercent(used, window);
2736
+ if (pct === null || !window) return null;
2737
+ const frac = used / window;
2738
+ if (frac < CTX_WARN) return null;
2739
+ const t = ctxTier(frac);
2740
+ return colorNote(`${t.dot} \u4E0A\u4E0B\u6587 ${pct}% \xB7 ${k(used)}/${k(window)} \xB7 ${t.advice}`, t.color);
2741
+ }
2742
+ function buildContextCard(used, window) {
2743
+ const pct = ctxPercent(used, window);
2744
+ if (pct === null) {
2745
+ const line = used > 0 ? `\u{1F9E0} \u5DF2\u7528 ${k(used)} tokens\uFF08\u4E0A\u4E0B\u6587\u7A97\u53E3\u672A\u77E5\uFF09` : "\u{1F9E0} \u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\uFF0C\u8DD1\u4E00\u8F6E\u5BF9\u8BDD\u540E\u518D\u770B `/context`\u3002";
2746
+ return card([note(line)], { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2747
+ }
2748
+ const t = ctxTier(used / window);
2749
+ const els = [colorNote(`${t.dot} **\u4E0A\u4E0B\u6587 ${pct}%** \xB7 ${k(used)}/${k(window)} tokens`, t.color)];
2750
+ els.push(note(t.level >= 1 ? `${t.advice}\uFF1A\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\u3002` : "\u7A7A\u95F4\u5145\u8DB3\uFF0C\u65E0\u9700\u538B\u7F29\u3002"));
2751
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2752
+ }
2753
+ var COMPACT_SPINNER = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
2754
+ function buildCompactingCard(tick = 0) {
2755
+ const spin = COMPACT_SPINNER[(tick % COMPACT_SPINNER.length + COMPACT_SPINNER.length) % COMPACT_SPINNER.length];
2756
+ return card([colorNote(`\u{1F5DC}\uFE0F \u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587 ${spin}`, "blue"), note("\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF0C\u8BF7\u7A0D\u5019\u3002")], {
2757
+ summary: "\u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587"
2758
+ });
2759
+ }
2760
+ function buildCompactedCard(usage, before) {
2761
+ const els = [colorNote("\u2705 \u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210", "green")];
2762
+ const pct = usage ? ctxPercent(usage.usedTokens, usage.contextWindow) : null;
2763
+ const dropped = usage != null && before != null && usage.usedTokens < before.used;
2764
+ if (usage && pct !== null && usage.contextWindow && (dropped || before == null)) {
2765
+ const beforePct = before ? ctxPercent(before.used, before.window) : null;
2766
+ const from = dropped && beforePct !== null ? `${beforePct}% \u2192 ` : "";
2767
+ els.push(note(`\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\uFF0C\u73B0\u5DF2\u7528 ${from}${pct}%\uFF08${k(usage.usedTokens)}/${k(usage.contextWindow)} tokens\uFF09\u3002`));
2768
+ } else {
2769
+ els.push(note("\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\u3001\u817E\u51FA\u7A7A\u95F4\u7EE7\u7EED\uFF1B\u53D1\u4E0B\u4E00\u6761\u6D88\u606F\u540E\uFF0C`/context` \u5373\u53EF\u770B\u5230\u5360\u7528\u4E0B\u964D\u3002"));
2770
+ }
2771
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210" });
2772
+ }
2773
+ function buildCompactFailedCard(message) {
2774
+ return card([colorNote(`\u26A0\uFE0F \u538B\u7F29\u5931\u8D25\uFF1A${message}`, "red")], { summary: "\u538B\u7F29\u5931\u8D25" });
2775
+ }
2776
+ function buildAutoCompactCard() {
2777
+ return card(
2778
+ [
2779
+ hr(),
2780
+ colorNote("\u{1F5DC}\uFE0F \u2500\u2500\u2500 \u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29 \u2500\u2500\u2500", "blue"),
2781
+ note("\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\u3001\u817E\u51FA\u7A7A\u95F4\u7EE7\u7EED\uFF1B\u6700\u8FD1\u7684\u4E0A\u4E0B\u6587\u4FDD\u7559\u3002")
2782
+ ],
2783
+ { summary: "\u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29" }
2784
+ );
2785
+ }
2786
+
2638
2787
  // src/card/run-card.ts
2788
+ function gaugeEl(state) {
2789
+ return state.usage ? runCardGauge(state.usage.used, state.usage.window) : null;
2790
+ }
2639
2791
  var RC = {
2640
2792
  stop: "run.stop"
2641
2793
  };
@@ -2668,6 +2820,8 @@ function renderRunning(state, rc) {
2668
2820
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
2669
2821
  if (state.footer) elements.push(footerStatus(state.footer));
2670
2822
  if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2823
+ const gauge = gaugeEl(state);
2824
+ if (gauge) elements.push(gauge);
2671
2825
  return elements;
2672
2826
  }
2673
2827
  function renderTerminal(state, rc) {
@@ -2701,6 +2855,8 @@ function renderTerminal(state, rc) {
2701
2855
  } else if (state.terminal === "done" && !answer) {
2702
2856
  elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2703
2857
  }
2858
+ const gauge = gaugeEl(state);
2859
+ if (gauge) elements.push(gauge);
2704
2860
  return elements;
2705
2861
  }
2706
2862
  function lastTextIndex(blocks) {
@@ -2914,35 +3070,55 @@ var RunCardStream = class {
2914
3070
  }
2915
3071
  }
2916
3072
  /** Create the entity from the initial (running) card and send a message
2917
- * referencing it by card_id. Returns the carrier message id. */
3073
+ * referencing it by card_id. Returns the carrier message id.
3074
+ *
3075
+ * A just-created CardKit entity occasionally hasn't propagated when the message
3076
+ * referencing it is sent — Feishu 400s with 230099 / ErrCode 11310 "cardid is
3077
+ * invalid" and the run card silently fails to appear (this surfaced as
3078
+ * intermittent intake.fail). Same transient, same fix as
3079
+ * {@link ../card/managed#sendManagedCard}: retry the whole create+send with a
3080
+ * short backoff. Only this transient retries — Feishu rejected the message
3081
+ * outright (nothing sent), and a re-created entity that's never referenced is a
3082
+ * harmless orphan, so no duplicate card. */
2918
3083
  async create(channel, chatId, initialCard, opts) {
2919
- const created = await channel.rawClient.cardkit.v1.card.create({
2920
- data: { type: "card_json", data: JSON.stringify(initialCard) }
2921
- });
2922
- const cardId = created.data?.card_id;
2923
- if (!cardId) {
2924
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
2925
- }
2926
- this.cardId = cardId;
2927
- this.lastContent = JSON.stringify(initialCard);
2928
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
2929
- let messageId;
2930
- if (opts.replyTo) {
2931
- const r = await channel.rawClient.im.v1.message.reply({
2932
- path: { message_id: opts.replyTo },
2933
- data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
2934
- });
2935
- messageId = r.data?.message_id;
2936
- } else {
2937
- const r = await channel.rawClient.im.v1.message.create({
2938
- params: { receive_id_type: "chat_id" },
2939
- data: { receive_id: chatId, msg_type: "interactive", content }
3084
+ const attempt = async () => {
3085
+ const created = await channel.rawClient.cardkit.v1.card.create({
3086
+ data: { type: "card_json", data: JSON.stringify(initialCard) }
2940
3087
  });
2941
- messageId = r.data?.message_id;
3088
+ const cardId = created.data?.card_id;
3089
+ if (!cardId) {
3090
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
3091
+ }
3092
+ this.cardId = cardId;
3093
+ this.lastContent = JSON.stringify(initialCard);
3094
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
3095
+ let messageId;
3096
+ if (opts.replyTo) {
3097
+ const r = await channel.rawClient.im.v1.message.reply({
3098
+ path: { message_id: opts.replyTo },
3099
+ data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
3100
+ });
3101
+ messageId = r.data?.message_id;
3102
+ } else {
3103
+ const r = await channel.rawClient.im.v1.message.create({
3104
+ params: { receive_id_type: "chat_id" },
3105
+ data: { receive_id: chatId, msg_type: "interactive", content }
3106
+ });
3107
+ messageId = r.data?.message_id;
3108
+ }
3109
+ if (!messageId) throw new Error("run card send returned no message_id");
3110
+ this._messageId = messageId;
3111
+ return messageId;
3112
+ };
3113
+ for (let i = 0; ; i++) {
3114
+ try {
3115
+ return await attempt();
3116
+ } catch (err) {
3117
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
3118
+ log.fail("card", err, { phase: "run-stream-create", attempt: i, retry: true });
3119
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
3120
+ }
2942
3121
  }
2943
- if (!messageId) throw new Error("run card send returned no message_id");
2944
- this._messageId = messageId;
2945
- return messageId;
2946
3122
  }
2947
3123
  /** Throttled whole-card stream update. Skips identical/too-soon pushes;
2948
3124
  * `force` flushes regardless (still de-duped on content). */
@@ -3226,8 +3402,8 @@ async function updateProject(name, patch) {
3226
3402
  if (!p) return;
3227
3403
  const actual = typeof patch === "function" ? patch(p) : patch;
3228
3404
  const target = p;
3229
- for (const [k, v] of Object.entries(actual)) {
3230
- if (v !== void 0) target[k] = v;
3405
+ for (const [k2, v] of Object.entries(actual)) {
3406
+ if (v !== void 0) target[k2] = v;
3231
3407
  }
3232
3408
  await write(projects);
3233
3409
  });
@@ -3286,13 +3462,18 @@ var DM = {
3286
3462
  rmAllowed: "dm.allow.rm",
3287
3463
  // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3288
3464
  projectSettings: "dm.projectSettings",
3465
+ // 🧵 话题钻取:项目总览的「🧵 N 话题」按钮 → 该项目话题列表卡
3466
+ projectTopics: "dm.projectTopics",
3289
3467
  setNoMentionDm: "dm.proj.noMention",
3468
+ // 🗜️ 自动压缩:项目级开关(同群设置里的那个,DM 里也能改),按钮携带项目名 n
3469
+ setAutoCompactDm: "dm.proj.autoCompact",
3290
3470
  // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3291
3471
  permission: "dm.proj.perm",
3292
3472
  permissionSubmit: "dm.proj.perm.submit"
3293
3473
  };
3294
3474
  var GS = {
3295
- setNoMention: "gs.noMention"
3475
+ setNoMention: "gs.noMention",
3476
+ setAutoCompact: "gs.autoCompact"
3296
3477
  };
3297
3478
  function kindLabel(kind) {
3298
3479
  return kind === "single" ? "\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4" : "\u{1F465} \u591A\u8BDD\u9898\u7FA4";
@@ -3565,6 +3746,7 @@ function buildNewProjectDoneCard(p) {
3565
3746
  );
3566
3747
  return card(elements, { header: { title, template: "green" } });
3567
3748
  }
3749
+ var PROJECT_TOPICS_MAX = 50;
3568
3750
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
3569
3751
  if (projects.length === 0) {
3570
3752
  return card(
@@ -3574,25 +3756,15 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3574
3756
  }
3575
3757
  const elements = [];
3576
3758
  for (const p of projects) {
3759
+ const topicCount = (p.chatId ? sessionsByChat.get(p.chatId) : void 0)?.length ?? 0;
3760
+ const dir = `\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`;
3761
+ const meta = p.chatId ? `${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4";
3577
3762
  elements.push(md(`**${p.name}**${p.blank ? " _(\u7A7A\u767D)_" : ""}`));
3578
- elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
3579
- elements.push(
3580
- note(
3581
- p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
3582
- )
3583
- );
3584
- const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
3585
- if (sessions.length === 0) {
3586
- elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3587
- } else {
3588
- const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3589
- for (const s of sorted) {
3590
- const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 40);
3591
- elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3592
- }
3593
- }
3763
+ elements.push(note(`${dir}
3764
+ ${meta}`));
3594
3765
  const row = [];
3595
3766
  if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
3767
+ row.push(button(`\u{1F9F5} ${topicCount} \u8BDD\u9898`, { a: DM.projectTopics, n: p.name }));
3596
3768
  row.push(button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: p.name }));
3597
3769
  row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
3598
3770
  elements.push(actions(row));
@@ -3602,6 +3774,26 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3602
3774
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
3603
3775
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
3604
3776
  }
3777
+ function buildProjectTopicsCard(project, sessions) {
3778
+ const elements = [md(`**${project.name}** \xB7 \u5171 ${sessions.length} \u4E2A\u8BDD\u9898`)];
3779
+ if (sessions.length === 0) {
3780
+ elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3781
+ } else {
3782
+ const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3783
+ for (const s of sorted.slice(0, PROJECT_TOPICS_MAX)) {
3784
+ const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 50);
3785
+ elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3786
+ }
3787
+ if (sorted.length > PROJECT_TOPICS_MAX) {
3788
+ elements.push(note(`\xB7 \u2026\u8FD8\u6709 ${sorted.length - PROJECT_TOPICS_MAX} \u4E2A\u8BDD\u9898\uFF08\u66F4\u65E9\u7684\u53EF\u5728\u7FA4\u91CC \`/resume\` \u6062\u590D\uFF09`));
3789
+ }
3790
+ }
3791
+ const nav = [];
3792
+ if (project.chatId) nav.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(project.chatId)));
3793
+ nav.push(button("\u2B05\uFE0F \u9879\u76EE\u5217\u8868", { a: DM.projects }));
3794
+ elements.push(hr(), actions(nav));
3795
+ return card(elements, { header: { title: `\u{1F9F5} \u8BDD\u9898 \xB7 ${project.name}`, template: "wathet" } });
3796
+ }
3605
3797
  function buildRmConfirmCard(name, origin) {
3606
3798
  const note_ = (origin ?? "created") === "joined" ? "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u786E\u8BA4\u540E**\u6211\u4F1A\u9000\u51FA\u8BE5\u7FA4**\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002";
3607
3799
  return card(
@@ -3675,6 +3867,7 @@ function buildWatchdogCustomCard(cfg) {
3675
3867
  function buildGroupSettingsCard(project) {
3676
3868
  const kind = project.kind ?? "multi";
3677
3869
  const noMention = project.noMention ?? defaultNoMention(project);
3870
+ const autoCompact = project.autoCompact ?? true;
3678
3871
  const scopeNote = kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u7684\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002";
3679
3872
  return card(
3680
3873
  [
@@ -3685,7 +3878,12 @@ function buildGroupSettingsCard(project) {
3685
3878
  { label: "\u5173", value: "off" }
3686
3879
  ]),
3687
3880
  note(scopeNote),
3688
- note("\u26A0\uFE0F \u514D@ \u9700\u5E94\u7528\u5DF2\u5F00\u901A\u300C\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\u300D(im:message.group_msg)\u6743\u9650\uFF0C\u5426\u5219\u6536\u4E0D\u5230\u975E @ \u6D88\u606F\u3002")
3881
+ note("\u26A0\uFE0F \u514D@ \u9700\u5E94\u7528\u5DF2\u5F00\u901A\u300C\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\u300D(im:message.group_msg)\u6743\u9650\uFF0C\u5426\u5219\u6536\u4E0D\u5230\u975E @ \u6D88\u606F\u3002"),
3882
+ ...optionRow("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587", GS.setAutoCompact, autoCompact ? "on" : "off", [
3883
+ { label: "\u5F00", value: "on" },
3884
+ { label: "\u5173", value: "off" }
3885
+ ]),
3886
+ note("\u5F00\u542F\u540E\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\u65F6 Codex \u81EA\u52A8\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF08\u9ED8\u8BA4\u5F00\uFF09\u3002\u6539\u52A8\u4E0B\u4E00\u8F6E\u4F1A\u8BDD\u751F\u6548\u3002")
3689
3887
  ],
3690
3888
  { header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
3691
3889
  );
@@ -3803,6 +4001,7 @@ function buildPermissionCard(p) {
3803
4001
  function buildProjectSettingsCard(project) {
3804
4002
  const kind = project.kind ?? "multi";
3805
4003
  const noMention = project.noMention ?? defaultNoMention(project);
4004
+ const autoCompact = project.autoCompact ?? true;
3806
4005
  return card(
3807
4006
  [
3808
4007
  md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
@@ -3820,6 +4019,13 @@ function buildProjectSettingsCard(project) {
3820
4019
  kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002"
3821
4020
  ),
3822
4021
  hr(),
4022
+ md("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587"),
4023
+ actions([
4024
+ button("\u5F00", { a: DM.setAutoCompactDm, v: "on", n: project.name }, autoCompact ? "primary" : "default"),
4025
+ button("\u5173", { a: DM.setAutoCompactDm, v: "off", n: project.name }, autoCompact ? "default" : "primary")
4026
+ ]),
4027
+ note("\u5F00\u542F\u540E\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\u65F6 Codex \u81EA\u52A8\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF08\u9ED8\u8BA4\u5F00\uFF09\u3002\u6539\u52A8\u4E0B\u4E00\u8F6E\u4F1A\u8BDD\u751F\u6548\u3002"),
4028
+ hr(),
3823
4029
  actions([button("\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", { a: DM.allowlist, n: project.name }, "primary")]),
3824
4030
  note("\u8BBE\u7F6E\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex\uFF08\u7A7A = \u6240\u6709\u4EBA\uFF09\u3002"),
3825
4031
  hr(),
@@ -5365,8 +5571,8 @@ async function patchSession(threadId, patch) {
5365
5571
  const rec = sessions.find((s) => s.threadId === threadId);
5366
5572
  if (!rec) return;
5367
5573
  const target = rec;
5368
- for (const [k, v] of Object.entries(patch)) {
5369
- if (v !== void 0) target[k] = v;
5574
+ for (const [k2, v] of Object.entries(patch)) {
5575
+ if (v !== void 0) target[k2] = v;
5370
5576
  }
5371
5577
  rec.updatedAt = Date.now();
5372
5578
  await write2(sessions);
@@ -5484,7 +5690,7 @@ function walkForImageKeys(node, out) {
5484
5690
  }
5485
5691
  const obj = node;
5486
5692
  if (obj.tag === "img" && typeof obj.image_key === "string") out.push(obj.image_key);
5487
- for (const k of Object.keys(obj)) walkForImageKeys(obj[k], out);
5693
+ for (const k2 of Object.keys(obj)) walkForImageKeys(obj[k2], out);
5488
5694
  }
5489
5695
  async function downloadOne(channel, ref, index) {
5490
5696
  try {
@@ -6125,6 +6331,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6125
6331
  const runCards = /* @__PURE__ */ new Map();
6126
6332
  const runStreams = /* @__PURE__ */ new Map();
6127
6333
  const lastRunCard = /* @__PURE__ */ new Map();
6334
+ const lastUsage = /* @__PURE__ */ new Map();
6128
6335
  let modelsCache = null;
6129
6336
  async function listModels() {
6130
6337
  if (!modelsCache) modelsCache = await backend.listModels();
@@ -6220,6 +6427,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6220
6427
  await postModelCard(msg, ts.sessionKey);
6221
6428
  return;
6222
6429
  }
6430
+ if (cmd === "compact") {
6431
+ await runCompact(msg, ts.sessionKey, false, ts);
6432
+ return;
6433
+ }
6434
+ if (cmd === "context") {
6435
+ await postContextCard(msg, ts.sessionKey, false);
6436
+ return;
6437
+ }
6223
6438
  handleTurn(msg, text, ts.sessionKey, true, project, ts);
6224
6439
  return;
6225
6440
  }
@@ -6233,6 +6448,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6233
6448
  await postModelCard(msg, ts.sessionKey);
6234
6449
  return;
6235
6450
  }
6451
+ if (cmd === "compact") {
6452
+ await runCompact(msg, ts.sessionKey, true, ts);
6453
+ return;
6454
+ }
6455
+ if (cmd === "context") {
6456
+ await postContextCard(msg, ts.sessionKey, true);
6457
+ return;
6458
+ }
6236
6459
  handleTurn(msg, text, ts.sessionKey, false, project, ts);
6237
6460
  return;
6238
6461
  }
@@ -6248,8 +6471,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6248
6471
  await postGroupSettings(msg, project);
6249
6472
  return;
6250
6473
  }
6251
- if (cmd === "model") {
6252
- await channel.send(msg.chatId, { markdown: "`/model` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
6474
+ if (cmd === "model" || cmd === "compact" || cmd === "context") {
6475
+ await channel.send(msg.chatId, { markdown: `\`/${cmd}\` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
6253
6476
  return;
6254
6477
  }
6255
6478
  startTopicDirectly(msg, text, project);
@@ -6257,7 +6480,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6257
6480
  function parseCommand(text) {
6258
6481
  const m = /^\/(\w+)/.exec(text);
6259
6482
  const name = m?.[1]?.toLowerCase();
6260
- return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
6483
+ return name === "resume" || name === "model" || name === "settings" || name === "help" || name === "compact" || name === "context" ? name : null;
6261
6484
  }
6262
6485
  function shouldRespondWithoutMention(project, msg) {
6263
6486
  if (!(project.noMention ?? defaultNoMention(project))) return false;
@@ -6286,7 +6509,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6286
6509
  function turnPerm(project, senderId) {
6287
6510
  if (!project) return {};
6288
6511
  const t = turnTier(project, isAdmin(cfg, senderId));
6289
- return { mode: t.mode, network: project.network, roleSuffix: t.split ? t.role : void 0 };
6512
+ return { mode: t.mode, network: project.network, autoCompact: project.autoCompact, roleSuffix: t.split ? t.role : void 0 };
6290
6513
  }
6291
6514
  function turnSession(baseKey, project, senderId) {
6292
6515
  const perm = turnPerm(project, senderId);
@@ -6351,7 +6574,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6351
6574
  let firstText = preIngested ? text : await ingestContext(msg, text);
6352
6575
  const { thread: resolved, recreated } = await resolveThread(sessionKey, msg.chatId, {
6353
6576
  mode: perm.mode,
6354
- network: perm.network
6577
+ network: perm.network,
6578
+ autoCompact: perm.autoCompact
6355
6579
  });
6356
6580
  let thread = resolved;
6357
6581
  const neverSeen = !thread;
@@ -6359,7 +6583,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6359
6583
  const prior = neverSeen ? void 0 : await getSession(sessionKey);
6360
6584
  if (!thread) {
6361
6585
  const cwd = project?.cwd ?? fallbackCwd;
6362
- thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
6586
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6363
6587
  sessions.set(sessionKey, thread);
6364
6588
  await upsertSession({
6365
6589
  threadId: sessionKey,
@@ -6418,7 +6642,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6418
6642
  model: rec.model,
6419
6643
  effort: rec.effort,
6420
6644
  mode: perm?.mode,
6421
- network: perm?.network
6645
+ network: perm?.network,
6646
+ autoCompact: perm?.autoCompact
6422
6647
  });
6423
6648
  sessions.set(threadId, resumed);
6424
6649
  return { thread: resumed, recreated: false };
@@ -6431,7 +6656,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6431
6656
  model: rec.model,
6432
6657
  effort: rec.effort,
6433
6658
  mode: perm?.mode ?? project?.mode,
6434
- network: perm?.network ?? project?.network
6659
+ network: perm?.network ?? project?.network,
6660
+ autoCompact: perm?.autoCompact ?? project?.autoCompact
6435
6661
  });
6436
6662
  sessions.set(threadId, fresh);
6437
6663
  await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
@@ -6459,7 +6685,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6459
6685
  const { model, effort } = pickDefault(await listModels());
6460
6686
  let thread;
6461
6687
  try {
6462
- thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
6688
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6463
6689
  } catch (err) {
6464
6690
  reaction.done();
6465
6691
  log.fail("card", err, { phase: "start-topic" });
@@ -6533,6 +6759,76 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6533
6759
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
6534
6760
  });
6535
6761
  }
6762
+ async function postContextCard(msg, sessionKey, inThread) {
6763
+ const u = lastUsage.get(sessionKey);
6764
+ await sendManagedCard(channel, msg.chatId, buildContextCard(u?.used ?? 0, u?.window ?? null), msg.messageId, inThread).catch(
6765
+ (err) => log.fail("card", err, { phase: "context" })
6766
+ );
6767
+ }
6768
+ const COMPACT_ANIM_INTERVAL_MS = 800;
6769
+ async function runCompact(msg, sessionKey, inThread, perm) {
6770
+ const reply = (markdown) => channel.send(msg.chatId, { markdown }, { replyTo: msg.messageId, replyInThread: inThread }).then(() => void 0, () => void 0);
6771
+ if (active.get(sessionKey)) {
6772
+ await reply("\u23F3 \u8FD9\u4E00\u8F6E\u8FD8\u5728\u8DD1\uFF0C\u7ED3\u675F\u540E\u518D `/compact`\u3002");
6773
+ return;
6774
+ }
6775
+ const { thread } = await resolveThread(sessionKey, msg.chatId, {
6776
+ mode: perm.mode,
6777
+ network: perm.network,
6778
+ autoCompact: perm.autoCompact
6779
+ });
6780
+ if (!thread) {
6781
+ await reply("\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u5F00\u59CB\uFF0C\u5148\u53D1\u6761\u6D88\u606F\u804A\u4E24\u53E5\u518D `/compact`\u3002");
6782
+ return;
6783
+ }
6784
+ let cardMsgId;
6785
+ try {
6786
+ const sent = await sendManagedCard(channel, msg.chatId, buildCompactingCard(0), msg.messageId, inThread);
6787
+ cardMsgId = sent.messageId;
6788
+ } catch (err) {
6789
+ log.fail("card", err, { phase: "compact-start-card" });
6790
+ }
6791
+ let stop = false;
6792
+ const wakers = [];
6793
+ const sleep = (ms) => new Promise((res) => {
6794
+ const t = setTimeout(res, ms);
6795
+ wakers.push(() => {
6796
+ clearTimeout(t);
6797
+ res();
6798
+ });
6799
+ });
6800
+ const anim = (async () => {
6801
+ let tick = 0;
6802
+ while (!stop && cardMsgId) {
6803
+ await sleep(COMPACT_ANIM_INTERVAL_MS);
6804
+ if (stop || !cardMsgId) break;
6805
+ tick++;
6806
+ await updateManagedCard(channel, cardMsgId, buildCompactingCard(tick)).catch(() => void 0);
6807
+ }
6808
+ })();
6809
+ const settle = async (result) => {
6810
+ stop = true;
6811
+ wakers.forEach((w) => w());
6812
+ await anim;
6813
+ if (cardMsgId && await updateManagedCard(channel, cardMsgId, result)) return;
6814
+ await sendManagedCard(channel, msg.chatId, result, msg.messageId, inThread).catch(
6815
+ (err) => log.fail("card", err, { phase: "compact-settle" })
6816
+ );
6817
+ };
6818
+ const before = lastUsage.get(sessionKey) ?? null;
6819
+ try {
6820
+ const { usage } = await thread.compact();
6821
+ if (usage) lastUsage.set(sessionKey, { used: usage.usedTokens, window: usage.contextWindow });
6822
+ else lastUsage.delete(sessionKey);
6823
+ log.info("intake", "compact", { sessionKey, used: usage?.usedTokens ?? null, before: before?.used ?? null });
6824
+ await settle(buildCompactedCard(usage, before));
6825
+ } catch (err) {
6826
+ const m = err instanceof Error ? err.message : String(err);
6827
+ const unsupported = /method not found|-32601|unknown (method|request)/i.test(m);
6828
+ log.fail("intake", err, { phase: "compact" });
6829
+ await settle(buildCompactFailedCard(unsupported ? "\u5F53\u524D codex \u7248\u672C\u4E0D\u652F\u6301 /compact\uFF0C\u8BF7\u5347\u7EA7\u540E\u518D\u8BD5\u3002" : m));
6830
+ }
6831
+ }
6536
6832
  async function postHelpCard(msg, scope, inThread = false, project) {
6537
6833
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
6538
6834
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
@@ -6548,24 +6844,26 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6548
6844
  const settleUpdate = (msgId, c, fallbackChatId) => {
6549
6845
  const armedAt = Date.now();
6550
6846
  void (async () => {
6551
- await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6552
- const card2 = typeof c === "function" ? await c() : c;
6553
- const ok = await updateManagedCard(channel, msgId, card2);
6554
- log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
6555
- if (!ok && fallbackChatId) {
6556
- await sendManagedCard(channel, fallbackChatId, card2).catch(
6557
- (err) => log.fail("console", err, { phase: "settle-fallback" })
6558
- );
6847
+ try {
6848
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6849
+ const card2 = typeof c === "function" ? await c() : c;
6850
+ const ok = await updateManagedCard(channel, msgId, card2);
6851
+ log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
6852
+ if (!ok && fallbackChatId) {
6853
+ await sendManagedCard(channel, fallbackChatId, card2);
6854
+ }
6855
+ } catch (err) {
6856
+ log.fail("console", err, { phase: "settle-update", msgId });
6559
6857
  }
6560
6858
  })();
6561
6859
  };
6562
6860
  function pruneResumePending() {
6563
6861
  const now = Date.now();
6564
- for (const [k, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k);
6862
+ for (const [k2, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k2);
6565
6863
  }
6566
6864
  function pruneModelPending() {
6567
6865
  const now = Date.now();
6568
- for (const [k, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k);
6866
+ for (const [k2, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k2);
6569
6867
  }
6570
6868
  function authPending(map, evt) {
6571
6869
  const state = map.get(evt.messageId);
@@ -6929,6 +7227,19 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6929
7227
  }
6930
7228
  return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
6931
7229
  });
7230
+ }).on(GS.setAutoCompact, ({ evt, value }) => {
7231
+ if (!isAdmin(cfg, evt.operator?.openId ?? "")) return;
7232
+ const on = value.v === "on";
7233
+ patch(evt, async () => {
7234
+ const project = await getProjectByChatId(evt.chatId);
7235
+ if (project) {
7236
+ await updateProject(project.name, { autoCompact: on });
7237
+ await evictLiveSessionsForChat(project.chatId);
7238
+ log.info("console", "group-autocompact", { project: project.name, on });
7239
+ return buildGroupSettingsCard({ ...project, autoCompact: on });
7240
+ }
7241
+ return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", autoCompact: on });
7242
+ });
6932
7243
  }).on(DM.admins, ({ evt }) => {
6933
7244
  if (!dmAdmin(evt.operator?.openId)) return;
6934
7245
  patch(
@@ -7020,6 +7331,15 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7020
7331
  const p = await getProjectByName(name);
7021
7332
  return p ? buildProjectSettingsCard(p) : buildDmMenuCard();
7022
7333
  });
7334
+ }).on(DM.projectTopics, ({ evt, value }) => {
7335
+ if (!dmAdmin(evt.operator?.openId)) return;
7336
+ const name = typeof value.n === "string" ? value.n : "";
7337
+ patch(evt, async () => {
7338
+ const p = await getProjectByName(name);
7339
+ if (!p) return buildDmMenuCard();
7340
+ const sessions2 = (await listSessions()).filter((s) => s.chatId === p.chatId);
7341
+ return buildProjectTopicsCard(p, sessions2);
7342
+ });
7023
7343
  }).on(DM.setNoMentionDm, ({ evt, value }) => {
7024
7344
  if (!dmAdmin(evt.operator?.openId)) return;
7025
7345
  const name = typeof value.n === "string" ? value.n : "";
@@ -7030,6 +7350,18 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7030
7350
  await updateProject(name, { noMention: on });
7031
7351
  return buildProjectSettingsCard({ ...p, noMention: on });
7032
7352
  });
7353
+ }).on(DM.setAutoCompactDm, ({ evt, value }) => {
7354
+ if (!dmAdmin(evt.operator?.openId)) return;
7355
+ const name = typeof value.n === "string" ? value.n : "";
7356
+ const on = value.v === "on";
7357
+ patch(evt, async () => {
7358
+ const p = await getProjectByName(name);
7359
+ if (!p) return buildDmMenuCard();
7360
+ await updateProject(name, { autoCompact: on });
7361
+ await evictLiveSessionsForChat(p.chatId);
7362
+ log.info("console", "project-autocompact", { project: name, on });
7363
+ return buildProjectSettingsCard({ ...p, autoCompact: on });
7364
+ });
7033
7365
  }).on(DM.permission, ({ evt, value }) => {
7034
7366
  if (!dmAdmin(evt.operator?.openId)) return;
7035
7367
  const name = typeof value.n === "string" ? value.n : "";
@@ -7221,6 +7553,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7221
7553
  }
7222
7554
  lastEvAt = tEv;
7223
7555
  evCount++;
7556
+ if (et === "context_usage" && topicThreadId) {
7557
+ const cu = ev;
7558
+ lastUsage.set(topicThreadId, { used: cu.usedTokens, window: cu.contextWindow });
7559
+ } else if (et === "context_compacted") {
7560
+ void sendManagedCard(channel, opts.chatId, buildAutoCompactCard(), cardMsgId, !opts.flat).catch(
7561
+ (err) => log.fail("card", err, { phase: "auto-compact-notice" })
7562
+ );
7563
+ }
7224
7564
  render.apply(ev);
7225
7565
  rc.rs = render.snapshot();
7226
7566
  stream2.streamCoalesced(channel, buildRunCard(rc), ANSWER_EID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {