@modelzen/feishu-codex-bridge 0.3.3 → 0.3.4

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 (3) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.js +679 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,8 @@
30
30
  - **流式卡片**:推理 / 命令 / 文件改动 / 结果实时刷新到一张可折叠卡片。
31
31
  - **免 @ 对话**:项目群话题内可直接说话、不必每次 @(可逐群开关)。
32
32
  - **文档评论回复(可选)**:在飞书云文档(doc/docx/sheet/file,含知识库 wiki)的评论里 **@机器人**,它会读评论、跑 Codex、把答案回到同一条评论线程里;每篇文档一条连续会话。需额外开通文档评论权限并订阅评论事件(见下方配置)。
33
- - **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、诊断、重连。
33
+ - **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、用量、诊断、重连。
34
+ - **📊 Codex 用量**:5 小时 / 7 天限额进度(剩余 % + 重置时间)、lifetime tokens、连续使用天数、GitHub 风格每日用量热力图;一键生成**战绩分享卡**,可原生转发给任何人或群(数据来自 Codex 个人资料页同款接口,需 ChatGPT 登录)。
34
35
  - **稳定隔离**:每会话独立 app-server 进程;卡死有 watchdog(默认 120s)→ 终止 → 回收,异常不波及其他群。
35
36
  - **本地加密密钥库**:飞书应用密钥用 AES-256-GCM 存在 `~/.feishu-codex-bridge/`,不入仓库、不进环境变量。
36
37
  - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。注:跨平台指进程运行与后台自启;「项目内只读/读写」隐私沙箱仅 macOS / 原生 Windows 可强制(见[安全须知](#-安全须知))。
@@ -166,7 +167,8 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
166
167
  - **话题 = 会话**:对某条消息开话题后,话题内可**免 @** 连续对话,是一条连贯的 Codex 会话。
167
168
  - **文档评论 @机器人**:在飞书文档评论里 @ 它就回(前提:已开通文档评论权限 + 订阅 `drive.notice.comment_add_v1`,且机器人对该文档有访问权限)。只支持 doc/docx/sheet/file;评论框不渲染 markdown,回复为纯文本,超长会截断。
168
169
  - **终止**:卡片上的 **⏹** 随时终止当前轮;卡死超过 watchdog 阈值(默认 120s)自动中止并回收进程。
169
- - **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog / 管理员)、诊断、重连,全在私聊菜单里。
170
+ - **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog / 管理员)、用量、诊断、重连,全在私聊菜单里。
171
+ - **📊 用量**:点「用量」看 5h/7d 限额(剩余 % + 重置时间)与 Codex 个人统计(lifetime tokens / streak / 每日热力图);点「📤 生成分享卡」得到一张可转发的战绩卡——长按(手机)或右键(电脑)即可转发,数据定格在生成时刻。
170
172
 
171
173
  ---
172
174
 
package/dist/cli.js CHANGED
@@ -2072,6 +2072,7 @@ var RunRender = class {
2072
2072
  // src/card/cards.ts
2073
2073
  function card(elements, opts = {}) {
2074
2074
  const config = { update_multi: true };
2075
+ if (opts.forward === false) config.enable_forward = false;
2075
2076
  if (opts.streaming) {
2076
2077
  config.streaming_mode = true;
2077
2078
  config.streaming_config = {
@@ -3268,6 +3269,12 @@ var DM = {
3268
3269
  reconnect: "dm.reconnect",
3269
3270
  update: "dm.update",
3270
3271
  updateDo: "dm.update.do",
3272
+ // 📊 Codex 用量(限额 + 个人资料统计 + 热力图);share 打开内容选择卡,
3273
+ // shareDo 按所选区块生成可转发的分享卡
3274
+ usage: "dm.usage",
3275
+ usageRefresh: "dm.usage.refresh",
3276
+ usageShare: "dm.usage.share",
3277
+ usageShareDo: "dm.usage.share.do",
3271
3278
  rmConfirm: "dm.rmConfirm",
3272
3279
  rmDo: "dm.rmDo",
3273
3280
  rmCancel: "dm.rmCancel",
@@ -3308,6 +3315,7 @@ function buildDmMenuCard() {
3308
3315
  button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.settings })
3309
3316
  ]),
3310
3317
  actions([
3318
+ button("\u{1F4CA} \u7528\u91CF", { a: DM.usage }),
3311
3319
  button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
3312
3320
  button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect }),
3313
3321
  button("\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", { a: DM.update })
@@ -4477,10 +4485,615 @@ async function restartDaemon() {
4477
4485
  await getServiceAdapter().restart();
4478
4486
  }
4479
4487
 
4488
+ // src/agent/codex-appserver/usage.ts
4489
+ import { readFile as readFile8 } from "fs/promises";
4490
+ import { homedir as homedir5 } from "os";
4491
+ import { join as join12 } from "path";
4492
+ var DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
4493
+ var HTTP_TIMEOUT_MS = 15e3;
4494
+ var REFRESH_TIMEOUT_MS = 2e4;
4495
+ var EXP_SKEW_MS = 6e4;
4496
+ var PROFILE_CACHE_MS = 5 * 6e4;
4497
+ var USAGE_CACHE_MS = 3e4;
4498
+ var UsageError = class extends Error {
4499
+ constructor(kind, message) {
4500
+ super(message);
4501
+ this.kind = kind;
4502
+ this.name = "UsageError";
4503
+ }
4504
+ kind;
4505
+ };
4506
+ function resolveCodexHome() {
4507
+ return process.env.CODEX_HOME ?? join12(homedir5(), ".codex");
4508
+ }
4509
+ async function readCodexAuth() {
4510
+ const file = join12(resolveCodexHome(), "auth.json");
4511
+ let lastErr;
4512
+ for (let i = 0; i < 3; i++) {
4513
+ let raw;
4514
+ try {
4515
+ raw = await readFile8(file, "utf8");
4516
+ } catch (err) {
4517
+ throw new UsageError("no-auth", `\u8BFB\u4E0D\u5230 ${file}\uFF1A${err instanceof Error ? err.message : String(err)}`);
4518
+ }
4519
+ try {
4520
+ const j = JSON.parse(raw);
4521
+ const accessToken = j.tokens?.access_token;
4522
+ if (!accessToken) throw new UsageError("api-key-mode", "auth.json \u6CA1\u6709 ChatGPT access_token\uFF08API-key \u767B\u5F55\u6A21\u5F0F\uFF09");
4523
+ return { accessToken, accountId: j.tokens?.account_id, lastRefresh: j.last_refresh };
4524
+ } catch (err) {
4525
+ if (err instanceof UsageError) throw err;
4526
+ lastErr = err;
4527
+ await new Promise((r) => setTimeout(r, 100));
4528
+ }
4529
+ }
4530
+ throw new UsageError("no-auth", `auth.json \u53CD\u590D\u89E3\u6790\u5931\u8D25\uFF1A${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
4531
+ }
4532
+ function jwtExpMs(token) {
4533
+ const part = token.split(".")[1];
4534
+ if (!part) return void 0;
4535
+ try {
4536
+ const payload = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
4537
+ return typeof payload.exp === "number" ? payload.exp * 1e3 : void 0;
4538
+ } catch {
4539
+ return void 0;
4540
+ }
4541
+ }
4542
+ async function chatgptBaseUrl() {
4543
+ try {
4544
+ const raw = await readFile8(join12(resolveCodexHome(), "config.toml"), "utf8");
4545
+ for (const line of raw.split("\n")) {
4546
+ const t = line.trim();
4547
+ if (t.startsWith("[")) break;
4548
+ const m = /^chatgpt_base_url\s*=\s*"([^"]+)"/.exec(t);
4549
+ if (m?.[1]) return m[1].replace(/\/+$/, "");
4550
+ }
4551
+ } catch {
4552
+ }
4553
+ return DEFAULT_BASE_URL;
4554
+ }
4555
+ var refreshInFlight = null;
4556
+ async function refreshViaAppServer() {
4557
+ if (refreshInFlight) return refreshInFlight;
4558
+ refreshInFlight = (async () => {
4559
+ const before = await readCodexAuth().catch(() => void 0);
4560
+ const bin = resolveCodexBin();
4561
+ if (!bin) return null;
4562
+ const client = new AppServerClient({ bin, cwd: process.cwd(), clientName: "feishu-codex-bridge-usage" });
4563
+ let account = void 0;
4564
+ try {
4565
+ await withDeadline2(client.connect(), REFRESH_TIMEOUT_MS, "usage-refresh connect");
4566
+ const res = await withDeadline2(
4567
+ client.request("account/read", { refreshToken: true }),
4568
+ REFRESH_TIMEOUT_MS,
4569
+ "account/read refresh"
4570
+ );
4571
+ account = res?.account;
4572
+ } catch (err) {
4573
+ log.fail("usage", err, { phase: "refresh" });
4574
+ return null;
4575
+ } finally {
4576
+ await client.close().catch(() => void 0);
4577
+ }
4578
+ const after = await readCodexAuth().catch(() => void 0);
4579
+ if (after && after.accessToken !== before?.accessToken) return after;
4580
+ if (account === null) return "permanent-failure";
4581
+ return null;
4582
+ })();
4583
+ try {
4584
+ return await refreshInFlight;
4585
+ } finally {
4586
+ refreshInFlight = null;
4587
+ }
4588
+ }
4589
+ function withDeadline2(p, ms, label) {
4590
+ return new Promise((resolve7, reject) => {
4591
+ const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
4592
+ p.then(
4593
+ (v) => {
4594
+ clearTimeout(t);
4595
+ resolve7(v);
4596
+ },
4597
+ (e) => {
4598
+ clearTimeout(t);
4599
+ reject(e);
4600
+ }
4601
+ );
4602
+ });
4603
+ }
4604
+ async function fetchWham(base, path, auth) {
4605
+ const ctl = new AbortController();
4606
+ const t = setTimeout(() => ctl.abort(), HTTP_TIMEOUT_MS);
4607
+ try {
4608
+ const resp = await fetch(`${base}${path}`, {
4609
+ headers: {
4610
+ Authorization: `Bearer ${auth.accessToken}`,
4611
+ ...auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {},
4612
+ "User-Agent": "codex-cli"
4613
+ },
4614
+ signal: ctl.signal
4615
+ });
4616
+ if (!resp.ok) return { status: resp.status };
4617
+ return { status: resp.status, json: await resp.json() };
4618
+ } finally {
4619
+ clearTimeout(t);
4620
+ }
4621
+ }
4622
+ async function whamGet(path) {
4623
+ let auth = await readCodexAuth();
4624
+ const exp = jwtExpMs(auth.accessToken);
4625
+ if (exp !== void 0 && exp <= Date.now() + EXP_SKEW_MS) {
4626
+ const refreshed = await refreshViaAppServer();
4627
+ if (refreshed === "permanent-failure") throw new UsageError("need-relogin", "Codex \u767B\u5F55\u6001\u5DF2\u5931\u6548");
4628
+ if (refreshed) auth = refreshed;
4629
+ else throw new UsageError("transient", "\u767B\u5F55\u6001\u4E34\u671F\u4E14\u6682\u65F6\u65E0\u6CD5\u5237\u65B0");
4630
+ }
4631
+ const base = await chatgptBaseUrl();
4632
+ const attempt = async (a) => {
4633
+ try {
4634
+ return await fetchWham(base, path, a);
4635
+ } catch (err) {
4636
+ throw new UsageError("transient", `\u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
4637
+ }
4638
+ };
4639
+ let res = await attempt(auth);
4640
+ if (res.status === 401) {
4641
+ const fresh = await readCodexAuth();
4642
+ if (fresh.accessToken !== auth.accessToken) {
4643
+ auth = fresh;
4644
+ res = await attempt(auth).catch(() => res);
4645
+ }
4646
+ }
4647
+ if (res.status === 401) {
4648
+ const refreshed = await refreshViaAppServer();
4649
+ if (refreshed === "permanent-failure" || refreshed === null) {
4650
+ throw refreshed === null ? new UsageError("transient", "\u6682\u65F6\u65E0\u6CD5\u5237\u65B0 Codex \u767B\u5F55\u6001") : new UsageError("need-relogin", "Codex \u767B\u5F55\u6001\u5DF2\u5931\u6548");
4651
+ }
4652
+ res = await attempt(refreshed);
4653
+ if (res.status === 401) throw new UsageError("need-relogin", "\u5237\u65B0\u540E\u4ECD 401\uFF0C\u8D26\u53F7\u4FA7\u5DF2\u62D2\u7EDD");
4654
+ }
4655
+ if (res.json === void 0) throw new UsageError("transient", `HTTP ${res.status} (${path})`);
4656
+ return res.json;
4657
+ }
4658
+ function mapWindow(w) {
4659
+ if (!w || typeof w.used_percent !== "number") return void 0;
4660
+ return {
4661
+ usedPercent: Math.min(100, Math.max(0, w.used_percent)),
4662
+ windowSeconds: w.limit_window_seconds,
4663
+ resetAt: w.reset_at
4664
+ };
4665
+ }
4666
+ function mapUsageResponse(raw, fetchedAt) {
4667
+ const mapBucket = (rl, name) => ({
4668
+ ...name ? { name } : {},
4669
+ primary: mapWindow(rl?.primary_window),
4670
+ secondary: mapWindow(rl?.secondary_window)
4671
+ });
4672
+ return {
4673
+ planType: raw.plan_type,
4674
+ main: mapBucket(raw.rate_limit),
4675
+ extras: (raw.additional_rate_limits ?? []).filter((x) => x?.rate_limit).map((x) => mapBucket(x.rate_limit, x.limit_name)),
4676
+ fetchedAt
4677
+ };
4678
+ }
4679
+ function mapProfileResponse(raw) {
4680
+ const s = raw.stats ?? {};
4681
+ return {
4682
+ // 只用 display_name,绝不兜底 username——后者是邮箱 local part,会随可转发的
4683
+ // 分享卡泄出去;display_name 缺失时卡片侧降级「我的」。
4684
+ displayName: raw.profile?.display_name || void 0,
4685
+ lifetimeTokens: s.lifetime_tokens,
4686
+ peakDailyTokens: s.peak_daily_tokens,
4687
+ currentStreakDays: s.current_streak_days,
4688
+ longestStreakDays: s.longest_streak_days,
4689
+ longestTurnSec: s.longest_running_turn_sec,
4690
+ totalThreads: s.total_threads,
4691
+ fastModePct: s.fast_mode_usage_percentage,
4692
+ totalSkillsUsed: s.total_skills_used,
4693
+ uniqueSkillsUsed: s.unique_skills_used,
4694
+ mostUsedEffort: s.most_used_reasoning_effort,
4695
+ mostUsedEffortPct: s.most_used_reasoning_effort_percentage,
4696
+ topInvocations: (s.top_invocations ?? []).map((t) => ({
4697
+ name: t.plugin_name ?? t.skill_name ?? "",
4698
+ count: t.usage_count ?? 0,
4699
+ kind: t.plugin_name ? "plugin" : "skill"
4700
+ })).filter((t) => t.name),
4701
+ dailyBuckets: (s.daily_usage_buckets ?? []).filter((b) => typeof b.start_date === "string").map((b) => ({ date: b.start_date, tokens: b.tokens ?? 0 })),
4702
+ statsAsOf: raw.metadata?.stats_as_of
4703
+ };
4704
+ }
4705
+ var profileCache = null;
4706
+ var usageCache = null;
4707
+ async function fetchProfileStats(force = false) {
4708
+ if (!force && profileCache && Date.now() - profileCache.at < PROFILE_CACHE_MS) return profileCache.data;
4709
+ const raw = await whamGet("/wham/profiles/me");
4710
+ const data = mapProfileResponse(raw);
4711
+ profileCache = { at: Date.now(), data };
4712
+ return data;
4713
+ }
4714
+ async function fetchUsageSnapshot(force = false) {
4715
+ if (!force && usageCache && Date.now() - usageCache.at < USAGE_CACHE_MS) return usageCache.data;
4716
+ const raw = await whamGet("/wham/usage");
4717
+ const data = mapUsageResponse(raw, Date.now());
4718
+ usageCache = { at: Date.now(), data };
4719
+ return data;
4720
+ }
4721
+ async function fetchUsageBundle(force = false) {
4722
+ const [profile, usage] = await Promise.all([fetchProfileStats(force), fetchUsageSnapshot(force)]);
4723
+ return { profile, usage };
4724
+ }
4725
+
4726
+ // src/card/usage-cards.ts
4727
+ function formatTokensZh(n) {
4728
+ if (n === void 0 || n === null || Number.isNaN(n)) return "\u2014";
4729
+ const fmt = (v) => {
4730
+ const s = v.toFixed(1);
4731
+ return s.endsWith(".0") ? s.slice(0, -2) : s;
4732
+ };
4733
+ if (n >= 1e8) return `${fmt(n / 1e8)}\u4EBF`;
4734
+ if (n >= 1e4) {
4735
+ const s = fmt(n / 1e4);
4736
+ return s === "10000" ? "1\u4EBF" : `${s}\u4E07`;
4737
+ }
4738
+ return n.toLocaleString("en-US");
4739
+ }
4740
+ function windowLabel(seconds) {
4741
+ if (!seconds) return "\u9650\u989D";
4742
+ if (seconds === 18e3) return "5 \u5C0F\u65F6";
4743
+ if (seconds === 604800) return "7 \u5929";
4744
+ return seconds < 86400 ? `${Math.round(seconds / 3600)} \u5C0F\u65F6` : `${Math.round(seconds / 86400)} \u5929`;
4745
+ }
4746
+ function resetLabel(resetAtSec, nowMs2 = Date.now()) {
4747
+ const d = new Date(resetAtSec * 1e3);
4748
+ const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
4749
+ const dayKey = (x) => `${x.getFullYear()}-${x.getMonth()}-${x.getDate()}`;
4750
+ const now = new Date(nowMs2);
4751
+ if (dayKey(d) === dayKey(now)) return `\u4ECA\u5929 ${hm}`;
4752
+ const tomorrow = new Date(nowMs2 + 864e5);
4753
+ if (dayKey(d) === dayKey(tomorrow)) return `\u660E\u5929 ${hm}`;
4754
+ return `${d.getMonth() + 1}\u6708${d.getDate()}\u65E5 ${hm}`;
4755
+ }
4756
+ function localDateStr(d = /* @__PURE__ */ new Date()) {
4757
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4758
+ }
4759
+ function toEpochDay(date) {
4760
+ const [y = 1970, m = 1, d = 1] = date.split("-").map(Number);
4761
+ return Date.UTC(y, m - 1, d) / 864e5;
4762
+ }
4763
+ function fromEpochDay(day) {
4764
+ const d = new Date(day * 864e5);
4765
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
4766
+ }
4767
+ function mondayOf(day) {
4768
+ const dow = new Date(day * 864e5).getUTCDay();
4769
+ return day - (dow + 6) % 7;
4770
+ }
4771
+ var DAY_LABELS = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
4772
+ function heatmapCells(buckets, today = localDateStr(), weeks = 14) {
4773
+ const todayDay = toEpochDay(today);
4774
+ const tokensByDay = /* @__PURE__ */ new Map();
4775
+ for (const b of buckets) tokensByDay.set(toEpochDay(b.date), b.tokens);
4776
+ const startMonday = mondayOf(todayDay) - (weeks - 1) * 7;
4777
+ const weekLabel = (c) => {
4778
+ const d = new Date((startMonday + c * 7) * 864e5);
4779
+ return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
4780
+ };
4781
+ const values = [];
4782
+ for (let c = 0; c < weeks; c++) {
4783
+ for (let r = 0; r < 7; r++) {
4784
+ const day = startMonday + c * 7 + r;
4785
+ if (day > todayDay) continue;
4786
+ const v = tokensByDay.get(day) ?? 0;
4787
+ const d = new Date(day * 864e5);
4788
+ const date = `${d.getUTCMonth() + 1}\u6708${d.getUTCDate()}\u65E5`;
4789
+ const label = v > 0 ? `${date} \u4F7F\u7528\u4E86 ${formatTokensZh(v)} Token` : `${date} \u65E0\u7528\u91CF`;
4790
+ values.push({ week: weekLabel(c), day: DAY_LABELS[r] ?? "", value: v, label });
4791
+ }
4792
+ }
4793
+ return { values, startDate: fromEpochDay(startMonday), endDate: today, weeks };
4794
+ }
4795
+ var HEAT_RANGE = ["#ebedf0", "#bbdefb", "#64b5f6", "#1e88e5", "#0d47a1"];
4796
+ var ROUNDED_CELL = "M -0.5 -0.25 Q -0.5 -0.5 -0.25 -0.5 L 0.25 -0.5 Q 0.5 -0.5 0.5 -0.25 L 0.5 0.25 Q 0.5 0.5 0.25 0.5 L -0.25 0.5 Q -0.5 0.5 -0.5 0.25 Z";
4797
+ function heatmapChartEl(buckets, today) {
4798
+ const h = heatmapCells(buckets, today);
4799
+ return {
4800
+ tag: "chart",
4801
+ aspect_ratio: "2:1",
4802
+ chart_spec: {
4803
+ type: "common",
4804
+ padding: 4,
4805
+ data: [{ id: "usage", values: h.values }],
4806
+ series: [
4807
+ {
4808
+ type: "heatmap",
4809
+ xField: "week",
4810
+ yField: "day",
4811
+ valueField: "label",
4812
+ cell: { style: { fill: { field: "value", scale: "color" }, shape: ROUNDED_CELL } }
4813
+ }
4814
+ ],
4815
+ color: { type: "linear", domain: [{ dataId: "usage", fields: ["value"] }], range: HEAT_RANGE },
4816
+ axes: [
4817
+ {
4818
+ orient: "bottom",
4819
+ type: "band",
4820
+ bandPadding: 0.25,
4821
+ domainLine: { visible: false },
4822
+ tick: { visible: false }
4823
+ },
4824
+ {
4825
+ orient: "left",
4826
+ type: "band",
4827
+ bandPadding: 0.25,
4828
+ domainLine: { visible: false },
4829
+ tick: { visible: false },
4830
+ label: { visible: false }
4831
+ }
4832
+ ],
4833
+ legends: { visible: false },
4834
+ tooltip: { visible: true, mark: { title: { visible: false } } }
4835
+ }
4836
+ };
4837
+ }
4838
+ function planLabel(plan) {
4839
+ if (!plan) return void 0;
4840
+ const m = {
4841
+ free: "Free",
4842
+ go: "Go",
4843
+ plus: "Plus",
4844
+ pro: "Pro",
4845
+ prolite: "Pro Lite",
4846
+ team: "Team",
4847
+ business: "Business",
4848
+ enterprise: "Enterprise",
4849
+ edu: "Edu",
4850
+ education: "Edu"
4851
+ };
4852
+ return m[plan] ?? plan.charAt(0).toUpperCase() + plan.slice(1);
4853
+ }
4854
+ function formatDurationZh(seconds) {
4855
+ if (seconds === void 0 || seconds === null || Number.isNaN(seconds) || seconds < 0) return "\u2014";
4856
+ const mins = Math.round(seconds / 60);
4857
+ if (mins < 60) return `${mins} \u5206`;
4858
+ const h = Math.floor(mins / 60);
4859
+ const rem = mins % 60;
4860
+ return rem ? `${h} \u5C0F\u65F6 ${rem} \u5206` : `${h} \u5C0F\u65F6`;
4861
+ }
4862
+ var remainingPct = (w) => Math.max(0, 100 - w.usedPercent);
4863
+ function progressChartEl(w) {
4864
+ const label = `${windowLabel(w.windowSeconds)}\u5269\u4F59`;
4865
+ return {
4866
+ tag: "chart",
4867
+ height: "40px",
4868
+ chart_spec: {
4869
+ type: "linearProgress",
4870
+ data: [{ id: "p", values: [{ type: label, value: remainingPct(w) / 100 }] }],
4871
+ xField: "value",
4872
+ yField: "type",
4873
+ cornerRadius: 8,
4874
+ bandWidth: 12,
4875
+ axes: [
4876
+ { orient: "left", type: "band", visible: false },
4877
+ { orient: "bottom", type: "linear", visible: false }
4878
+ ],
4879
+ tooltip: {
4880
+ visible: true,
4881
+ mark: { title: { visible: false }, content: [{ key: label, value: `${remainingPct(w)}%` }] }
4882
+ }
4883
+ }
4884
+ };
4885
+ }
4886
+ function rateLimitElements(bucket, nowMs2) {
4887
+ const out = [];
4888
+ const icons = ["\u26A1", "\u{1F4C5}"];
4889
+ [bucket.primary, bucket.secondary].forEach((w, i) => {
4890
+ if (!w) return;
4891
+ const reset = w.resetAt ? `\u3000<font color='grey'>${resetLabel(w.resetAt, nowMs2)} \u91CD\u7F6E</font>` : "";
4892
+ out.push(md(`${icons[i]} **${windowLabel(w.windowSeconds)}\u9650\u989D**\u3000\u5269\u4F59 ${remainingPct(w)}%${reset}`));
4893
+ out.push(progressChartEl(w));
4894
+ });
4895
+ if (!out.length) return [note("\u6682\u65E0\u9650\u989D\u6570\u636E")];
4896
+ return out;
4897
+ }
4898
+ function statColumns(items) {
4899
+ return {
4900
+ tag: "column_set",
4901
+ flex_mode: "flow",
4902
+ horizontal_spacing: "large",
4903
+ columns: items.map((it) => ({
4904
+ tag: "column",
4905
+ width: "auto",
4906
+ elements: [
4907
+ { tag: "markdown", content: `**${it.value}**`, text_size: "heading" },
4908
+ noteMd(it.label)
4909
+ ]
4910
+ }))
4911
+ };
4912
+ }
4913
+ function profileStatItems(p) {
4914
+ return [
4915
+ { value: formatTokensZh(p.lifetimeTokens), label: "\u7D2F\u8BA1 Token \u6570" },
4916
+ { value: formatTokensZh(p.peakDailyTokens), label: "\u5CF0\u503C Token \u6570" },
4917
+ { value: formatDurationZh(p.longestTurnSec), label: "\u6700\u957F\u4EFB\u52A1\u65F6\u957F" },
4918
+ { value: p.currentStreakDays !== void 0 ? `${p.currentStreakDays} \u5929` : "\u2014", label: "\u5F53\u524D\u8FDE\u7EED\u5929\u6570" },
4919
+ { value: p.longestStreakDays !== void 0 ? `${p.longestStreakDays} \u5929` : "\u2014", label: "\u6700\u957F\u8FDE\u7EED\u5929\u6570" }
4920
+ ];
4921
+ }
4922
+ function heatmapElements(p, today) {
4923
+ return [md("\u{1F4C8} **\u6BCF\u65E5 Token \u7528\u91CF**"), heatmapChartEl(p.dailyBuckets, today)];
4924
+ }
4925
+ function effortLabel(effort) {
4926
+ const m = { minimal: "\u6781\u4F4E", low: "\u4F4E", medium: "\u4E2D", high: "\u9AD8", xhigh: "\u8D85\u9AD8" };
4927
+ return m[effort] ?? effort;
4928
+ }
4929
+ function insightsElements(p) {
4930
+ const left = [];
4931
+ if (p.fastModePct !== void 0) left.push(`Fast Mode\u3000**${Math.round(p.fastModePct)}%**`);
4932
+ if (p.mostUsedEffort) {
4933
+ const pct = p.mostUsedEffortPct !== void 0 ? ` \xB7 ${Math.round(p.mostUsedEffortPct)}%` : "";
4934
+ left.push(`\u6700\u5E38\u7528\u63A8\u7406\u3000**${effortLabel(p.mostUsedEffort)}${pct}**`);
4935
+ }
4936
+ if (p.uniqueSkillsUsed !== void 0) left.push(`\u4F7F\u7528\u8FC7\u7684\u6280\u80FD\u3000**${p.uniqueSkillsUsed}**`);
4937
+ if (p.totalSkillsUsed !== void 0) left.push(`\u6280\u80FD\u8C03\u7528\u603B\u6570\u3000**${p.totalSkillsUsed.toLocaleString("en-US")}**`);
4938
+ if (p.totalThreads !== void 0) left.push(`\u4F1A\u8BDD\u603B\u6570\u3000**${p.totalThreads.toLocaleString("en-US")}**`);
4939
+ const right = p.topInvocations.slice(0, 5).map((t) => `${t.kind === "plugin" ? "@" : "$"}${t.name}\u3000**\xD7${t.count}**`);
4940
+ const col = (title, lines) => ({
4941
+ tag: "column",
4942
+ width: "weighted",
4943
+ weight: 1,
4944
+ elements: [md(`**${title}**`), noteMd(lines.join("\n"))]
4945
+ });
4946
+ const columns = [];
4947
+ if (left.length) columns.push(col("\u6D3B\u52A8\u6D1E\u5BDF", left));
4948
+ if (right.length) columns.push(col("\u5E38\u7528\u63D2\u4EF6 / \u6280\u80FD", right));
4949
+ if (!columns.length) return [];
4950
+ return [
4951
+ { tag: "column_set", flex_mode: columns.length === 2 ? "bisect" : "stretch", horizontal_spacing: "large", columns }
4952
+ ];
4953
+ }
4954
+ function joinWithHr(blocks) {
4955
+ const present = blocks.filter((b) => b.length);
4956
+ const out = [];
4957
+ present.forEach((b, i) => {
4958
+ if (i) out.push(hr());
4959
+ out.push(...b);
4960
+ });
4961
+ return out;
4962
+ }
4963
+ var usageButtons = () => actions([
4964
+ button("\u{1F504} \u5237\u65B0", { a: DM.usageRefresh }),
4965
+ button("\u{1F4E4} \u751F\u6210\u5206\u4EAB\u5361", { a: DM.usageShare }, "primary"),
4966
+ button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })
4967
+ ]);
4968
+ var ERROR_COPY = {
4969
+ "no-auth": {
4970
+ title: "\u672A\u627E\u5230 Codex \u767B\u5F55\u6001",
4971
+ hint: "\u672C\u673A\u6CA1\u6709\u53EF\u8BFB\u7684 `~/.codex/auth.json`\uFF0C\u8BF7\u5728\u5BBF\u4E3B\u673A\u7EC8\u7AEF\u8FD0\u884C `codex login` \u540E\u91CD\u8BD5\u3002"
4972
+ },
4973
+ "api-key-mode": {
4974
+ title: "\u5F53\u524D\u662F API-key \u767B\u5F55\u6A21\u5F0F",
4975
+ hint: "\u7528\u91CF\u7EDF\u8BA1\u4E0E\u9650\u989D\u6570\u636E\u4EC5 **ChatGPT \u767B\u5F55**\uFF08`codex login`\uFF09\u53EF\u7528\uFF0CAPI-key \u6A21\u5F0F\u6CA1\u6709\u8FD9\u4EFD\u6570\u636E\u3002"
4976
+ },
4977
+ "need-relogin": {
4978
+ title: "Codex \u767B\u5F55\u6001\u5DF2\u5931\u6548",
4979
+ hint: "\u4EE4\u724C\u5DF2\u65E0\u6CD5\u5237\u65B0\uFF08\u8FC7\u671F/\u88AB\u64A4\u9500\uFF09\uFF0C\u8BF7\u5728\u5BBF\u4E3B\u673A\u7EC8\u7AEF\u91CD\u65B0\u8FD0\u884C `codex login`\u3002"
4980
+ },
4981
+ transient: {
4982
+ title: "\u6682\u65F6\u62C9\u4E0D\u5230\u6570\u636E",
4983
+ hint: "\u7F51\u7EDC\u6216 ChatGPT \u670D\u52A1\u6CE2\u52A8\uFF0C\u7A0D\u540E\u70B9\u300C\u{1F504} \u5237\u65B0\u300D\u91CD\u8BD5\u3002"
4984
+ }
4985
+ };
4986
+ function buildUsageCard(state) {
4987
+ if (state.phase === "loading") {
4988
+ return card([md("\u23F3 \u6B63\u5728\u62C9\u53D6 Codex \u7528\u91CF\u6570\u636E\u2026"), note("\u67E5\u8BE2 ChatGPT \u540E\u7AEF\uFF0C\u901A\u5E38 1~3 \u79D2\u3002")], {
4989
+ header: { title: "\u{1F4CA} Codex \u7528\u91CF", template: "wathet" },
4990
+ forward: false
4991
+ });
4992
+ }
4993
+ if (state.phase === "error") {
4994
+ const copy = ERROR_COPY[state.kind];
4995
+ return card(
4996
+ [
4997
+ md(`\u26A0\uFE0F **${copy.title}**`),
4998
+ md(copy.hint),
4999
+ ...state.kind === "transient" ? [note(state.message)] : [],
5000
+ usageButtons()
5001
+ ],
5002
+ { header: { title: "\u{1F4CA} Codex \u7528\u91CF", template: "orange" }, forward: false }
5003
+ );
5004
+ }
5005
+ const { profile, usage } = state.data;
5006
+ const nowMs2 = state.now ?? Date.now();
5007
+ const elements = joinWithHr([
5008
+ rateLimitElements(usage.main, nowMs2),
5009
+ [statColumns(profileStatItems(profile))],
5010
+ heatmapElements(profile, state.today),
5011
+ insightsElements(profile)
5012
+ ]);
5013
+ const plan = planLabel(usage.planType);
5014
+ elements.push(
5015
+ note(`\u7EDF\u8BA1\u622A\u81F3 ${profile.statsAsOf ?? "\u2014"}${plan ? ` \xB7 ${plan} \u5957\u9910` : ""} \xB7 \u6570\u636E\u6765\u81EA Codex \u4E2A\u4EBA\u8D44\u6599`),
5016
+ usageButtons()
5017
+ );
5018
+ return card(elements, {
5019
+ header: {
5020
+ title: "\u{1F4CA} Codex \u7528\u91CF",
5021
+ template: "wathet",
5022
+ ...profile.displayName ? { subtitle: profile.displayName } : {}
5023
+ },
5024
+ forward: false
5025
+ });
5026
+ }
5027
+ var SHARE_SECTIONS = [
5028
+ { key: "stats", label: "\u6838\u5FC3\u7EDF\u8BA1\uFF08\u7D2F\u8BA1 / \u5CF0\u503C / \u8FDE\u7EED\u5929\u6570\uFF09" },
5029
+ { key: "heatmap", label: "\u6BCF\u65E5\u7528\u91CF\u70ED\u529B\u56FE" },
5030
+ { key: "insights", label: "\u6D3B\u52A8\u6D1E\u5BDF\u4E0E\u5E38\u7528\u6280\u80FD" },
5031
+ { key: "limits", label: "\u9650\u989D\u8FDB\u5EA6\uFF085 \u5C0F\u65F6 / 7 \u5929\uFF09" },
5032
+ { key: "plan", label: "\u5957\u9910\u4FE1\u606F" }
5033
+ ];
5034
+ function parseShareSections(v) {
5035
+ const all = SHARE_SECTIONS.map((s) => s.key);
5036
+ const raw = Array.isArray(v) ? v : typeof v === "string" && v ? v.split(",") : [];
5037
+ const picked = raw.filter((x) => all.includes(String(x)));
5038
+ return new Set(picked.length ? picked : all);
5039
+ }
5040
+ function buildShareConfigCard(done = false) {
5041
+ return card(
5042
+ [
5043
+ md("\u9009\u62E9\u8981\u653E\u8FDB\u5206\u4EAB\u5361\u7684\u5185\u5BB9\uFF08**\u4E0D\u9009 = \u5168\u90E8\u5C55\u793A**\uFF09\uFF0C\u751F\u6210\u540E\u957F\u6309 / \u53F3\u952E\u5373\u53EF\u8F6C\u53D1\uFF1A"),
5044
+ {
5045
+ tag: "form",
5046
+ name: "shareCfg",
5047
+ elements: [
5048
+ {
5049
+ tag: "multi_select_static",
5050
+ name: "secs",
5051
+ placeholder: { tag: "plain_text", content: "\u9ED8\u8BA4\u5168\u90E8\u5C55\u793A\uFF0C\u53EF\u53EA\u6311\u90E8\u5206\u533A\u5757" },
5052
+ options: SHARE_SECTIONS.map((s) => ({ text: { tag: "plain_text", content: s.label }, value: s.key }))
5053
+ },
5054
+ submitButton("\u{1F4E4} \u751F\u6210\u5206\u4EAB\u5361", { a: DM.usageShareDo })
5055
+ ]
5056
+ },
5057
+ ...done ? [note("\u2705 \u5206\u4EAB\u5361\u5DF2\u751F\u6210\uFF08\u89C1\u4E0B\u65B9\u65B0\u5361\u7247\uFF09\u3002\u6362\u4E2A\u7EC4\u5408\u53EF\u518D\u6B21\u751F\u6210\u3002")] : [],
5058
+ actions([button("\u2B05\uFE0F \u8FD4\u56DE\u7528\u91CF", { a: DM.usage }), button("\u{1F3E0} \u83DC\u5355", { a: DM.menu })])
5059
+ ],
5060
+ { header: { title: "\u{1F4E4} \u5206\u4EAB\u5185\u5BB9\u9009\u62E9", template: "blue" }, forward: false }
5061
+ );
5062
+ }
5063
+ function buildUsageShareCard(data, opts = {}) {
5064
+ const { profile, usage } = data;
5065
+ const nowMs2 = opts.now ?? Date.now();
5066
+ const sec = opts.sections ?? new Set(SHARE_SECTIONS.map((s) => s.key));
5067
+ const who = profile.displayName ? `${profile.displayName} \u7684` : "\u6211\u7684";
5068
+ const plan = planLabel(usage.planType);
5069
+ const elements = joinWithHr([
5070
+ sec.has("stats") ? [statColumns(profileStatItems(profile))] : [],
5071
+ sec.has("heatmap") ? heatmapElements(profile, opts.today) : [],
5072
+ sec.has("insights") ? insightsElements(profile) : [],
5073
+ sec.has("limits") ? rateLimitElements(usage.main, nowMs2) : [],
5074
+ sec.has("plan") && plan ? [md(`\u{1F48E} **\u5957\u9910**\u3000${plan}`)] : []
5075
+ ]);
5076
+ const stamp = new Date(nowMs2);
5077
+ const stampStr = `${stamp.getMonth() + 1}\u6708${stamp.getDate()}\u65E5 ${String(stamp.getHours()).padStart(2, "0")}:${String(stamp.getMinutes()).padStart(2, "0")}`;
5078
+ elements.push({
5079
+ tag: "markdown",
5080
+ content: `<font color='grey'>\u{1F916} \u7531 </font>[feishu-codex-bridge](https://my.feishu.cn/docx/AFKNdf4QaooL5OxSR8bc5H7vn7b)<font color='grey'> \u4E8E ${stampStr} \u751F\u6210</font>`,
5081
+ text_size: "notation",
5082
+ text_align: "right"
5083
+ });
5084
+ return card(elements, {
5085
+ header: {
5086
+ title: `\u{1F4CA} ${who} Codex \u7528\u91CF`,
5087
+ template: "blue",
5088
+ ...profile.statsAsOf ? { subtitle: `\u7EDF\u8BA1\u622A\u81F3 ${profile.statsAsOf}` } : {}
5089
+ }
5090
+ });
5091
+ }
5092
+
4480
5093
  // src/project/lifecycle.ts
4481
5094
  import { mkdir as mkdir9 } from "fs/promises";
4482
5095
  import { existsSync as existsSync7 } from "fs";
4483
- import { isAbsolute as isAbsolute2, join as join12, resolve as resolve6 } from "path";
5096
+ import { isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
4484
5097
 
4485
5098
  // src/project/git-info.ts
4486
5099
  import { execFile } from "child_process";
@@ -4622,7 +5235,7 @@ async function resolveCwd(name, existingPath) {
4622
5235
  if (!existsSync7(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
4623
5236
  return { cwd: cwd2, blank: false };
4624
5237
  }
4625
- const cwd = join12(paths.projectsRootDir, name);
5238
+ const cwd = join13(paths.projectsRootDir, name);
4626
5239
  await mkdir9(cwd, { recursive: true });
4627
5240
  return { cwd, blank: true };
4628
5241
  }
@@ -4702,12 +5315,12 @@ async function leaveChat(channel, chatId) {
4702
5315
  }
4703
5316
 
4704
5317
  // src/bot/session-store.ts
4705
- import { mkdir as mkdir10, readFile as readFile8, rename as rename5, writeFile as writeFile8 } from "fs/promises";
5318
+ import { mkdir as mkdir10, readFile as readFile9, rename as rename5, writeFile as writeFile8 } from "fs/promises";
4706
5319
  import { dirname as dirname10 } from "path";
4707
5320
  var FILE_VERSION3 = 1;
4708
5321
  async function read2() {
4709
5322
  try {
4710
- const text = await readFile8(paths.sessionsFile, "utf8");
5323
+ const text = await readFile9(paths.sessionsFile, "utf8");
4711
5324
  const parsed = JSON.parse(text);
4712
5325
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
4713
5326
  } catch (err) {
@@ -4764,7 +5377,7 @@ async function handleDmConsole(channel, cfg, msg) {
4764
5377
 
4765
5378
  // src/bot/media.ts
4766
5379
  import { mkdir as mkdir11, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
4767
- import { join as join13 } from "path";
5380
+ import { join as join14 } from "path";
4768
5381
  var MAX_IMAGES2 = 9;
4769
5382
  var MEDIA_TTL_MS = 60 * 6e4;
4770
5383
  var EXT_BY_CONTENT_TYPE = {
@@ -4869,7 +5482,7 @@ async function downloadOne(channel, ref, index) {
4869
5482
  params: { type: "image" }
4870
5483
  });
4871
5484
  const ext = extFromHeaders(res.headers);
4872
- const file = join13(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
5485
+ const file = join14(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
4873
5486
  await res.writeFile(file);
4874
5487
  return file;
4875
5488
  } catch (err) {
@@ -4903,7 +5516,7 @@ async function pruneOldMedia() {
4903
5516
  }
4904
5517
  const cutoff = Date.now() - MEDIA_TTL_MS;
4905
5518
  for (const name of entries) {
4906
- const file = join13(paths.mediaDir, name);
5519
+ const file = join14(paths.mediaDir, name);
4907
5520
  try {
4908
5521
  const st = await stat3(file);
4909
5522
  if (st.mtimeMs < cutoff) await rm4(file, { force: true });
@@ -5714,6 +6327,46 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5714
6327
  const freshMenu = (evt) => {
5715
6328
  patch(evt, buildDmMenuCard());
5716
6329
  };
6330
+ const runUsage = (evt, force) => {
6331
+ if (!dmAdmin(evt.operator?.openId)) return;
6332
+ void (async () => {
6333
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6334
+ let msgId = evt.messageId;
6335
+ const okLoading = await updateManagedCard(channel, msgId, buildUsageCard({ phase: "loading" })).catch(
6336
+ () => false
6337
+ );
6338
+ if (!okLoading) {
6339
+ const sent = await sendManagedCard(channel, evt.chatId, buildUsageCard({ phase: "loading" })).catch(
6340
+ (e) => {
6341
+ log.fail("console", e, { phase: "usage-loading" });
6342
+ return void 0;
6343
+ }
6344
+ );
6345
+ if (!sent) return;
6346
+ msgId = sent.messageId;
6347
+ }
6348
+ let state;
6349
+ try {
6350
+ state = { phase: "ready", data: await fetchUsageBundle(force) };
6351
+ } catch (err) {
6352
+ log.fail("console", err, { phase: "usage" });
6353
+ state = {
6354
+ phase: "error",
6355
+ kind: err instanceof UsageError ? err.kind : "transient",
6356
+ message: err instanceof Error ? err.message : String(err)
6357
+ };
6358
+ }
6359
+ const ok = await updateManagedCard(channel, msgId, buildUsageCard(state)).catch((e) => {
6360
+ log.fail("console", e, { phase: "usage-render" });
6361
+ return false;
6362
+ });
6363
+ if (!ok) {
6364
+ await sendManagedCard(channel, evt.chatId, buildUsageCard(state)).catch(
6365
+ (e) => log.fail("console", e, { phase: "usage-fallback" })
6366
+ );
6367
+ }
6368
+ })();
6369
+ };
5717
6370
  const renderProjectList = async () => {
5718
6371
  const [projects, sessions2] = await Promise.all([listProjects(), listSessions()]);
5719
6372
  const byChat = /* @__PURE__ */ new Map();
@@ -5867,6 +6520,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
5867
6520
  await restartDaemon().catch((e) => log.fail("console", e, { phase: "update-restart" }));
5868
6521
  }
5869
6522
  })();
6523
+ }).on(DM.usage, ({ evt }) => runUsage(evt, false)).on(DM.usageRefresh, ({ evt }) => runUsage(evt, true)).on(DM.usageShare, ({ evt }) => {
6524
+ if (!dmAdmin(evt.operator?.openId)) return;
6525
+ patch(evt, buildShareConfigCard());
6526
+ }).on(DM.usageShareDo, ({ evt, formValue }) => {
6527
+ if (!dmAdmin(evt.operator?.openId)) return;
6528
+ const sections = parseShareSections(formValue?.secs);
6529
+ void (async () => {
6530
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6531
+ try {
6532
+ const data = await fetchUsageBundle();
6533
+ await sendManagedCard(channel, evt.chatId, buildUsageShareCard(data, { sections }), evt.messageId);
6534
+ log.info("console", "usage-share", { sections: [...sections].join(",") });
6535
+ await updateManagedCard(channel, evt.messageId, buildShareConfigCard(true)).catch(() => void 0);
6536
+ } catch (err) {
6537
+ log.fail("console", err, { phase: "usage-share" });
6538
+ const reason = err instanceof UsageError ? err.message : "\u62C9\u53D6\u7528\u91CF\u6570\u636E\u5931\u8D25";
6539
+ await channel.send(evt.chatId, { markdown: `\u26A0\uFE0F \u751F\u6210\u5206\u4EAB\u5361\u5931\u8D25\uFF1A${reason}` }, { replyTo: evt.messageId }).catch(() => void 0);
6540
+ }
6541
+ })();
5870
6542
  }).on(DM.rmConfirm, async ({ evt, value }) => {
5871
6543
  const name = typeof value.n === "string" ? value.n : void 0;
5872
6544
  if (!dmAdmin(evt.operator?.openId) || !name) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {