@modelzen/feishu-codex-bridge 0.3.2 → 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 +20 -11
  2. package/dist/cli.js +889 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,10 +30,11 @@
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
- - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。
37
+ - **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。注:跨平台指进程运行与后台自启;「项目内只读/读写」隐私沙箱仅 macOS / 原生 Windows 可强制(见[安全须知](#-安全须知))。
37
38
 
38
39
  ---
39
40
 
@@ -41,7 +42,7 @@
41
42
 
42
43
  | 依赖 | 说明 | 获取方式 |
43
44
  |------|------|----------|
44
- | **操作系统** | **macOS / Windows** 均支持;Linux·WSL 为 best-effort(已实现 systemd,未广泛实测) | — |
45
+ | **操作系统** | 运行/后台常驻:**macOS / Windows** 均支持,Linux·WSL 为 best-effort(已实现 systemd,未广泛实测)。注意:「项目内只读 / 读写」隐私档的沙箱强制仅 **macOS / 原生 Windows**,Linux·WSL 上这两档会 fail-closed 拒绝启动(见下方[安全须知](#-安全须知)) | — |
45
46
  | **Node.js ≥ 20** | 运行时 | <https://nodejs.org> 或 `nvm install 20` |
46
47
  | **Codex CLI** | 后端,bridge 会 spawn `codex app-server` | `npm i -g @openai/codex`,或装 Codex.app,或用 `CODEX_BIN` 指向已有二进制 |
47
48
  | **Codex 已登录** | app-server 需要 `~/.codex/auth.json` | `codex login` |
@@ -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
 
@@ -241,17 +243,24 @@ src/
241
243
 
242
244
  ## ⚠️ 安全须知
243
245
 
244
- 本机器人调用 Codex 时固定使用 **`approvalPolicy: "never"` + `sandbox: "danger-full-access"`** —— **无任何人工审批、对磁盘完全访问**。这意味着:
246
+ 机器人调用 Codex 始终是 **`approvalPolicy: "never"`**(无人工逐条审批),**沙箱就是唯一的安全闸门**。每个项目有一档**权限**,在私聊控制台「📁 项目列表 ⚙️ 设置 🔐 权限」里用下拉框选择后提交:
245
247
 
246
- > **任何能在项目群里给机器人发消息的人,都能在你这台机器上、以你的身份、在该项目目录里执行任意命令(读写文件、联网、运行脚本)。**
248
+ | 档位 | 能读 | 能写 | 适用 |
249
+ |------|------|------|------|
250
+ | 🔒 **项目内只读** | 仅项目文件夹 | ✗ | 外部群 / 不可信场景的问答机器人 |
251
+ | ✏️ **项目内读写** | 仅项目文件夹 | 仅项目文件夹 | 自己的编码项目,但禁止它碰机器其余部分 |
252
+ | ⚠️ **完全访问** | 整台电脑 | 整台电脑 | 完全信任、你自己掌控的机器 |
247
253
 
248
- 因此:
249
-
250
- - 只把**你信任的人**拉进项目群;
251
- - 在**你自己掌控的机器/账号**上运行,最好是隔离的开发机或容器;
252
- - 绑定的项目目录里不要放你不愿被读写的敏感数据;
254
+ - **管理员 / 普通用户可分设**:🔐 权限里有「管理员档」和「普通用户档」两个下拉。两档**不同**时,管理员与群里其他人**各用独立的 Codex 线程**(互不串沙箱、也互不串对话历史)——典型:外部群里管理员 `完全访问`、其他人 `项目内只读`。两档**相同** = 所有人一致(默认)。
255
+ - **默认值**:你自己新建的项目群 = `完全访问`(与历史行为一致);**别人把机器人拉进的存量/外部群** = `项目内只读`;普通用户档默认**同管理员档**(不分档)。**升级前没有 `mode` 字段的老项目按 `完全访问` 处理**,行为不变。
256
+ - 🔒/✏️ 靠 Codex 的自定义 permissions 档把读写都**锁死在项目文件夹内**(读不到 `~/.ssh`、`/etc` 等),由操作系统沙箱强制:**macOS(Seatbelt)与原生 Windows(restricted token)可强制**,其中 Windows 需 Codex 以 elevated 沙箱运行、否则它会**拒绝执行**(仍不泄漏)。**Linux / WSL 无法强制读限定**(沙箱只挡写、不限读,Landlock 读限制尚未实现,WSL 等同 Linux)——在这些平台选 🔒/✏️ 会被**直接拒绝启动(fail-closed),绝不静默降级为完全访问**;要在 Linux/WSL 用,请把 Codex 跑在容器/隔离环境里。
257
+ > Windows 上的强制是 Codex 自己做的,请先在真机自测一次(让机器人读项目文件夹外的文件,应被拒)再用于真实外部群。
258
+ - ⚠️ `完全访问` 档意味着:**任何能给机器人发消息的人,都能在你这台机器上、以你的身份执行任意命令(读写文件、联网、跑脚本)**。这一档只把**你信任的人**拉进群,在**你自己掌控的隔离机器**上跑,目录里别放不愿被读写的敏感数据。
259
+ - 「联网」是档位之外的独立开关,只影响它执行的 shell 命令能否上网,不影响模型本身和 Codex 自带的联网搜索。
253
260
  - 它不是多租户托管服务,是给你(和你信任的小团队)自用的桥。
254
261
 
262
+ > 把机器人拉进**外部群**做只读问答前,先在飞书开发者后台开启应用的「可被添加到外部群 / 外部可用范围」,再由群里的真人手动把机器人加进群(机器人无法自行加入)。
263
+
255
264
  ---
256
265
 
257
266
  ## ❓ 故障排查
package/dist/cli.js CHANGED
@@ -1442,7 +1442,28 @@ function mapItemComplete(item) {
1442
1442
 
1443
1443
  // src/agent/codex-appserver/backend.ts
1444
1444
  var APPROVAL_POLICY = "never";
1445
- var SANDBOX = "danger-full-access";
1445
+ function sandboxParams(mode, network) {
1446
+ if ((mode ?? "full") === "full") return { sandbox: "danger-full-access" };
1447
+ if (process.platform !== "darwin" && process.platform !== "win32") {
1448
+ throw new Error(
1449
+ "\u300C\u9879\u76EE\u5185\u53EA\u8BFB / \u9879\u76EE\u5185\u8BFB\u5199\u300D\u9760\u64CD\u4F5C\u7CFB\u7EDF\u6C99\u7BB1\u628A\u8BFB\u5199\u9501\u8FDB\u9879\u76EE\u6587\u4EF6\u5939\uFF0C\u76EE\u524D\u53EA\u6709 macOS \u4E0E\u539F\u751F Windows \u80FD\u5F3A\u5236\u6267\u884C\u3002\u5F53\u524D\u5E73\u53F0\uFF08Linux / WSL \u53EA\u6321\u5199\u3001\u4E0D\u9650\u5236\u8BFB\u53D6\uFF0C\u65E0\u6CD5\u4FDD\u8BC1\u4E0D\u6CC4\u9732\u9690\u79C1\uFF09\u5DF2\u62D2\u7EDD\u542F\u52A8\uFF08\u7EDD\u4E0D\u964D\u7EA7\u4E3A\u5B8C\u5168\u8BBF\u95EE\uFF09\u3002\u8BF7\u6539\u7528\u300C\u5B8C\u5168\u8BBF\u95EE\u300D\u3001\u628A Codex \u8DD1\u8FDB\u5BB9\u5668/\u9694\u79BB\u73AF\u5883\uFF0C\u6216\u5728 macOS / Windows \u4E0A\u8FD0\u884C\u3002"
1450
+ );
1451
+ }
1452
+ return {
1453
+ config: {
1454
+ default_permissions: "feishu",
1455
+ permissions: {
1456
+ feishu: {
1457
+ filesystem: {
1458
+ ":minimal": "read",
1459
+ ":workspace_roots": { ".": mode === "write" ? "write" : "read" }
1460
+ },
1461
+ network: { enabled: Boolean(network) }
1462
+ }
1463
+ }
1464
+ }
1465
+ };
1466
+ }
1446
1467
  var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1447
1468
  "\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",
1448
1469
  "",
@@ -1627,23 +1648,25 @@ var CodexAppServerBackend = class {
1627
1648
  }
1628
1649
  }
1629
1650
  async startThread(opts) {
1651
+ const sandbox = sandboxParams(opts.mode, opts.network);
1630
1652
  const client = await this.spawn(opts.cwd);
1631
1653
  const res = await client.request("thread/start", {
1632
1654
  cwd: opts.cwd,
1633
1655
  approvalPolicy: APPROVAL_POLICY,
1634
- sandbox: SANDBOX,
1656
+ ...sandbox,
1635
1657
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1636
1658
  ...opts.model ? { model: opts.model } : {}
1637
1659
  });
1638
1660
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1639
1661
  }
1640
1662
  async resumeThread(opts) {
1663
+ const sandbox = sandboxParams(opts.mode, opts.network);
1641
1664
  const client = await this.spawn(opts.cwd);
1642
1665
  const res = await client.request("thread/resume", {
1643
1666
  threadId: opts.codexThreadId,
1644
1667
  cwd: opts.cwd,
1645
1668
  approvalPolicy: APPROVAL_POLICY,
1646
- sandbox: SANDBOX,
1669
+ ...sandbox,
1647
1670
  developerInstructions: BRIDGE_DEVELOPER_INSTRUCTIONS,
1648
1671
  ...opts.model ? { model: opts.model } : {}
1649
1672
  });
@@ -2049,6 +2072,7 @@ var RunRender = class {
2049
2072
  // src/card/cards.ts
2050
2073
  function card(elements, opts = {}) {
2051
2074
  const config = { update_multi: true };
2075
+ if (opts.forward === false) config.enable_forward = false;
2052
2076
  if (opts.streaming) {
2053
2077
  config.streaming_mode = true;
2054
2078
  config.streaming_config = {
@@ -2194,6 +2218,7 @@ function selectMenu(opts) {
2194
2218
  tag: "select_static",
2195
2219
  name: opts.name,
2196
2220
  placeholder: { tag: "plain_text", content: opts.placeholder },
2221
+ ...opts.initial ? { initial_option: opts.initial } : {},
2197
2222
  options: opts.options.map((o) => ({ text: { tag: "plain_text", content: o.label }, value: o.value }))
2198
2223
  };
2199
2224
  }
@@ -3138,6 +3163,21 @@ import { dirname as dirname5 } from "path";
3138
3163
  function defaultNoMention(p) {
3139
3164
  return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
3140
3165
  }
3166
+ function effectiveMode(p) {
3167
+ return p.mode ?? "full";
3168
+ }
3169
+ function effectiveGuestMode(p) {
3170
+ return p.guestMode ?? effectiveMode(p);
3171
+ }
3172
+ function turnTier(p, isAdminSender) {
3173
+ const adminTier = effectiveMode(p);
3174
+ const guestTier = effectiveGuestMode(p);
3175
+ return {
3176
+ mode: isAdminSender ? adminTier : guestTier,
3177
+ role: isAdminSender ? "admin" : "guest",
3178
+ split: guestTier !== adminTier
3179
+ };
3180
+ }
3141
3181
  var FILE_VERSION2 = 1;
3142
3182
  async function read() {
3143
3183
  try {
@@ -3229,6 +3269,12 @@ var DM = {
3229
3269
  reconnect: "dm.reconnect",
3230
3270
  update: "dm.update",
3231
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",
3232
3278
  rmConfirm: "dm.rmConfirm",
3233
3279
  rmDo: "dm.rmDo",
3234
3280
  rmCancel: "dm.rmCancel",
@@ -3247,7 +3293,10 @@ var DM = {
3247
3293
  rmAllowed: "dm.allow.rm",
3248
3294
  // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3249
3295
  projectSettings: "dm.projectSettings",
3250
- setNoMentionDm: "dm.proj.noMention"
3296
+ setNoMentionDm: "dm.proj.noMention",
3297
+ // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3298
+ permission: "dm.proj.perm",
3299
+ permissionSubmit: "dm.proj.perm.submit"
3251
3300
  };
3252
3301
  var GS = {
3253
3302
  setNoMention: "gs.noMention"
@@ -3266,6 +3315,7 @@ function buildDmMenuCard() {
3266
3315
  button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.settings })
3267
3316
  ]),
3268
3317
  actions([
3318
+ button("\u{1F4CA} \u7528\u91CF", { a: DM.usage }),
3269
3319
  button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
3270
3320
  button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect }),
3271
3321
  button("\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", { a: DM.update })
@@ -3689,6 +3739,56 @@ function buildAddAdminCard(members) {
3689
3739
  { header: { title: "\u2795 \u6DFB\u52A0\u7BA1\u7406\u5458", template: "blue" } }
3690
3740
  );
3691
3741
  }
3742
+ var MODE_OPTS = [
3743
+ { value: "qa", label: "\u{1F512} \u9879\u76EE\u5185\u53EA\u8BFB", desc: "\u53EA\u80FD\u67E5\u770B\u9879\u76EE\u6587\u4EF6\u5939\u91CC\u7684\u5185\u5BB9\uFF0C\u4E0D\u4F1A\u6539\u4EFB\u4F55\u6587\u4EF6" },
3744
+ { value: "write", label: "\u270F\uFE0F \u9879\u76EE\u5185\u8BFB\u5199", desc: "\u80FD\u67E5\u770B\u5E76\u4FEE\u6539\u9879\u76EE\u6587\u4EF6\u5939\u91CC\u7684\u6587\u4EF6\uFF0C\u4F46\u78B0\u4E0D\u5230\u6587\u4EF6\u5939\u5916" },
3745
+ { value: "full", label: "\u26A0\uFE0F \u5B8C\u5168\u8BBF\u95EE", desc: "\u80FD\u8BFB\u5199\u6574\u53F0\u7535\u8111\u4E0A\u7684\u4EFB\u4F55\u6587\u4EF6" }
3746
+ ];
3747
+ function tierLabel(m) {
3748
+ return MODE_OPTS.find((o) => o.value === m)?.label ?? m;
3749
+ }
3750
+ var TIER_SELECT_OPTS = MODE_OPTS.map((o) => ({ label: `${o.label} \u2014 ${o.desc}`, value: o.value }));
3751
+ function permissionSummary(p) {
3752
+ const admin = effectiveMode(p);
3753
+ const guest = effectiveGuestMode(p);
3754
+ return admin === guest ? `\u6240\u6709\u4EBA\uFF1A${tierLabel(admin)}` : `\u7BA1\u7406\u5458\uFF1A${tierLabel(admin)}\u3000\xB7\u3000\u5176\u4ED6\u4EBA\uFF1A${tierLabel(guest)}`;
3755
+ }
3756
+ function buildPermissionCard(p) {
3757
+ const network = p.network ?? false;
3758
+ return card(
3759
+ [
3760
+ md(`**\u{1F510} \u6743\u9650** \xB7 ${p.name}`),
3761
+ note(
3762
+ "codex \u6C99\u7BB1\u7684\u8BBF\u95EE\u8303\u56F4\u3002\u300C\u7BA1\u7406\u5458\u6863\u300D\u7ED9 owner / \u7BA1\u7406\u5458\uFF0C\u300C\u666E\u901A\u7528\u6237\u6863\u300D\u7ED9\u7FA4\u91CC\u5176\u4ED6\u4EBA\u3002\u4E24\u6863**\u4E0D\u540C**\u65F6\uFF0C\u4E24\u7C7B\u4EBA\u5404\u7528\u72EC\u7ACB\u7EBF\u7A0B\uFF08\u4E92\u4E0D\u4E32\u6C99\u7BB1\u4E0E\u5BF9\u8BDD\u5386\u53F2\uFF09\uFF1B**\u76F8\u540C**\u5219\u6240\u6709\u4EBA\u4E00\u81F4\u3002"
3763
+ ),
3764
+ form("perm", [
3765
+ md("\u{1F451} **\u7BA1\u7406\u5458\u6863**"),
3766
+ selectMenu({ name: "mode", placeholder: "\u9009\u62E9\u7BA1\u7406\u5458\u6743\u9650\u6863", options: TIER_SELECT_OPTS, initial: effectiveMode(p) }),
3767
+ md("\u{1F465} **\u666E\u901A\u7528\u6237\u6863**"),
3768
+ selectMenu({
3769
+ name: "guestMode",
3770
+ placeholder: "\u9009\u62E9\u666E\u901A\u7528\u6237\u6743\u9650\u6863",
3771
+ options: TIER_SELECT_OPTS,
3772
+ initial: effectiveGuestMode(p)
3773
+ }),
3774
+ md("\u{1F310} **\u8054\u7F51**\uFF08\u53EA\u5BF9\u53EA\u8BFB / \u8BFB\u5199\u6863\u6709\u610F\u4E49\uFF1B\u5B8C\u5168\u8BBF\u95EE\u6052\u8054\u7F51\uFF09"),
3775
+ selectMenu({
3776
+ name: "network",
3777
+ placeholder: "\u8054\u7F51\u5F00\u5173",
3778
+ options: [
3779
+ { label: "\u5173\uFF08\u9ED8\u8BA4\uFF0C\u66F4\u5B89\u5168\uFF09", value: "off" },
3780
+ { label: "\u5F00", value: "on" }
3781
+ ],
3782
+ initial: network ? "on" : "off"
3783
+ }),
3784
+ actions([submitButton("\u2705 \u4FDD\u5B58\u6743\u9650", { a: DM.permissionSubmit, n: p.name }, "primary", "submit_perm")])
3785
+ ]),
3786
+ note("\u4FDD\u5B58\u4F1A\u65AD\u5F00\u672C\u9879\u76EE\u6B63\u5728\u8FDB\u884C\u7684\u4F1A\u8BDD\uFF0C\u8BA9\u65B0\u6863\u4F4D\u7ACB\u5373\u751F\u6548\u3002"),
3787
+ actions([button("\u2B05\uFE0F \u8FD4\u56DE\u8BBE\u7F6E", { a: DM.projectSettings, n: p.name })])
3788
+ ],
3789
+ { header: { title: "\u{1F510} \u6743\u9650", template: "blue" } }
3790
+ );
3791
+ }
3692
3792
  function buildProjectSettingsCard(project) {
3693
3793
  const kind = project.kind ?? "multi";
3694
3794
  const noMention = project.noMention ?? defaultNoMention(project);
@@ -3697,6 +3797,9 @@ function buildProjectSettingsCard(project) {
3697
3797
  md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
3698
3798
  note(`${kindLabel(kind)}${project.cwd ? ` \xB7 \u{1F4C2} \`${project.cwd}\`` : ""}`),
3699
3799
  hr(),
3800
+ actions([button("\u{1F510} \u6743\u9650", { a: DM.permission, n: project.name }, "primary")]),
3801
+ note(`\u5F53\u524D ${permissionSummary(project)}\u3000\xB7\u3000codex \u6C99\u7BB1\u53EF\u8BBF\u95EE\u7684\u8303\u56F4\uFF08\u7BA1\u7406\u5458 / \u666E\u901A\u7528\u6237\u53EF\u5206\u8BBE\uFF09\u3002`),
3802
+ hr(),
3700
3803
  md("\u270B \u514D@\uFF08\u4E0D\u7528 @ \u4E5F\u56DE\u590D\uFF09"),
3701
3804
  actions([
3702
3805
  button("\u5F00", { a: DM.setNoMentionDm, v: "on", n: project.name }, noMention ? "primary" : "default"),
@@ -4382,10 +4485,615 @@ async function restartDaemon() {
4382
4485
  await getServiceAdapter().restart();
4383
4486
  }
4384
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
+
4385
5093
  // src/project/lifecycle.ts
4386
5094
  import { mkdir as mkdir9 } from "fs/promises";
4387
5095
  import { existsSync as existsSync7 } from "fs";
4388
- 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";
4389
5097
 
4390
5098
  // src/project/git-info.ts
4391
5099
  import { execFile } from "child_process";
@@ -4527,7 +5235,7 @@ async function resolveCwd(name, existingPath) {
4527
5235
  if (!existsSync7(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
4528
5236
  return { cwd: cwd2, blank: false };
4529
5237
  }
4530
- const cwd = join12(paths.projectsRootDir, name);
5238
+ const cwd = join13(paths.projectsRootDir, name);
4531
5239
  await mkdir9(cwd, { recursive: true });
4532
5240
  return { cwd, blank: true };
4533
5241
  }
@@ -4547,9 +5255,19 @@ async function createProject(channel, input2) {
4547
5255
  params: { member_id_type: "open_id" },
4548
5256
  data: { manager_ids: [input2.ownerOpenId] }
4549
5257
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
4550
- const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi", origin: "created" };
5258
+ const project = {
5259
+ name,
5260
+ chatId,
5261
+ cwd,
5262
+ blank,
5263
+ createdAt: Date.now(),
5264
+ kind: input2.kind ?? "multi",
5265
+ origin: "created",
5266
+ mode: input2.mode ?? "full",
5267
+ network: input2.network ?? false
5268
+ };
4551
5269
  await addProject(project);
4552
- log.info("project", "create", { name, chatId, cwd, blank });
5270
+ log.info("project", "create", { name, chatId, cwd, blank, mode: project.mode });
4553
5271
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
4554
5272
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
4555
5273
  return project;
@@ -4569,10 +5287,12 @@ async function joinExistingGroup(channel, input2) {
4569
5287
  createdAt: Date.now(),
4570
5288
  kind: input2.kind ?? "multi",
4571
5289
  origin: "joined",
4572
- addedBy: input2.addedBy
5290
+ addedBy: input2.addedBy,
5291
+ mode: input2.mode ?? "qa",
5292
+ network: input2.network ?? false
4573
5293
  };
4574
5294
  await addProject(project);
4575
- log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind });
5295
+ log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind, mode: project.mode });
4576
5296
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard-join" }));
4577
5297
  return project;
4578
5298
  }
@@ -4595,12 +5315,12 @@ async function leaveChat(channel, chatId) {
4595
5315
  }
4596
5316
 
4597
5317
  // src/bot/session-store.ts
4598
- 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";
4599
5319
  import { dirname as dirname10 } from "path";
4600
5320
  var FILE_VERSION3 = 1;
4601
5321
  async function read2() {
4602
5322
  try {
4603
- const text = await readFile8(paths.sessionsFile, "utf8");
5323
+ const text = await readFile9(paths.sessionsFile, "utf8");
4604
5324
  const parsed = JSON.parse(text);
4605
5325
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
4606
5326
  } catch (err) {
@@ -4657,7 +5377,7 @@ async function handleDmConsole(channel, cfg, msg) {
4657
5377
 
4658
5378
  // src/bot/media.ts
4659
5379
  import { mkdir as mkdir11, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
4660
- import { join as join13 } from "path";
5380
+ import { join as join14 } from "path";
4661
5381
  var MAX_IMAGES2 = 9;
4662
5382
  var MEDIA_TTL_MS = 60 * 6e4;
4663
5383
  var EXT_BY_CONTENT_TYPE = {
@@ -4762,7 +5482,7 @@ async function downloadOne(channel, ref, index) {
4762
5482
  params: { type: "image" }
4763
5483
  });
4764
5484
  const ext = extFromHeaders(res.headers);
4765
- const file = join13(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
5485
+ const file = join14(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
4766
5486
  await res.writeFile(file);
4767
5487
  return file;
4768
5488
  } catch (err) {
@@ -4796,7 +5516,7 @@ async function pruneOldMedia() {
4796
5516
  }
4797
5517
  const cutoff = Date.now() - MEDIA_TTL_MS;
4798
5518
  for (const name of entries) {
4799
- const file = join13(paths.mediaDir, name);
5519
+ const file = join14(paths.mediaDir, name);
4800
5520
  try {
4801
5521
  const st = await stat3(file);
4802
5522
  if (st.mtimeMs < cutoff) await rm4(file, { force: true });
@@ -5095,6 +5815,21 @@ function pickOpenId(formValue) {
5095
5815
  }
5096
5816
  return void 0;
5097
5817
  }
5818
+ function selectValue(formValue, name) {
5819
+ const c = (() => {
5820
+ const raw = formValue?.[name];
5821
+ return Array.isArray(raw) ? raw[0] : raw;
5822
+ })();
5823
+ if (typeof c === "string") return c;
5824
+ if (c && typeof c === "object") {
5825
+ const o = c;
5826
+ for (const v of [o.value, o.id]) if (typeof v === "string") return v;
5827
+ }
5828
+ return void 0;
5829
+ }
5830
+ function asTier(v) {
5831
+ return v === "qa" || v === "write" || v === "full" ? v : void 0;
5832
+ }
5098
5833
  function createOrchestrator(channel, cfg, fallbackCwd) {
5099
5834
  const backend = createBackend();
5100
5835
  const sessions = /* @__PURE__ */ new Map();
@@ -5198,11 +5933,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5198
5933
  await postGroupSettings(msg, project);
5199
5934
  return;
5200
5935
  }
5936
+ const ts = turnSession(msg.chatId, project, msg.senderId);
5201
5937
  if (cmd === "model") {
5202
- await postModelCard(msg, msg.chatId);
5938
+ await postModelCard(msg, ts.sessionKey);
5203
5939
  return;
5204
5940
  }
5205
- handleTurn(msg, text, msg.chatId, true, project);
5941
+ handleTurn(msg, text, ts.sessionKey, true, project, ts);
5206
5942
  return;
5207
5943
  }
5208
5944
  if (msg.threadId) {
@@ -5210,11 +5946,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5210
5946
  await postHelpCard(msg, "topic", true, project);
5211
5947
  return;
5212
5948
  }
5949
+ const ts = turnSession(msg.threadId, project, msg.senderId);
5213
5950
  if (cmd === "model") {
5214
- await postModelCard(msg, msg.threadId);
5951
+ await postModelCard(msg, ts.sessionKey);
5215
5952
  return;
5216
5953
  }
5217
- handleTurn(msg, text, msg.threadId, false, project);
5954
+ handleTurn(msg, text, ts.sessionKey, false, project, ts);
5218
5955
  return;
5219
5956
  }
5220
5957
  if (cmd === "help") {
@@ -5264,13 +6001,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5264
6001
  log.info("card", "group-settings", { project: project.name });
5265
6002
  });
5266
6003
  }
5267
- async function handleTurn(msg, text, sessionKey, flat, project) {
6004
+ function turnPerm(project, senderId) {
6005
+ if (!project) return {};
6006
+ const t = turnTier(project, isAdmin(cfg, senderId));
6007
+ return { mode: t.mode, network: project.network, roleSuffix: t.split ? t.role : void 0 };
6008
+ }
6009
+ function turnSession(baseKey, project, senderId) {
6010
+ const perm = turnPerm(project, senderId);
6011
+ return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
6012
+ }
6013
+ async function handleTurn(msg, text, sessionKey, flat, project, perm) {
5268
6014
  const existing = active.get(sessionKey);
5269
6015
  if (existing) {
5270
6016
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
5271
6017
  const cur = active.get(sessionKey);
5272
6018
  if (!cur) {
5273
- startReservedRun(msg, text, sessionKey, flat, project, images);
6019
+ startReservedRun(msg, text, sessionKey, flat, project, perm, images);
5274
6020
  return;
5275
6021
  }
5276
6022
  if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
@@ -5289,9 +6035,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5289
6035
  log.info("intake", "queued", { depth: cur.queue.length });
5290
6036
  return;
5291
6037
  }
5292
- startReservedRun(msg, text, sessionKey, flat, project);
6038
+ startReservedRun(msg, text, sessionKey, flat, project, perm);
5293
6039
  }
5294
- function startReservedRun(msg, text, sessionKey, flat, project, preloadedImages) {
6040
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
5295
6041
  const existing = active.get(sessionKey);
5296
6042
  if (existing) {
5297
6043
  existing.queue.push({ text, images: preloadedImages });
@@ -5304,10 +6050,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5304
6050
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5305
6051
  try {
5306
6052
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
5307
- let thread = await resolveThread(sessionKey, msg.chatId);
6053
+ let thread = await resolveThread(sessionKey, msg.chatId, { mode: perm.mode, network: perm.network });
5308
6054
  if (!thread) {
5309
6055
  const cwd = project?.cwd ?? fallbackCwd;
5310
- thread = await backend.startThread({ cwd });
6056
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
5311
6057
  sessions.set(sessionKey, thread);
5312
6058
  await upsertSession({
5313
6059
  threadId: sessionKey,
@@ -5342,7 +6088,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5342
6088
  }
5343
6089
  });
5344
6090
  }
5345
- async function resolveThread(threadId, chatId) {
6091
+ async function resolveThread(threadId, chatId, perm) {
5346
6092
  const live = sessions.get(threadId);
5347
6093
  if (live) return live;
5348
6094
  const rec = await getSession(threadId);
@@ -5352,7 +6098,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5352
6098
  cwd: rec.cwd,
5353
6099
  codexThreadId: rec.codexThreadId,
5354
6100
  model: rec.model,
5355
- effort: rec.effort
6101
+ effort: rec.effort,
6102
+ mode: perm?.mode,
6103
+ network: perm?.network
5356
6104
  });
5357
6105
  sessions.set(threadId, resumed);
5358
6106
  return resumed;
@@ -5360,20 +6108,39 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5360
6108
  log.fail("agent", err, { phase: "resume-on-turn", threadId });
5361
6109
  const project = await getProjectByChatId(chatId);
5362
6110
  const cwd = project?.cwd ?? rec.cwd ?? fallbackCwd;
5363
- const fresh = await backend.startThread({ cwd, model: rec.model, effort: rec.effort });
6111
+ const fresh = await backend.startThread({
6112
+ cwd,
6113
+ model: rec.model,
6114
+ effort: rec.effort,
6115
+ mode: perm?.mode ?? project?.mode,
6116
+ network: perm?.network ?? project?.network
6117
+ });
5364
6118
  sessions.set(threadId, fresh);
5365
6119
  return fresh;
5366
6120
  }
5367
6121
  }
6122
+ async function evictLiveSessionsForChat(chatId) {
6123
+ let closed = 0;
6124
+ for (const rec of await listSessions()) {
6125
+ if (rec.chatId !== chatId) continue;
6126
+ const live = sessions.get(rec.threadId);
6127
+ if (!live) continue;
6128
+ sessions.delete(rec.threadId);
6129
+ void live.close().catch(() => void 0);
6130
+ closed++;
6131
+ }
6132
+ if (closed) log.info("console", "tier-evict", { chatId, closed });
6133
+ }
5368
6134
  function startTopicDirectly(msg, text, project) {
5369
6135
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
5370
6136
  const reaction = runReaction(msg.messageId, !sema.hasFree());
5371
6137
  const cwd = project?.cwd ?? fallbackCwd;
6138
+ const perm = turnPerm(project, msg.senderId);
5372
6139
  if (project) void refreshBranch(channel, project).catch(() => void 0);
5373
6140
  const { model, effort } = pickDefault(await listModels());
5374
6141
  let thread;
5375
6142
  try {
5376
- thread = await backend.startThread({ cwd, model, effort });
6143
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
5377
6144
  } catch (err) {
5378
6145
  reaction.done();
5379
6146
  log.fail("card", err, { phase: "start-topic" });
@@ -5395,7 +6162,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5395
6162
  effort,
5396
6163
  cwd,
5397
6164
  summary: text.slice(0, 80) || "(\u7A7A)",
5398
- requesterOpenId: msg.senderId
6165
+ requesterOpenId: msg.senderId,
6166
+ roleSuffix: perm.roleSuffix
5399
6167
  },
5400
6168
  reaction,
5401
6169
  () => reaction.done()
@@ -5559,6 +6327,46 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
5559
6327
  const freshMenu = (evt) => {
5560
6328
  patch(evt, buildDmMenuCard());
5561
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
+ };
5562
6370
  const renderProjectList = async () => {
5563
6371
  const [projects, sessions2] = await Promise.all([listProjects(), listSessions()]);
5564
6372
  const byChat = /* @__PURE__ */ new Map();
@@ -5712,6 +6520,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
5712
6520
  await restartDaemon().catch((e) => log.fail("console", e, { phase: "update-restart" }));
5713
6521
  }
5714
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
+ })();
5715
6542
  }).on(DM.rmConfirm, async ({ evt, value }) => {
5716
6543
  const name = typeof value.n === "string" ? value.n : void 0;
5717
6544
  if (!dmAdmin(evt.operator?.openId) || !name) return;
@@ -5872,6 +6699,31 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5872
6699
  await updateProject(name, { noMention: on });
5873
6700
  return buildProjectSettingsCard({ ...p, noMention: on });
5874
6701
  });
6702
+ }).on(DM.permission, ({ evt, value }) => {
6703
+ if (!dmAdmin(evt.operator?.openId)) return;
6704
+ const name = typeof value.n === "string" ? value.n : "";
6705
+ patch(evt, async () => {
6706
+ const p = await getProjectByName(name);
6707
+ return p ? buildPermissionCard(p) : buildDmMenuCard();
6708
+ });
6709
+ }).on(DM.permissionSubmit, ({ evt, value, formValue }) => {
6710
+ if (!dmAdmin(evt.operator?.openId)) return;
6711
+ const name = typeof value.n === "string" ? value.n : "";
6712
+ const mode = asTier(selectValue(formValue, "mode"));
6713
+ const guestMode = asTier(selectValue(formValue, "guestMode"));
6714
+ const network = selectValue(formValue, "network") === "on";
6715
+ void (async () => {
6716
+ const p = await getProjectByName(name);
6717
+ if (!p) return;
6718
+ await updateProject(name, { ...mode ? { mode } : {}, ...guestMode ? { guestMode } : {}, network });
6719
+ await evictLiveSessionsForChat(p.chatId);
6720
+ log.info("console", "permission", { project: name, mode, guestMode, network });
6721
+ const fresh = await getProjectByName(name);
6722
+ if (!fresh) return;
6723
+ await sendManagedCard(channel, evt.chatId, buildProjectSettingsCard(fresh)).catch(
6724
+ (e) => log.fail("console", e, { phase: "permission-result" })
6725
+ );
6726
+ })();
5875
6727
  });
5876
6728
  async function resumeFromCard(evt, state, codexThreadId) {
5877
6729
  try {
@@ -5973,13 +6825,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5973
6825
  if (activeKey.startsWith("pending:")) {
5974
6826
  const tid = await getThreadId(channel, messageId);
5975
6827
  if (tid) {
6828
+ const key = opts.roleSuffix ? `${tid}#${opts.roleSuffix}` : tid;
5976
6829
  active.delete(activeKey);
5977
- active.set(tid, state);
5978
- sessions.set(tid, opts.thread);
5979
- activeKey = tid;
5980
- topicThreadId = tid;
5981
- rc.threadId = tid;
5982
- await persist(tid);
6830
+ active.set(key, state);
6831
+ sessions.set(key, opts.thread);
6832
+ activeKey = key;
6833
+ topicThreadId = key;
6834
+ rc.threadId = key;
6835
+ await persist(key);
5983
6836
  }
5984
6837
  } else {
5985
6838
  topicThreadId = activeKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.2",
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": {