@modelzen/feishu-codex-bridge 0.3.3 → 0.3.5
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 +5 -2
- package/dist/cli.js +786 -19
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
- **流式卡片**:推理 / 命令 / 文件改动 / 结果实时刷新到一张可折叠卡片。
|
|
31
31
|
- **免 @ 对话**:项目群话题内可直接说话、不必每次 @(可逐群开关)。
|
|
32
32
|
- **文档评论回复(可选)**:在飞书云文档(doc/docx/sheet/file,含知识库 wiki)的评论里 **@机器人**,它会读评论、跑 Codex、把答案回到同一条评论线程里;每篇文档一条连续会话。需额外开通文档评论权限并订阅评论事件(见下方配置)。
|
|
33
|
-
- **私聊控制台**:私聊机器人弹交互菜单 ——
|
|
33
|
+
- **私聊控制台**:私聊机器人弹交互菜单 —— 新建项目、项目列表、设置、用量、诊断、重连。
|
|
34
|
+
- **📊 Codex 用量**:5 小时 / 7 天限额进度(剩余 % + 重置时间)、lifetime tokens、连续使用天数、GitHub 风格每日用量热力图;一键生成**战绩分享卡**,可原生转发给任何人或群(数据来自 Codex 个人资料页同款接口,需 ChatGPT 登录)。
|
|
34
35
|
- **稳定隔离**:每会话独立 app-server 进程;卡死有 watchdog(默认 120s)→ 终止 → 回收,异常不波及其他群。
|
|
35
36
|
- **本地加密密钥库**:飞书应用密钥用 AES-256-GCM 存在 `~/.feishu-codex-bridge/`,不入仓库、不进环境变量。
|
|
36
37
|
- **跨平台常驻**:macOS / Windows / Linux·WSL 均可注册成后台服务、开机或登录自启(分别走 launchd / 登录自启免管理员 / systemd)。注:跨平台指进程运行与后台自启;「项目内只读/读写」隐私沙箱仅 macOS / 原生 Windows 可强制(见[安全须知](#-安全须知))。
|
|
@@ -164,9 +165,11 @@ feishu-codex-bridge bot rm <名> # 移除一个机器人配置
|
|
|
164
165
|
- **💬 单会话群**:整群就是**一条连续会话**(全程**免 @**、消息按序排队、无 `/resume`)。适合**个人单线深入**、像私聊一样直接聊。
|
|
165
166
|
- **干活**:在项目群里 **@机器人** 描述需求;机器人在该群绑定的目录里跑 Codex,流式卡片回结果。
|
|
166
167
|
- **话题 = 会话**:对某条消息开话题后,话题内可**免 @** 连续对话,是一条连贯的 Codex 会话。
|
|
168
|
+
- **发图 / 发附件**:直接在消息里**发图片**,Codex 能看到(多模态读图);**发文件附件**(日志 / PDF / 代码等),桥会把它下载到本地并把**绝对路径**告诉 Codex,让它用工具直接打开分析。⚠️ 附件落在桥的全局临时目录(`~/.feishu-codex-bridge/inbound`,1h 后自动清),**只有「完全访问」档**能读到——「项目内只读 / 读写」档的沙箱把读取锁在项目目录内,读不到该目录。单文件上限 50MB、单条消息最多 9 个;合并转发里的附件飞书官方不支持取,故不支持。
|
|
167
169
|
- **文档评论 @机器人**:在飞书文档评论里 @ 它就回(前提:已开通文档评论权限 + 订阅 `drive.notice.comment_add_v1`,且机器人对该文档有访问权限)。只支持 doc/docx/sheet/file;评论框不渲染 markdown,回复为纯文本,超长会截断。
|
|
168
170
|
- **终止**:卡片上的 **⏹** 随时终止当前轮;卡死超过 watchdog 阈值(默认 120s)自动中止并回收进程。
|
|
169
|
-
- **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog /
|
|
171
|
+
- **私聊控制台**:项目列表、设置(模型 / 推理强度 / 免 @ / watchdog / 管理员)、用量、诊断、重连,全在私聊菜单里。
|
|
172
|
+
- **📊 用量**:点「用量」看 5h/7d 限额(剩余 % + 重置时间)与 Codex 个人统计(lifetime tokens / streak / 每日热力图);点「📤 生成分享卡」得到一张可转发的战绩卡——长按(手机)或右键(电脑)即可转发,数据定格在生成时刻。
|
|
170
173
|
|
|
171
174
|
---
|
|
172
175
|
|
package/dist/cli.js
CHANGED
|
@@ -68,7 +68,10 @@ var paths = {
|
|
|
68
68
|
* passes lark-cli's AssertSecurePath audit.
|
|
69
69
|
*/
|
|
70
70
|
secretsGetterScript: join(appDir, "secrets-getter"),
|
|
71
|
-
mediaDir: join(appDir, "media")
|
|
71
|
+
mediaDir: join(appDir, "media"),
|
|
72
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
73
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
74
|
+
inboundDir: join(appDir, "inbound")
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
// src/config/bots.ts
|
|
@@ -2072,6 +2075,7 @@ var RunRender = class {
|
|
|
2072
2075
|
// src/card/cards.ts
|
|
2073
2076
|
function card(elements, opts = {}) {
|
|
2074
2077
|
const config = { update_multi: true };
|
|
2078
|
+
if (opts.forward === false) config.enable_forward = false;
|
|
2075
2079
|
if (opts.streaming) {
|
|
2076
2080
|
config.streaming_mode = true;
|
|
2077
2081
|
config.streaming_config = {
|
|
@@ -3268,6 +3272,12 @@ var DM = {
|
|
|
3268
3272
|
reconnect: "dm.reconnect",
|
|
3269
3273
|
update: "dm.update",
|
|
3270
3274
|
updateDo: "dm.update.do",
|
|
3275
|
+
// 📊 Codex 用量(限额 + 个人资料统计 + 热力图);share 打开内容选择卡,
|
|
3276
|
+
// shareDo 按所选区块生成可转发的分享卡
|
|
3277
|
+
usage: "dm.usage",
|
|
3278
|
+
usageRefresh: "dm.usage.refresh",
|
|
3279
|
+
usageShare: "dm.usage.share",
|
|
3280
|
+
usageShareDo: "dm.usage.share.do",
|
|
3271
3281
|
rmConfirm: "dm.rmConfirm",
|
|
3272
3282
|
rmDo: "dm.rmDo",
|
|
3273
3283
|
rmCancel: "dm.rmCancel",
|
|
@@ -3308,6 +3318,7 @@ function buildDmMenuCard() {
|
|
|
3308
3318
|
button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.settings })
|
|
3309
3319
|
]),
|
|
3310
3320
|
actions([
|
|
3321
|
+
button("\u{1F4CA} \u7528\u91CF", { a: DM.usage }),
|
|
3311
3322
|
button("\u{1FA7A} \u8BCA\u65AD", { a: DM.doctor }),
|
|
3312
3323
|
button("\u{1F504} \u91CD\u8FDE", { a: DM.reconnect }),
|
|
3313
3324
|
button("\u2B06\uFE0F \u7248\u672C\u66F4\u65B0", { a: DM.update })
|
|
@@ -4477,10 +4488,615 @@ async function restartDaemon() {
|
|
|
4477
4488
|
await getServiceAdapter().restart();
|
|
4478
4489
|
}
|
|
4479
4490
|
|
|
4491
|
+
// src/agent/codex-appserver/usage.ts
|
|
4492
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
4493
|
+
import { homedir as homedir5 } from "os";
|
|
4494
|
+
import { join as join12 } from "path";
|
|
4495
|
+
var DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
|
|
4496
|
+
var HTTP_TIMEOUT_MS = 15e3;
|
|
4497
|
+
var REFRESH_TIMEOUT_MS = 2e4;
|
|
4498
|
+
var EXP_SKEW_MS = 6e4;
|
|
4499
|
+
var PROFILE_CACHE_MS = 5 * 6e4;
|
|
4500
|
+
var USAGE_CACHE_MS = 3e4;
|
|
4501
|
+
var UsageError = class extends Error {
|
|
4502
|
+
constructor(kind, message) {
|
|
4503
|
+
super(message);
|
|
4504
|
+
this.kind = kind;
|
|
4505
|
+
this.name = "UsageError";
|
|
4506
|
+
}
|
|
4507
|
+
kind;
|
|
4508
|
+
};
|
|
4509
|
+
function resolveCodexHome() {
|
|
4510
|
+
return process.env.CODEX_HOME ?? join12(homedir5(), ".codex");
|
|
4511
|
+
}
|
|
4512
|
+
async function readCodexAuth() {
|
|
4513
|
+
const file = join12(resolveCodexHome(), "auth.json");
|
|
4514
|
+
let lastErr;
|
|
4515
|
+
for (let i = 0; i < 3; i++) {
|
|
4516
|
+
let raw;
|
|
4517
|
+
try {
|
|
4518
|
+
raw = await readFile8(file, "utf8");
|
|
4519
|
+
} catch (err) {
|
|
4520
|
+
throw new UsageError("no-auth", `\u8BFB\u4E0D\u5230 ${file}\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
4521
|
+
}
|
|
4522
|
+
try {
|
|
4523
|
+
const j = JSON.parse(raw);
|
|
4524
|
+
const accessToken = j.tokens?.access_token;
|
|
4525
|
+
if (!accessToken) throw new UsageError("api-key-mode", "auth.json \u6CA1\u6709 ChatGPT access_token\uFF08API-key \u767B\u5F55\u6A21\u5F0F\uFF09");
|
|
4526
|
+
return { accessToken, accountId: j.tokens?.account_id, lastRefresh: j.last_refresh };
|
|
4527
|
+
} catch (err) {
|
|
4528
|
+
if (err instanceof UsageError) throw err;
|
|
4529
|
+
lastErr = err;
|
|
4530
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
throw new UsageError("no-auth", `auth.json \u53CD\u590D\u89E3\u6790\u5931\u8D25\uFF1A${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
4534
|
+
}
|
|
4535
|
+
function jwtExpMs(token) {
|
|
4536
|
+
const part = token.split(".")[1];
|
|
4537
|
+
if (!part) return void 0;
|
|
4538
|
+
try {
|
|
4539
|
+
const payload = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
4540
|
+
return typeof payload.exp === "number" ? payload.exp * 1e3 : void 0;
|
|
4541
|
+
} catch {
|
|
4542
|
+
return void 0;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
async function chatgptBaseUrl() {
|
|
4546
|
+
try {
|
|
4547
|
+
const raw = await readFile8(join12(resolveCodexHome(), "config.toml"), "utf8");
|
|
4548
|
+
for (const line of raw.split("\n")) {
|
|
4549
|
+
const t = line.trim();
|
|
4550
|
+
if (t.startsWith("[")) break;
|
|
4551
|
+
const m = /^chatgpt_base_url\s*=\s*"([^"]+)"/.exec(t);
|
|
4552
|
+
if (m?.[1]) return m[1].replace(/\/+$/, "");
|
|
4553
|
+
}
|
|
4554
|
+
} catch {
|
|
4555
|
+
}
|
|
4556
|
+
return DEFAULT_BASE_URL;
|
|
4557
|
+
}
|
|
4558
|
+
var refreshInFlight = null;
|
|
4559
|
+
async function refreshViaAppServer() {
|
|
4560
|
+
if (refreshInFlight) return refreshInFlight;
|
|
4561
|
+
refreshInFlight = (async () => {
|
|
4562
|
+
const before = await readCodexAuth().catch(() => void 0);
|
|
4563
|
+
const bin = resolveCodexBin();
|
|
4564
|
+
if (!bin) return null;
|
|
4565
|
+
const client = new AppServerClient({ bin, cwd: process.cwd(), clientName: "feishu-codex-bridge-usage" });
|
|
4566
|
+
let account = void 0;
|
|
4567
|
+
try {
|
|
4568
|
+
await withDeadline2(client.connect(), REFRESH_TIMEOUT_MS, "usage-refresh connect");
|
|
4569
|
+
const res = await withDeadline2(
|
|
4570
|
+
client.request("account/read", { refreshToken: true }),
|
|
4571
|
+
REFRESH_TIMEOUT_MS,
|
|
4572
|
+
"account/read refresh"
|
|
4573
|
+
);
|
|
4574
|
+
account = res?.account;
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
log.fail("usage", err, { phase: "refresh" });
|
|
4577
|
+
return null;
|
|
4578
|
+
} finally {
|
|
4579
|
+
await client.close().catch(() => void 0);
|
|
4580
|
+
}
|
|
4581
|
+
const after = await readCodexAuth().catch(() => void 0);
|
|
4582
|
+
if (after && after.accessToken !== before?.accessToken) return after;
|
|
4583
|
+
if (account === null) return "permanent-failure";
|
|
4584
|
+
return null;
|
|
4585
|
+
})();
|
|
4586
|
+
try {
|
|
4587
|
+
return await refreshInFlight;
|
|
4588
|
+
} finally {
|
|
4589
|
+
refreshInFlight = null;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
function withDeadline2(p, ms, label) {
|
|
4593
|
+
return new Promise((resolve7, reject) => {
|
|
4594
|
+
const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
4595
|
+
p.then(
|
|
4596
|
+
(v) => {
|
|
4597
|
+
clearTimeout(t);
|
|
4598
|
+
resolve7(v);
|
|
4599
|
+
},
|
|
4600
|
+
(e) => {
|
|
4601
|
+
clearTimeout(t);
|
|
4602
|
+
reject(e);
|
|
4603
|
+
}
|
|
4604
|
+
);
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
async function fetchWham(base, path, auth) {
|
|
4608
|
+
const ctl = new AbortController();
|
|
4609
|
+
const t = setTimeout(() => ctl.abort(), HTTP_TIMEOUT_MS);
|
|
4610
|
+
try {
|
|
4611
|
+
const resp = await fetch(`${base}${path}`, {
|
|
4612
|
+
headers: {
|
|
4613
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
4614
|
+
...auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {},
|
|
4615
|
+
"User-Agent": "codex-cli"
|
|
4616
|
+
},
|
|
4617
|
+
signal: ctl.signal
|
|
4618
|
+
});
|
|
4619
|
+
if (!resp.ok) return { status: resp.status };
|
|
4620
|
+
return { status: resp.status, json: await resp.json() };
|
|
4621
|
+
} finally {
|
|
4622
|
+
clearTimeout(t);
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
async function whamGet(path) {
|
|
4626
|
+
let auth = await readCodexAuth();
|
|
4627
|
+
const exp = jwtExpMs(auth.accessToken);
|
|
4628
|
+
if (exp !== void 0 && exp <= Date.now() + EXP_SKEW_MS) {
|
|
4629
|
+
const refreshed = await refreshViaAppServer();
|
|
4630
|
+
if (refreshed === "permanent-failure") throw new UsageError("need-relogin", "Codex \u767B\u5F55\u6001\u5DF2\u5931\u6548");
|
|
4631
|
+
if (refreshed) auth = refreshed;
|
|
4632
|
+
else throw new UsageError("transient", "\u767B\u5F55\u6001\u4E34\u671F\u4E14\u6682\u65F6\u65E0\u6CD5\u5237\u65B0");
|
|
4633
|
+
}
|
|
4634
|
+
const base = await chatgptBaseUrl();
|
|
4635
|
+
const attempt = async (a) => {
|
|
4636
|
+
try {
|
|
4637
|
+
return await fetchWham(base, path, a);
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
throw new UsageError("transient", `\u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
4640
|
+
}
|
|
4641
|
+
};
|
|
4642
|
+
let res = await attempt(auth);
|
|
4643
|
+
if (res.status === 401) {
|
|
4644
|
+
const fresh = await readCodexAuth();
|
|
4645
|
+
if (fresh.accessToken !== auth.accessToken) {
|
|
4646
|
+
auth = fresh;
|
|
4647
|
+
res = await attempt(auth).catch(() => res);
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
if (res.status === 401) {
|
|
4651
|
+
const refreshed = await refreshViaAppServer();
|
|
4652
|
+
if (refreshed === "permanent-failure" || refreshed === null) {
|
|
4653
|
+
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");
|
|
4654
|
+
}
|
|
4655
|
+
res = await attempt(refreshed);
|
|
4656
|
+
if (res.status === 401) throw new UsageError("need-relogin", "\u5237\u65B0\u540E\u4ECD 401\uFF0C\u8D26\u53F7\u4FA7\u5DF2\u62D2\u7EDD");
|
|
4657
|
+
}
|
|
4658
|
+
if (res.json === void 0) throw new UsageError("transient", `HTTP ${res.status} (${path})`);
|
|
4659
|
+
return res.json;
|
|
4660
|
+
}
|
|
4661
|
+
function mapWindow(w) {
|
|
4662
|
+
if (!w || typeof w.used_percent !== "number") return void 0;
|
|
4663
|
+
return {
|
|
4664
|
+
usedPercent: Math.min(100, Math.max(0, w.used_percent)),
|
|
4665
|
+
windowSeconds: w.limit_window_seconds,
|
|
4666
|
+
resetAt: w.reset_at
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
function mapUsageResponse(raw, fetchedAt) {
|
|
4670
|
+
const mapBucket = (rl, name) => ({
|
|
4671
|
+
...name ? { name } : {},
|
|
4672
|
+
primary: mapWindow(rl?.primary_window),
|
|
4673
|
+
secondary: mapWindow(rl?.secondary_window)
|
|
4674
|
+
});
|
|
4675
|
+
return {
|
|
4676
|
+
planType: raw.plan_type,
|
|
4677
|
+
main: mapBucket(raw.rate_limit),
|
|
4678
|
+
extras: (raw.additional_rate_limits ?? []).filter((x) => x?.rate_limit).map((x) => mapBucket(x.rate_limit, x.limit_name)),
|
|
4679
|
+
fetchedAt
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
function mapProfileResponse(raw) {
|
|
4683
|
+
const s = raw.stats ?? {};
|
|
4684
|
+
return {
|
|
4685
|
+
// 只用 display_name,绝不兜底 username——后者是邮箱 local part,会随可转发的
|
|
4686
|
+
// 分享卡泄出去;display_name 缺失时卡片侧降级「我的」。
|
|
4687
|
+
displayName: raw.profile?.display_name || void 0,
|
|
4688
|
+
lifetimeTokens: s.lifetime_tokens,
|
|
4689
|
+
peakDailyTokens: s.peak_daily_tokens,
|
|
4690
|
+
currentStreakDays: s.current_streak_days,
|
|
4691
|
+
longestStreakDays: s.longest_streak_days,
|
|
4692
|
+
longestTurnSec: s.longest_running_turn_sec,
|
|
4693
|
+
totalThreads: s.total_threads,
|
|
4694
|
+
fastModePct: s.fast_mode_usage_percentage,
|
|
4695
|
+
totalSkillsUsed: s.total_skills_used,
|
|
4696
|
+
uniqueSkillsUsed: s.unique_skills_used,
|
|
4697
|
+
mostUsedEffort: s.most_used_reasoning_effort,
|
|
4698
|
+
mostUsedEffortPct: s.most_used_reasoning_effort_percentage,
|
|
4699
|
+
topInvocations: (s.top_invocations ?? []).map((t) => ({
|
|
4700
|
+
name: t.plugin_name ?? t.skill_name ?? "",
|
|
4701
|
+
count: t.usage_count ?? 0,
|
|
4702
|
+
kind: t.plugin_name ? "plugin" : "skill"
|
|
4703
|
+
})).filter((t) => t.name),
|
|
4704
|
+
dailyBuckets: (s.daily_usage_buckets ?? []).filter((b) => typeof b.start_date === "string").map((b) => ({ date: b.start_date, tokens: b.tokens ?? 0 })),
|
|
4705
|
+
statsAsOf: raw.metadata?.stats_as_of
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
var profileCache = null;
|
|
4709
|
+
var usageCache = null;
|
|
4710
|
+
async function fetchProfileStats(force = false) {
|
|
4711
|
+
if (!force && profileCache && Date.now() - profileCache.at < PROFILE_CACHE_MS) return profileCache.data;
|
|
4712
|
+
const raw = await whamGet("/wham/profiles/me");
|
|
4713
|
+
const data = mapProfileResponse(raw);
|
|
4714
|
+
profileCache = { at: Date.now(), data };
|
|
4715
|
+
return data;
|
|
4716
|
+
}
|
|
4717
|
+
async function fetchUsageSnapshot(force = false) {
|
|
4718
|
+
if (!force && usageCache && Date.now() - usageCache.at < USAGE_CACHE_MS) return usageCache.data;
|
|
4719
|
+
const raw = await whamGet("/wham/usage");
|
|
4720
|
+
const data = mapUsageResponse(raw, Date.now());
|
|
4721
|
+
usageCache = { at: Date.now(), data };
|
|
4722
|
+
return data;
|
|
4723
|
+
}
|
|
4724
|
+
async function fetchUsageBundle(force = false) {
|
|
4725
|
+
const [profile, usage] = await Promise.all([fetchProfileStats(force), fetchUsageSnapshot(force)]);
|
|
4726
|
+
return { profile, usage };
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
// src/card/usage-cards.ts
|
|
4730
|
+
function formatTokensZh(n) {
|
|
4731
|
+
if (n === void 0 || n === null || Number.isNaN(n)) return "\u2014";
|
|
4732
|
+
const fmt = (v) => {
|
|
4733
|
+
const s = v.toFixed(1);
|
|
4734
|
+
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
|
4735
|
+
};
|
|
4736
|
+
if (n >= 1e8) return `${fmt(n / 1e8)}\u4EBF`;
|
|
4737
|
+
if (n >= 1e4) {
|
|
4738
|
+
const s = fmt(n / 1e4);
|
|
4739
|
+
return s === "10000" ? "1\u4EBF" : `${s}\u4E07`;
|
|
4740
|
+
}
|
|
4741
|
+
return n.toLocaleString("en-US");
|
|
4742
|
+
}
|
|
4743
|
+
function windowLabel(seconds) {
|
|
4744
|
+
if (!seconds) return "\u9650\u989D";
|
|
4745
|
+
if (seconds === 18e3) return "5 \u5C0F\u65F6";
|
|
4746
|
+
if (seconds === 604800) return "7 \u5929";
|
|
4747
|
+
return seconds < 86400 ? `${Math.round(seconds / 3600)} \u5C0F\u65F6` : `${Math.round(seconds / 86400)} \u5929`;
|
|
4748
|
+
}
|
|
4749
|
+
function resetLabel(resetAtSec, nowMs2 = Date.now()) {
|
|
4750
|
+
const d = new Date(resetAtSec * 1e3);
|
|
4751
|
+
const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
4752
|
+
const dayKey = (x) => `${x.getFullYear()}-${x.getMonth()}-${x.getDate()}`;
|
|
4753
|
+
const now = new Date(nowMs2);
|
|
4754
|
+
if (dayKey(d) === dayKey(now)) return `\u4ECA\u5929 ${hm}`;
|
|
4755
|
+
const tomorrow = new Date(nowMs2 + 864e5);
|
|
4756
|
+
if (dayKey(d) === dayKey(tomorrow)) return `\u660E\u5929 ${hm}`;
|
|
4757
|
+
return `${d.getMonth() + 1}\u6708${d.getDate()}\u65E5 ${hm}`;
|
|
4758
|
+
}
|
|
4759
|
+
function localDateStr(d = /* @__PURE__ */ new Date()) {
|
|
4760
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
4761
|
+
}
|
|
4762
|
+
function toEpochDay(date) {
|
|
4763
|
+
const [y = 1970, m = 1, d = 1] = date.split("-").map(Number);
|
|
4764
|
+
return Date.UTC(y, m - 1, d) / 864e5;
|
|
4765
|
+
}
|
|
4766
|
+
function fromEpochDay(day) {
|
|
4767
|
+
const d = new Date(day * 864e5);
|
|
4768
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
4769
|
+
}
|
|
4770
|
+
function mondayOf(day) {
|
|
4771
|
+
const dow = new Date(day * 864e5).getUTCDay();
|
|
4772
|
+
return day - (dow + 6) % 7;
|
|
4773
|
+
}
|
|
4774
|
+
var DAY_LABELS = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
|
|
4775
|
+
function heatmapCells(buckets, today = localDateStr(), weeks = 14) {
|
|
4776
|
+
const todayDay = toEpochDay(today);
|
|
4777
|
+
const tokensByDay = /* @__PURE__ */ new Map();
|
|
4778
|
+
for (const b of buckets) tokensByDay.set(toEpochDay(b.date), b.tokens);
|
|
4779
|
+
const startMonday = mondayOf(todayDay) - (weeks - 1) * 7;
|
|
4780
|
+
const weekLabel = (c) => {
|
|
4781
|
+
const d = new Date((startMonday + c * 7) * 864e5);
|
|
4782
|
+
return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
|
|
4783
|
+
};
|
|
4784
|
+
const values = [];
|
|
4785
|
+
for (let c = 0; c < weeks; c++) {
|
|
4786
|
+
for (let r = 0; r < 7; r++) {
|
|
4787
|
+
const day = startMonday + c * 7 + r;
|
|
4788
|
+
if (day > todayDay) continue;
|
|
4789
|
+
const v = tokensByDay.get(day) ?? 0;
|
|
4790
|
+
const d = new Date(day * 864e5);
|
|
4791
|
+
const date = `${d.getUTCMonth() + 1}\u6708${d.getUTCDate()}\u65E5`;
|
|
4792
|
+
const label = v > 0 ? `${date} \u4F7F\u7528\u4E86 ${formatTokensZh(v)} Token` : `${date} \u65E0\u7528\u91CF`;
|
|
4793
|
+
values.push({ week: weekLabel(c), day: DAY_LABELS[r] ?? "", value: v, label });
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
return { values, startDate: fromEpochDay(startMonday), endDate: today, weeks };
|
|
4797
|
+
}
|
|
4798
|
+
var HEAT_RANGE = ["#ebedf0", "#bbdefb", "#64b5f6", "#1e88e5", "#0d47a1"];
|
|
4799
|
+
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";
|
|
4800
|
+
function heatmapChartEl(buckets, today) {
|
|
4801
|
+
const h = heatmapCells(buckets, today);
|
|
4802
|
+
return {
|
|
4803
|
+
tag: "chart",
|
|
4804
|
+
aspect_ratio: "2:1",
|
|
4805
|
+
chart_spec: {
|
|
4806
|
+
type: "common",
|
|
4807
|
+
padding: 4,
|
|
4808
|
+
data: [{ id: "usage", values: h.values }],
|
|
4809
|
+
series: [
|
|
4810
|
+
{
|
|
4811
|
+
type: "heatmap",
|
|
4812
|
+
xField: "week",
|
|
4813
|
+
yField: "day",
|
|
4814
|
+
valueField: "label",
|
|
4815
|
+
cell: { style: { fill: { field: "value", scale: "color" }, shape: ROUNDED_CELL } }
|
|
4816
|
+
}
|
|
4817
|
+
],
|
|
4818
|
+
color: { type: "linear", domain: [{ dataId: "usage", fields: ["value"] }], range: HEAT_RANGE },
|
|
4819
|
+
axes: [
|
|
4820
|
+
{
|
|
4821
|
+
orient: "bottom",
|
|
4822
|
+
type: "band",
|
|
4823
|
+
bandPadding: 0.25,
|
|
4824
|
+
domainLine: { visible: false },
|
|
4825
|
+
tick: { visible: false }
|
|
4826
|
+
},
|
|
4827
|
+
{
|
|
4828
|
+
orient: "left",
|
|
4829
|
+
type: "band",
|
|
4830
|
+
bandPadding: 0.25,
|
|
4831
|
+
domainLine: { visible: false },
|
|
4832
|
+
tick: { visible: false },
|
|
4833
|
+
label: { visible: false }
|
|
4834
|
+
}
|
|
4835
|
+
],
|
|
4836
|
+
legends: { visible: false },
|
|
4837
|
+
tooltip: { visible: true, mark: { title: { visible: false } } }
|
|
4838
|
+
}
|
|
4839
|
+
};
|
|
4840
|
+
}
|
|
4841
|
+
function planLabel(plan) {
|
|
4842
|
+
if (!plan) return void 0;
|
|
4843
|
+
const m = {
|
|
4844
|
+
free: "Free",
|
|
4845
|
+
go: "Go",
|
|
4846
|
+
plus: "Plus",
|
|
4847
|
+
pro: "Pro",
|
|
4848
|
+
prolite: "Pro Lite",
|
|
4849
|
+
team: "Team",
|
|
4850
|
+
business: "Business",
|
|
4851
|
+
enterprise: "Enterprise",
|
|
4852
|
+
edu: "Edu",
|
|
4853
|
+
education: "Edu"
|
|
4854
|
+
};
|
|
4855
|
+
return m[plan] ?? plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
4856
|
+
}
|
|
4857
|
+
function formatDurationZh(seconds) {
|
|
4858
|
+
if (seconds === void 0 || seconds === null || Number.isNaN(seconds) || seconds < 0) return "\u2014";
|
|
4859
|
+
const mins = Math.round(seconds / 60);
|
|
4860
|
+
if (mins < 60) return `${mins} \u5206`;
|
|
4861
|
+
const h = Math.floor(mins / 60);
|
|
4862
|
+
const rem = mins % 60;
|
|
4863
|
+
return rem ? `${h} \u5C0F\u65F6 ${rem} \u5206` : `${h} \u5C0F\u65F6`;
|
|
4864
|
+
}
|
|
4865
|
+
var remainingPct = (w) => Math.max(0, 100 - w.usedPercent);
|
|
4866
|
+
function progressChartEl(w) {
|
|
4867
|
+
const label = `${windowLabel(w.windowSeconds)}\u5269\u4F59`;
|
|
4868
|
+
return {
|
|
4869
|
+
tag: "chart",
|
|
4870
|
+
height: "40px",
|
|
4871
|
+
chart_spec: {
|
|
4872
|
+
type: "linearProgress",
|
|
4873
|
+
data: [{ id: "p", values: [{ type: label, value: remainingPct(w) / 100 }] }],
|
|
4874
|
+
xField: "value",
|
|
4875
|
+
yField: "type",
|
|
4876
|
+
cornerRadius: 8,
|
|
4877
|
+
bandWidth: 12,
|
|
4878
|
+
axes: [
|
|
4879
|
+
{ orient: "left", type: "band", visible: false },
|
|
4880
|
+
{ orient: "bottom", type: "linear", visible: false }
|
|
4881
|
+
],
|
|
4882
|
+
tooltip: {
|
|
4883
|
+
visible: true,
|
|
4884
|
+
mark: { title: { visible: false }, content: [{ key: label, value: `${remainingPct(w)}%` }] }
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
function rateLimitElements(bucket, nowMs2) {
|
|
4890
|
+
const out = [];
|
|
4891
|
+
const icons = ["\u26A1", "\u{1F4C5}"];
|
|
4892
|
+
[bucket.primary, bucket.secondary].forEach((w, i) => {
|
|
4893
|
+
if (!w) return;
|
|
4894
|
+
const reset = w.resetAt ? `\u3000<font color='grey'>${resetLabel(w.resetAt, nowMs2)} \u91CD\u7F6E</font>` : "";
|
|
4895
|
+
out.push(md(`${icons[i]} **${windowLabel(w.windowSeconds)}\u9650\u989D**\u3000\u5269\u4F59 ${remainingPct(w)}%${reset}`));
|
|
4896
|
+
out.push(progressChartEl(w));
|
|
4897
|
+
});
|
|
4898
|
+
if (!out.length) return [note("\u6682\u65E0\u9650\u989D\u6570\u636E")];
|
|
4899
|
+
return out;
|
|
4900
|
+
}
|
|
4901
|
+
function statColumns(items) {
|
|
4902
|
+
return {
|
|
4903
|
+
tag: "column_set",
|
|
4904
|
+
flex_mode: "flow",
|
|
4905
|
+
horizontal_spacing: "large",
|
|
4906
|
+
columns: items.map((it) => ({
|
|
4907
|
+
tag: "column",
|
|
4908
|
+
width: "auto",
|
|
4909
|
+
elements: [
|
|
4910
|
+
{ tag: "markdown", content: `**${it.value}**`, text_size: "heading" },
|
|
4911
|
+
noteMd(it.label)
|
|
4912
|
+
]
|
|
4913
|
+
}))
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
function profileStatItems(p) {
|
|
4917
|
+
return [
|
|
4918
|
+
{ value: formatTokensZh(p.lifetimeTokens), label: "\u7D2F\u8BA1 Token \u6570" },
|
|
4919
|
+
{ value: formatTokensZh(p.peakDailyTokens), label: "\u5CF0\u503C Token \u6570" },
|
|
4920
|
+
{ value: formatDurationZh(p.longestTurnSec), label: "\u6700\u957F\u4EFB\u52A1\u65F6\u957F" },
|
|
4921
|
+
{ value: p.currentStreakDays !== void 0 ? `${p.currentStreakDays} \u5929` : "\u2014", label: "\u5F53\u524D\u8FDE\u7EED\u5929\u6570" },
|
|
4922
|
+
{ value: p.longestStreakDays !== void 0 ? `${p.longestStreakDays} \u5929` : "\u2014", label: "\u6700\u957F\u8FDE\u7EED\u5929\u6570" }
|
|
4923
|
+
];
|
|
4924
|
+
}
|
|
4925
|
+
function heatmapElements(p, today) {
|
|
4926
|
+
return [md("\u{1F4C8} **\u6BCF\u65E5 Token \u7528\u91CF**"), heatmapChartEl(p.dailyBuckets, today)];
|
|
4927
|
+
}
|
|
4928
|
+
function effortLabel(effort) {
|
|
4929
|
+
const m = { minimal: "\u6781\u4F4E", low: "\u4F4E", medium: "\u4E2D", high: "\u9AD8", xhigh: "\u8D85\u9AD8" };
|
|
4930
|
+
return m[effort] ?? effort;
|
|
4931
|
+
}
|
|
4932
|
+
function insightsElements(p) {
|
|
4933
|
+
const left = [];
|
|
4934
|
+
if (p.fastModePct !== void 0) left.push(`Fast Mode\u3000**${Math.round(p.fastModePct)}%**`);
|
|
4935
|
+
if (p.mostUsedEffort) {
|
|
4936
|
+
const pct = p.mostUsedEffortPct !== void 0 ? ` \xB7 ${Math.round(p.mostUsedEffortPct)}%` : "";
|
|
4937
|
+
left.push(`\u6700\u5E38\u7528\u63A8\u7406\u3000**${effortLabel(p.mostUsedEffort)}${pct}**`);
|
|
4938
|
+
}
|
|
4939
|
+
if (p.uniqueSkillsUsed !== void 0) left.push(`\u4F7F\u7528\u8FC7\u7684\u6280\u80FD\u3000**${p.uniqueSkillsUsed}**`);
|
|
4940
|
+
if (p.totalSkillsUsed !== void 0) left.push(`\u6280\u80FD\u8C03\u7528\u603B\u6570\u3000**${p.totalSkillsUsed.toLocaleString("en-US")}**`);
|
|
4941
|
+
if (p.totalThreads !== void 0) left.push(`\u4F1A\u8BDD\u603B\u6570\u3000**${p.totalThreads.toLocaleString("en-US")}**`);
|
|
4942
|
+
const right = p.topInvocations.slice(0, 5).map((t) => `${t.kind === "plugin" ? "@" : "$"}${t.name}\u3000**\xD7${t.count}**`);
|
|
4943
|
+
const col = (title, lines) => ({
|
|
4944
|
+
tag: "column",
|
|
4945
|
+
width: "weighted",
|
|
4946
|
+
weight: 1,
|
|
4947
|
+
elements: [md(`**${title}**`), noteMd(lines.join("\n"))]
|
|
4948
|
+
});
|
|
4949
|
+
const columns = [];
|
|
4950
|
+
if (left.length) columns.push(col("\u6D3B\u52A8\u6D1E\u5BDF", left));
|
|
4951
|
+
if (right.length) columns.push(col("\u5E38\u7528\u63D2\u4EF6 / \u6280\u80FD", right));
|
|
4952
|
+
if (!columns.length) return [];
|
|
4953
|
+
return [
|
|
4954
|
+
{ tag: "column_set", flex_mode: columns.length === 2 ? "bisect" : "stretch", horizontal_spacing: "large", columns }
|
|
4955
|
+
];
|
|
4956
|
+
}
|
|
4957
|
+
function joinWithHr(blocks) {
|
|
4958
|
+
const present = blocks.filter((b) => b.length);
|
|
4959
|
+
const out = [];
|
|
4960
|
+
present.forEach((b, i) => {
|
|
4961
|
+
if (i) out.push(hr());
|
|
4962
|
+
out.push(...b);
|
|
4963
|
+
});
|
|
4964
|
+
return out;
|
|
4965
|
+
}
|
|
4966
|
+
var usageButtons = () => actions([
|
|
4967
|
+
button("\u{1F504} \u5237\u65B0", { a: DM.usageRefresh }),
|
|
4968
|
+
button("\u{1F4E4} \u751F\u6210\u5206\u4EAB\u5361", { a: DM.usageShare }, "primary"),
|
|
4969
|
+
button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })
|
|
4970
|
+
]);
|
|
4971
|
+
var ERROR_COPY = {
|
|
4972
|
+
"no-auth": {
|
|
4973
|
+
title: "\u672A\u627E\u5230 Codex \u767B\u5F55\u6001",
|
|
4974
|
+
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"
|
|
4975
|
+
},
|
|
4976
|
+
"api-key-mode": {
|
|
4977
|
+
title: "\u5F53\u524D\u662F API-key \u767B\u5F55\u6A21\u5F0F",
|
|
4978
|
+
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"
|
|
4979
|
+
},
|
|
4980
|
+
"need-relogin": {
|
|
4981
|
+
title: "Codex \u767B\u5F55\u6001\u5DF2\u5931\u6548",
|
|
4982
|
+
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"
|
|
4983
|
+
},
|
|
4984
|
+
transient: {
|
|
4985
|
+
title: "\u6682\u65F6\u62C9\u4E0D\u5230\u6570\u636E",
|
|
4986
|
+
hint: "\u7F51\u7EDC\u6216 ChatGPT \u670D\u52A1\u6CE2\u52A8\uFF0C\u7A0D\u540E\u70B9\u300C\u{1F504} \u5237\u65B0\u300D\u91CD\u8BD5\u3002"
|
|
4987
|
+
}
|
|
4988
|
+
};
|
|
4989
|
+
function buildUsageCard(state) {
|
|
4990
|
+
if (state.phase === "loading") {
|
|
4991
|
+
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")], {
|
|
4992
|
+
header: { title: "\u{1F4CA} Codex \u7528\u91CF", template: "wathet" },
|
|
4993
|
+
forward: false
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4996
|
+
if (state.phase === "error") {
|
|
4997
|
+
const copy = ERROR_COPY[state.kind];
|
|
4998
|
+
return card(
|
|
4999
|
+
[
|
|
5000
|
+
md(`\u26A0\uFE0F **${copy.title}**`),
|
|
5001
|
+
md(copy.hint),
|
|
5002
|
+
...state.kind === "transient" ? [note(state.message)] : [],
|
|
5003
|
+
usageButtons()
|
|
5004
|
+
],
|
|
5005
|
+
{ header: { title: "\u{1F4CA} Codex \u7528\u91CF", template: "orange" }, forward: false }
|
|
5006
|
+
);
|
|
5007
|
+
}
|
|
5008
|
+
const { profile, usage } = state.data;
|
|
5009
|
+
const nowMs2 = state.now ?? Date.now();
|
|
5010
|
+
const elements = joinWithHr([
|
|
5011
|
+
rateLimitElements(usage.main, nowMs2),
|
|
5012
|
+
[statColumns(profileStatItems(profile))],
|
|
5013
|
+
heatmapElements(profile, state.today),
|
|
5014
|
+
insightsElements(profile)
|
|
5015
|
+
]);
|
|
5016
|
+
const plan = planLabel(usage.planType);
|
|
5017
|
+
elements.push(
|
|
5018
|
+
note(`\u7EDF\u8BA1\u622A\u81F3 ${profile.statsAsOf ?? "\u2014"}${plan ? ` \xB7 ${plan} \u5957\u9910` : ""} \xB7 \u6570\u636E\u6765\u81EA Codex \u4E2A\u4EBA\u8D44\u6599`),
|
|
5019
|
+
usageButtons()
|
|
5020
|
+
);
|
|
5021
|
+
return card(elements, {
|
|
5022
|
+
header: {
|
|
5023
|
+
title: "\u{1F4CA} Codex \u7528\u91CF",
|
|
5024
|
+
template: "wathet",
|
|
5025
|
+
...profile.displayName ? { subtitle: profile.displayName } : {}
|
|
5026
|
+
},
|
|
5027
|
+
forward: false
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
var SHARE_SECTIONS = [
|
|
5031
|
+
{ key: "stats", label: "\u6838\u5FC3\u7EDF\u8BA1\uFF08\u7D2F\u8BA1 / \u5CF0\u503C / \u8FDE\u7EED\u5929\u6570\uFF09" },
|
|
5032
|
+
{ key: "heatmap", label: "\u6BCF\u65E5\u7528\u91CF\u70ED\u529B\u56FE" },
|
|
5033
|
+
{ key: "insights", label: "\u6D3B\u52A8\u6D1E\u5BDF\u4E0E\u5E38\u7528\u6280\u80FD" },
|
|
5034
|
+
{ key: "limits", label: "\u9650\u989D\u8FDB\u5EA6\uFF085 \u5C0F\u65F6 / 7 \u5929\uFF09" },
|
|
5035
|
+
{ key: "plan", label: "\u5957\u9910\u4FE1\u606F" }
|
|
5036
|
+
];
|
|
5037
|
+
function parseShareSections(v) {
|
|
5038
|
+
const all = SHARE_SECTIONS.map((s) => s.key);
|
|
5039
|
+
const raw = Array.isArray(v) ? v : typeof v === "string" && v ? v.split(",") : [];
|
|
5040
|
+
const picked = raw.filter((x) => all.includes(String(x)));
|
|
5041
|
+
return new Set(picked.length ? picked : all);
|
|
5042
|
+
}
|
|
5043
|
+
function buildShareConfigCard(done = false) {
|
|
5044
|
+
return card(
|
|
5045
|
+
[
|
|
5046
|
+
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"),
|
|
5047
|
+
{
|
|
5048
|
+
tag: "form",
|
|
5049
|
+
name: "shareCfg",
|
|
5050
|
+
elements: [
|
|
5051
|
+
{
|
|
5052
|
+
tag: "multi_select_static",
|
|
5053
|
+
name: "secs",
|
|
5054
|
+
placeholder: { tag: "plain_text", content: "\u9ED8\u8BA4\u5168\u90E8\u5C55\u793A\uFF0C\u53EF\u53EA\u6311\u90E8\u5206\u533A\u5757" },
|
|
5055
|
+
options: SHARE_SECTIONS.map((s) => ({ text: { tag: "plain_text", content: s.label }, value: s.key }))
|
|
5056
|
+
},
|
|
5057
|
+
submitButton("\u{1F4E4} \u751F\u6210\u5206\u4EAB\u5361", { a: DM.usageShareDo })
|
|
5058
|
+
]
|
|
5059
|
+
},
|
|
5060
|
+
...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")] : [],
|
|
5061
|
+
actions([button("\u2B05\uFE0F \u8FD4\u56DE\u7528\u91CF", { a: DM.usage }), button("\u{1F3E0} \u83DC\u5355", { a: DM.menu })])
|
|
5062
|
+
],
|
|
5063
|
+
{ header: { title: "\u{1F4E4} \u5206\u4EAB\u5185\u5BB9\u9009\u62E9", template: "blue" }, forward: false }
|
|
5064
|
+
);
|
|
5065
|
+
}
|
|
5066
|
+
function buildUsageShareCard(data, opts = {}) {
|
|
5067
|
+
const { profile, usage } = data;
|
|
5068
|
+
const nowMs2 = opts.now ?? Date.now();
|
|
5069
|
+
const sec = opts.sections ?? new Set(SHARE_SECTIONS.map((s) => s.key));
|
|
5070
|
+
const who = profile.displayName ? `${profile.displayName} \u7684` : "\u6211\u7684";
|
|
5071
|
+
const plan = planLabel(usage.planType);
|
|
5072
|
+
const elements = joinWithHr([
|
|
5073
|
+
sec.has("stats") ? [statColumns(profileStatItems(profile))] : [],
|
|
5074
|
+
sec.has("heatmap") ? heatmapElements(profile, opts.today) : [],
|
|
5075
|
+
sec.has("insights") ? insightsElements(profile) : [],
|
|
5076
|
+
sec.has("limits") ? rateLimitElements(usage.main, nowMs2) : [],
|
|
5077
|
+
sec.has("plan") && plan ? [md(`\u{1F48E} **\u5957\u9910**\u3000${plan}`)] : []
|
|
5078
|
+
]);
|
|
5079
|
+
const stamp = new Date(nowMs2);
|
|
5080
|
+
const stampStr = `${stamp.getMonth() + 1}\u6708${stamp.getDate()}\u65E5 ${String(stamp.getHours()).padStart(2, "0")}:${String(stamp.getMinutes()).padStart(2, "0")}`;
|
|
5081
|
+
elements.push({
|
|
5082
|
+
tag: "markdown",
|
|
5083
|
+
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>`,
|
|
5084
|
+
text_size: "notation",
|
|
5085
|
+
text_align: "right"
|
|
5086
|
+
});
|
|
5087
|
+
return card(elements, {
|
|
5088
|
+
header: {
|
|
5089
|
+
title: `\u{1F4CA} ${who} Codex \u7528\u91CF`,
|
|
5090
|
+
template: "blue",
|
|
5091
|
+
...profile.statsAsOf ? { subtitle: `\u7EDF\u8BA1\u622A\u81F3 ${profile.statsAsOf}` } : {}
|
|
5092
|
+
}
|
|
5093
|
+
});
|
|
5094
|
+
}
|
|
5095
|
+
|
|
4480
5096
|
// src/project/lifecycle.ts
|
|
4481
5097
|
import { mkdir as mkdir9 } from "fs/promises";
|
|
4482
5098
|
import { existsSync as existsSync7 } from "fs";
|
|
4483
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5099
|
+
import { isAbsolute as isAbsolute2, join as join13, resolve as resolve6 } from "path";
|
|
4484
5100
|
|
|
4485
5101
|
// src/project/git-info.ts
|
|
4486
5102
|
import { execFile } from "child_process";
|
|
@@ -4622,7 +5238,7 @@ async function resolveCwd(name, existingPath) {
|
|
|
4622
5238
|
if (!existsSync7(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
|
|
4623
5239
|
return { cwd: cwd2, blank: false };
|
|
4624
5240
|
}
|
|
4625
|
-
const cwd =
|
|
5241
|
+
const cwd = join13(paths.projectsRootDir, name);
|
|
4626
5242
|
await mkdir9(cwd, { recursive: true });
|
|
4627
5243
|
return { cwd, blank: true };
|
|
4628
5244
|
}
|
|
@@ -4702,12 +5318,12 @@ async function leaveChat(channel, chatId) {
|
|
|
4702
5318
|
}
|
|
4703
5319
|
|
|
4704
5320
|
// src/bot/session-store.ts
|
|
4705
|
-
import { mkdir as mkdir10, readFile as
|
|
5321
|
+
import { mkdir as mkdir10, readFile as readFile9, rename as rename5, writeFile as writeFile8 } from "fs/promises";
|
|
4706
5322
|
import { dirname as dirname10 } from "path";
|
|
4707
5323
|
var FILE_VERSION3 = 1;
|
|
4708
5324
|
async function read2() {
|
|
4709
5325
|
try {
|
|
4710
|
-
const text = await
|
|
5326
|
+
const text = await readFile9(paths.sessionsFile, "utf8");
|
|
4711
5327
|
const parsed = JSON.parse(text);
|
|
4712
5328
|
return Array.isArray(parsed.sessions) ? parsed.sessions : [];
|
|
4713
5329
|
} catch (err) {
|
|
@@ -4764,7 +5380,7 @@ async function handleDmConsole(channel, cfg, msg) {
|
|
|
4764
5380
|
|
|
4765
5381
|
// src/bot/media.ts
|
|
4766
5382
|
import { mkdir as mkdir11, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
|
|
4767
|
-
import { join as
|
|
5383
|
+
import { join as join14 } from "path";
|
|
4768
5384
|
var MAX_IMAGES2 = 9;
|
|
4769
5385
|
var MEDIA_TTL_MS = 60 * 6e4;
|
|
4770
5386
|
var EXT_BY_CONTENT_TYPE = {
|
|
@@ -4791,7 +5407,7 @@ async function collectInboundImages(channel, msg) {
|
|
|
4791
5407
|
return [];
|
|
4792
5408
|
}
|
|
4793
5409
|
if (refs.length === 0) return [];
|
|
4794
|
-
await pruneOldMedia();
|
|
5410
|
+
await pruneOldMedia(paths.mediaDir);
|
|
4795
5411
|
try {
|
|
4796
5412
|
await mkdir11(paths.mediaDir, { recursive: true });
|
|
4797
5413
|
} catch {
|
|
@@ -4869,7 +5485,7 @@ async function downloadOne(channel, ref, index) {
|
|
|
4869
5485
|
params: { type: "image" }
|
|
4870
5486
|
});
|
|
4871
5487
|
const ext = extFromHeaders(res.headers);
|
|
4872
|
-
const file =
|
|
5488
|
+
const file = join14(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
|
|
4873
5489
|
await res.writeFile(file);
|
|
4874
5490
|
return file;
|
|
4875
5491
|
} catch (err) {
|
|
@@ -4894,16 +5510,16 @@ function readHeader(headers, name) {
|
|
|
4894
5510
|
function safeName(fileKey) {
|
|
4895
5511
|
return fileKey.replace(/[^a-zA-Z0-9_-]/g, "").slice(-40) || "img";
|
|
4896
5512
|
}
|
|
4897
|
-
async function pruneOldMedia() {
|
|
5513
|
+
async function pruneOldMedia(dir) {
|
|
4898
5514
|
let entries;
|
|
4899
5515
|
try {
|
|
4900
|
-
entries = await readdir2(
|
|
5516
|
+
entries = await readdir2(dir);
|
|
4901
5517
|
} catch {
|
|
4902
5518
|
return;
|
|
4903
5519
|
}
|
|
4904
5520
|
const cutoff = Date.now() - MEDIA_TTL_MS;
|
|
4905
5521
|
for (const name of entries) {
|
|
4906
|
-
const file =
|
|
5522
|
+
const file = join14(dir, name);
|
|
4907
5523
|
try {
|
|
4908
5524
|
const st = await stat3(file);
|
|
4909
5525
|
if (st.mtimeMs < cutoff) await rm4(file, { force: true });
|
|
@@ -4911,6 +5527,84 @@ async function pruneOldMedia() {
|
|
|
4911
5527
|
}
|
|
4912
5528
|
}
|
|
4913
5529
|
}
|
|
5530
|
+
var MAX_FILES = 9;
|
|
5531
|
+
var MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
5532
|
+
function messageHasFiles(msg) {
|
|
5533
|
+
return (msg.resources ?? []).some((r) => r.type === "file");
|
|
5534
|
+
}
|
|
5535
|
+
async function collectInboundFiles(channel, msg) {
|
|
5536
|
+
const refs = [];
|
|
5537
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5538
|
+
for (const r of msg.resources ?? []) {
|
|
5539
|
+
if (r.type === "file" && r.fileKey && !seen.has(r.fileKey)) {
|
|
5540
|
+
seen.add(r.fileKey);
|
|
5541
|
+
refs.push({ messageId: msg.messageId, fileKey: r.fileKey, fileName: r.fileName });
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
if (refs.length === 0) return [];
|
|
5545
|
+
await pruneOldMedia(paths.inboundDir);
|
|
5546
|
+
try {
|
|
5547
|
+
await mkdir11(paths.inboundDir, { recursive: true });
|
|
5548
|
+
} catch {
|
|
5549
|
+
}
|
|
5550
|
+
const out = [];
|
|
5551
|
+
for (const ref of refs.slice(0, MAX_FILES)) {
|
|
5552
|
+
const f = await downloadOneFile(channel, ref);
|
|
5553
|
+
if (f) out.push(f);
|
|
5554
|
+
}
|
|
5555
|
+
log.info("intake", "files", { found: refs.length, downloaded: out.length });
|
|
5556
|
+
return out;
|
|
5557
|
+
}
|
|
5558
|
+
async function downloadOneFile(channel, ref) {
|
|
5559
|
+
try {
|
|
5560
|
+
const res = await channel.rawClient.im.v1.messageResource.get({
|
|
5561
|
+
path: { message_id: ref.messageId, file_key: ref.fileKey },
|
|
5562
|
+
params: { type: "file" }
|
|
5563
|
+
});
|
|
5564
|
+
const declared = Number(readHeader(res.headers, "content-length"));
|
|
5565
|
+
if (Number.isFinite(declared) && declared > MAX_FILE_BYTES) {
|
|
5566
|
+
log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: declared });
|
|
5567
|
+
return void 0;
|
|
5568
|
+
}
|
|
5569
|
+
const name = cleanFileName(ref.fileName) || "attachment";
|
|
5570
|
+
const onDisk = `${safeName(ref.fileKey)}-${name}`;
|
|
5571
|
+
const file = join14(paths.inboundDir, onDisk);
|
|
5572
|
+
await res.writeFile(file);
|
|
5573
|
+
try {
|
|
5574
|
+
const st = await stat3(file);
|
|
5575
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
5576
|
+
await rm4(file, { force: true });
|
|
5577
|
+
log.warn("intake", "file-too-large", { fileKey: ref.fileKey.slice(0, 24), bytes: st.size });
|
|
5578
|
+
return void 0;
|
|
5579
|
+
}
|
|
5580
|
+
} catch {
|
|
5581
|
+
}
|
|
5582
|
+
return { path: file, name };
|
|
5583
|
+
} catch (err) {
|
|
5584
|
+
log.warn("intake", "file-download-failed", { fileKey: ref.fileKey.slice(0, 24), err: String(err) });
|
|
5585
|
+
return void 0;
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
function cleanFileName(name) {
|
|
5589
|
+
if (!name) return "";
|
|
5590
|
+
const base = name.split(/[/\\]/).pop() ?? name;
|
|
5591
|
+
const cleaned = base.replace(/[\x00-\x1f<>:"|?*]/g, "_").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
5592
|
+
return cleaned === "." || cleaned === ".." ? "" : cleaned;
|
|
5593
|
+
}
|
|
5594
|
+
function stripFileTokens(text) {
|
|
5595
|
+
return text.replace(/<file\b[^<]*\/>/g, "").replace(/[ \t]+\n/g, "\n").trim();
|
|
5596
|
+
}
|
|
5597
|
+
function weaveFileManifest(text, files) {
|
|
5598
|
+
const stripped = stripFileTokens(text);
|
|
5599
|
+
if (files.length === 0) return stripped;
|
|
5600
|
+
const lines = files.map((f) => `- ${f.name} \u2192 ${f.path}`).join("\n");
|
|
5601
|
+
const head = stripped ? `${stripped}
|
|
5602
|
+
|
|
5603
|
+
` : "";
|
|
5604
|
+
return `${head}[\u7528\u6237\u4E0A\u4F20\u4E86 ${files.length} \u4E2A\u9644\u4EF6\uFF0C\u5DF2\u4FDD\u5B58\u5230\u672C\u5730\uFF0C\u53EF\u7528 shell / \u8BFB\u53D6\u5DE5\u5177\u6309\u4E0B\u9762\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76F4\u63A5\u6253\u5F00\uFF1A
|
|
5605
|
+
${lines}
|
|
5606
|
+
]`;
|
|
5607
|
+
}
|
|
4914
5608
|
|
|
4915
5609
|
// src/bot/comments.ts
|
|
4916
5610
|
var SUPPORTED_FILE_TYPES = /* @__PURE__ */ new Set(["doc", "docx", "sheet", "file"]);
|
|
@@ -5397,20 +6091,30 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5397
6091
|
const perm = turnPerm(project, senderId);
|
|
5398
6092
|
return { sessionKey: perm.roleSuffix ? `${baseKey}#${perm.roleSuffix}` : baseKey, ...perm };
|
|
5399
6093
|
}
|
|
6094
|
+
async function ingestFiles(msg, text) {
|
|
6095
|
+
if (!messageHasFiles(msg)) return text;
|
|
6096
|
+
const files = await collectInboundFiles(channel, msg);
|
|
6097
|
+
const woven = weaveFileManifest(text, files);
|
|
6098
|
+
if (!woven.trim()) {
|
|
6099
|
+
return "\u7528\u6237\u53D1\u6765\u4E00\u4E2A\u9644\u4EF6\uFF0C\u4F46\u6865\u6CA1\u80FD\u4E0B\u8F7D\u5B83\uFF08\u53EF\u80FD\u8D85\u8FC7 50MB \u4E0A\u9650\u6216\u88AB\u98DE\u4E66\u62D2\u7EDD\uFF09\u3002\u8BF7\u544A\u8BC9\u7528\u6237\u9644\u4EF6\u6CA1\u8BFB\u5230\uFF0C\u53EF\u4EE5\u91CD\u53D1\uFF0C\u6216\u6539\u4E3A\u7C98\u8D34\u6587\u672C / \u53D1\u56FE\u7247\u3002";
|
|
6100
|
+
}
|
|
6101
|
+
return woven;
|
|
6102
|
+
}
|
|
5400
6103
|
async function handleTurn(msg, text, sessionKey, flat, project, perm) {
|
|
5401
6104
|
const existing = active.get(sessionKey);
|
|
5402
6105
|
if (existing) {
|
|
5403
6106
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6107
|
+
const woven = await ingestFiles(msg, text);
|
|
5404
6108
|
const cur = active.get(sessionKey);
|
|
5405
6109
|
if (!cur) {
|
|
5406
|
-
startReservedRun(msg,
|
|
6110
|
+
startReservedRun(msg, woven, sessionKey, flat, project, perm, images, true, text);
|
|
5407
6111
|
return;
|
|
5408
6112
|
}
|
|
5409
6113
|
if (getPendingPolicy(cfg) === "steer" && cur.run && cur.thread) {
|
|
5410
6114
|
const tid = cur.run.turnId();
|
|
5411
6115
|
if (tid) {
|
|
5412
6116
|
try {
|
|
5413
|
-
await cur.thread.steer({ text, images }, tid);
|
|
6117
|
+
await cur.thread.steer({ text: woven, images }, tid);
|
|
5414
6118
|
log.info("intake", "steer", { tid, images: images?.length ?? 0 });
|
|
5415
6119
|
return;
|
|
5416
6120
|
} catch (err) {
|
|
@@ -5418,13 +6122,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5418
6122
|
}
|
|
5419
6123
|
}
|
|
5420
6124
|
}
|
|
5421
|
-
cur.queue.push({ text, images });
|
|
6125
|
+
cur.queue.push({ text: woven, images });
|
|
5422
6126
|
log.info("intake", "queued", { depth: cur.queue.length });
|
|
5423
6127
|
return;
|
|
5424
6128
|
}
|
|
5425
6129
|
startReservedRun(msg, text, sessionKey, flat, project, perm);
|
|
5426
6130
|
}
|
|
5427
|
-
function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages) {
|
|
6131
|
+
function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
|
|
5428
6132
|
const existing = active.get(sessionKey);
|
|
5429
6133
|
if (existing) {
|
|
5430
6134
|
existing.queue.push({ text, images: preloadedImages });
|
|
@@ -5437,6 +6141,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5437
6141
|
const reaction = runReaction(msg.messageId, !sema.hasFree());
|
|
5438
6142
|
try {
|
|
5439
6143
|
const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
|
|
6144
|
+
const firstText = preIngested ? text : await ingestFiles(msg, text);
|
|
5440
6145
|
let thread = await resolveThread(sessionKey, msg.chatId, { mode: perm.mode, network: perm.network });
|
|
5441
6146
|
if (!thread) {
|
|
5442
6147
|
const cwd = project?.cwd ?? fallbackCwd;
|
|
@@ -5447,7 +6152,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5447
6152
|
chatId: msg.chatId,
|
|
5448
6153
|
cwd,
|
|
5449
6154
|
codexThreadId: thread.codexThreadId,
|
|
5450
|
-
|
|
6155
|
+
// `text` is already file-woven when preIngested; use the raw
|
|
6156
|
+
// `summaryText` (handleTurn's original) so the session label isn't
|
|
6157
|
+
// manifest boilerplate + a temp path.
|
|
6158
|
+
summary: stripFileTokens(summaryText2 ?? text).slice(0, 80),
|
|
5451
6159
|
createdAt: Date.now(),
|
|
5452
6160
|
updatedAt: Date.now()
|
|
5453
6161
|
});
|
|
@@ -5460,7 +6168,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5460
6168
|
replyInThread: !flat,
|
|
5461
6169
|
flat,
|
|
5462
6170
|
thread,
|
|
5463
|
-
firstText
|
|
6171
|
+
firstText,
|
|
5464
6172
|
images,
|
|
5465
6173
|
knownThreadId: sessionKey,
|
|
5466
6174
|
requesterOpenId: msg.senderId
|
|
@@ -5534,8 +6242,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5534
6242
|
await channel.send(msg.chatId, { markdown: `\u274C \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId }).catch(() => void 0);
|
|
5535
6243
|
return;
|
|
5536
6244
|
}
|
|
5537
|
-
const firstText = text || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
5538
6245
|
const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
|
|
6246
|
+
const firstText = await ingestFiles(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
|
|
5539
6247
|
log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
|
|
5540
6248
|
await launchRun(
|
|
5541
6249
|
{
|
|
@@ -5548,7 +6256,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5548
6256
|
model,
|
|
5549
6257
|
effort,
|
|
5550
6258
|
cwd,
|
|
5551
|
-
summary: text.slice(0, 80) || "(\u7A7A)",
|
|
6259
|
+
summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
|
|
5552
6260
|
requesterOpenId: msg.senderId,
|
|
5553
6261
|
roleSuffix: perm.roleSuffix
|
|
5554
6262
|
},
|
|
@@ -5714,6 +6422,46 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
|
|
|
5714
6422
|
const freshMenu = (evt) => {
|
|
5715
6423
|
patch(evt, buildDmMenuCard());
|
|
5716
6424
|
};
|
|
6425
|
+
const runUsage = (evt, force) => {
|
|
6426
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
6427
|
+
void (async () => {
|
|
6428
|
+
await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
|
|
6429
|
+
let msgId = evt.messageId;
|
|
6430
|
+
const okLoading = await updateManagedCard(channel, msgId, buildUsageCard({ phase: "loading" })).catch(
|
|
6431
|
+
() => false
|
|
6432
|
+
);
|
|
6433
|
+
if (!okLoading) {
|
|
6434
|
+
const sent = await sendManagedCard(channel, evt.chatId, buildUsageCard({ phase: "loading" })).catch(
|
|
6435
|
+
(e) => {
|
|
6436
|
+
log.fail("console", e, { phase: "usage-loading" });
|
|
6437
|
+
return void 0;
|
|
6438
|
+
}
|
|
6439
|
+
);
|
|
6440
|
+
if (!sent) return;
|
|
6441
|
+
msgId = sent.messageId;
|
|
6442
|
+
}
|
|
6443
|
+
let state;
|
|
6444
|
+
try {
|
|
6445
|
+
state = { phase: "ready", data: await fetchUsageBundle(force) };
|
|
6446
|
+
} catch (err) {
|
|
6447
|
+
log.fail("console", err, { phase: "usage" });
|
|
6448
|
+
state = {
|
|
6449
|
+
phase: "error",
|
|
6450
|
+
kind: err instanceof UsageError ? err.kind : "transient",
|
|
6451
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6452
|
+
};
|
|
6453
|
+
}
|
|
6454
|
+
const ok = await updateManagedCard(channel, msgId, buildUsageCard(state)).catch((e) => {
|
|
6455
|
+
log.fail("console", e, { phase: "usage-render" });
|
|
6456
|
+
return false;
|
|
6457
|
+
});
|
|
6458
|
+
if (!ok) {
|
|
6459
|
+
await sendManagedCard(channel, evt.chatId, buildUsageCard(state)).catch(
|
|
6460
|
+
(e) => log.fail("console", e, { phase: "usage-fallback" })
|
|
6461
|
+
);
|
|
6462
|
+
}
|
|
6463
|
+
})();
|
|
6464
|
+
};
|
|
5717
6465
|
const renderProjectList = async () => {
|
|
5718
6466
|
const [projects, sessions2] = await Promise.all([listProjects(), listSessions()]);
|
|
5719
6467
|
const byChat = /* @__PURE__ */ new Map();
|
|
@@ -5867,6 +6615,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
|
|
|
5867
6615
|
await restartDaemon().catch((e) => log.fail("console", e, { phase: "update-restart" }));
|
|
5868
6616
|
}
|
|
5869
6617
|
})();
|
|
6618
|
+
}).on(DM.usage, ({ evt }) => runUsage(evt, false)).on(DM.usageRefresh, ({ evt }) => runUsage(evt, true)).on(DM.usageShare, ({ evt }) => {
|
|
6619
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
6620
|
+
patch(evt, buildShareConfigCard());
|
|
6621
|
+
}).on(DM.usageShareDo, ({ evt, formValue }) => {
|
|
6622
|
+
if (!dmAdmin(evt.operator?.openId)) return;
|
|
6623
|
+
const sections = parseShareSections(formValue?.secs);
|
|
6624
|
+
void (async () => {
|
|
6625
|
+
await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
|
|
6626
|
+
try {
|
|
6627
|
+
const data = await fetchUsageBundle();
|
|
6628
|
+
await sendManagedCard(channel, evt.chatId, buildUsageShareCard(data, { sections }), evt.messageId);
|
|
6629
|
+
log.info("console", "usage-share", { sections: [...sections].join(",") });
|
|
6630
|
+
await updateManagedCard(channel, evt.messageId, buildShareConfigCard(true)).catch(() => void 0);
|
|
6631
|
+
} catch (err) {
|
|
6632
|
+
log.fail("console", err, { phase: "usage-share" });
|
|
6633
|
+
const reason = err instanceof UsageError ? err.message : "\u62C9\u53D6\u7528\u91CF\u6570\u636E\u5931\u8D25";
|
|
6634
|
+
await channel.send(evt.chatId, { markdown: `\u26A0\uFE0F \u751F\u6210\u5206\u4EAB\u5361\u5931\u8D25\uFF1A${reason}` }, { replyTo: evt.messageId }).catch(() => void 0);
|
|
6635
|
+
}
|
|
6636
|
+
})();
|
|
5870
6637
|
}).on(DM.rmConfirm, async ({ evt, value }) => {
|
|
5871
6638
|
const name = typeof value.n === "string" ? value.n : void 0;
|
|
5872
6639
|
if (!dmAdmin(evt.operator?.openId) || !name) return;
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ declare const paths: {
|
|
|
42
42
|
*/
|
|
43
43
|
secretsGetterScript: string;
|
|
44
44
|
mediaDir: string;
|
|
45
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
46
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
47
|
+
inboundDir: string;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
export { log, newTraceId, paths, withTrace };
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,10 @@ var paths = {
|
|
|
47
47
|
* passes lark-cli's AssertSecurePath audit.
|
|
48
48
|
*/
|
|
49
49
|
secretsGetterScript: join(appDir, "secrets-getter"),
|
|
50
|
-
mediaDir: join(appDir, "media")
|
|
50
|
+
mediaDir: join(appDir, "media"),
|
|
51
|
+
/** Inbound file attachments downloaded from chat, handed to codex by absolute
|
|
52
|
+
* path (codex has no native file input). TTL-pruned like {@link mediaDir}. */
|
|
53
|
+
inboundDir: join(appDir, "inbound")
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
// src/core/logger.ts
|