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