@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.
- package/README.md +20 -11
- package/dist/cli.js +889 -36
- 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
|
-
| **操作系统** |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 = {
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
5938
|
+
await postModelCard(msg, ts.sessionKey);
|
|
5203
5939
|
return;
|
|
5204
5940
|
}
|
|
5205
|
-
handleTurn(msg, text,
|
|
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,
|
|
5951
|
+
await postModelCard(msg, ts.sessionKey);
|
|
5215
5952
|
return;
|
|
5216
5953
|
}
|
|
5217
|
-
handleTurn(msg, text,
|
|
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
|
-
|
|
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({
|
|
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(
|
|
5978
|
-
sessions.set(
|
|
5979
|
-
activeKey =
|
|
5980
|
-
topicThreadId =
|
|
5981
|
-
rc.threadId =
|
|
5982
|
-
await persist(
|
|
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;
|