8000 feat(agy): show both Antigravity quota pools on one account by InfinityPacer · Pull Request #65 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Meterleaf 视觉设计

改动用户看得到的界面之前先读本文。这里记录界面的视觉方向和已经定下的令牌,令牌的值以
`src/web/styles.css` 为准,本文说明每个令牌用在哪里、为什么这样用。

## 方向

Meterleaf 是看账的工具,界面安静、密度适中,数字是主角。中性灰阶搭一个强调色,层次靠灰阶、
留白和细线分组,不用阴影堆叠或彩色色块。强调色只给进度、选中和主要操作,其他地方保持灰阶,
所以用户一眼看到的有颜色的东西,就是需要注意或可以操作的东西。

浅色与深色都支持,默认跟随系统。强调色有三套配色可选(翠绿、靛蓝、黑白),组件只用令牌,
不知道当前是哪套配色。

## 令牌

令牌只在 `src/web/styles.css` 定义,浅色写在 `:root`,深色写在 `.dark`,配色写在
`[data-palette]`。组件用 CSS 变量或 Tailwind 的令牌类(`bg-background`、`text-muted-foreground`、
`border-border`),不写十六进制色值。图表画在 canvas 里读不到 CSS 变量,由
`src/web/lib/theme-colors.ts` 取出当前主题的颜色。

| 令牌 | 浅色 / 深色 | 用在 |
| ------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- |
| `canvas` | `#f7f7f8` / `#0f0f11` | 页面底 |
| `surface` / `soft` / `elevated` | `#ffffff` `#f0f0f2` `#ffffff` / `#17171a` `#1f1f23` `#2e2e33` | 卡片与面板、弱一级的底(筛选、分段控件)、浮层 |
| `sidebar` | `#fbfbfc` / `#131316` | 桌面侧栏 |
| `ink` / `muted` / `faint` | `#18181b` `#5f5f68` `#8e8e97` / `#f2f2f3` `#a1a1aa` `#71717a` | 正文与数字、说明与次要数字、最弱的提示 |
| `line` / `line-soft` / `control-line` | `#e4e4e7` `#ededf0` `#d4d4d8` / `#2a2a2f` `#212125` `#3f3f46` | 分隔线、表格内的弱分隔、输入框与控件边框 |
| `track` | `#e9e9ec` / `#27272b` | 额度进度条的底槽 |
| `accent` / `accent-ink` | 翠绿 `#0e8a55` `#ffffff` / `#3ccb8a` `#07130d` | 进度、选中、焦点环、主要操作,`accent-ink` 是强调色上的文字 |
| `accent-soft` / `accent-line` | 强调色 8% / 12% 与 30% / 36% | 选中项与套餐徽标的底和边 |
| `warn` | `#c2410c` / `#f08a5d` | 额度用尽或接近用尽(`quota-alert` 是它的别名) |
| `danger` | `#c8323f` / `#f2798a` | 失败与破坏性操作 |
| `radius` | `6px` | 控件圆角基准,卡片用 8px,大面板用 12px,头像与状态点是圆形 |
| `shadow` | 极轻的 1px 投影 / 无 | 浅色下浮起的卡片,深色不用投影,靠 `elevated` 区分层次 |

靛蓝配色的强调色是 `#4f46e5` / `#8b8cf6`,黑白配色是 `#18181b` / `#f2f2f3`。`teal`、`blue`、`tint`
是旧名,都指向强调色,新代码不再使用。

## 字体与数字

- 字体是 Geist Variable,中文回落到苹方与微软雅黑。正文 16px,界面里最常用的是 14px(表格、
卡片正文)、13px(标签)和 12px(说明、重置时间),标题 18 到 30px。
- 字重以 400、500、600 为主,强调用 600,不用粗体堆层次。
- 金额、百分比、Tokens 与次数一律用等宽数字(`font-variant-numeric: tabular-nums`),上下行对齐,
数字变化时不跳动。
- 字间距保持 0,只有套餐徽标略放宽。

## 布局

- 900px 是手机与桌面的分界,手机样式在 `mobile.css` 与 `mobile-*.css`,桌面在 `desktop.css`。
两边共用的组件样式放在组件旁的 CSS 文件里。
- 间距以 4px 为单位,常用 8px 与 12px,区块之间 24px。
- 账户身份(头像、名称、套餐徽标)的样式统一在 `src/web/account-identity.css`,最后加载,总览、
账户页和手机首页看到的是同一个样子。头像颜色只表示账户类型(订阅、API、未知),不表示厂商。
- 套餐徽标只用强度区分档位,档位在 `src/web/lib/plan.ts` 定义。入门档是灰色描边,中间档是强调色
浅底,最高档是强调色实底。同档的 Claude、ChatGPT 与 Google 套餐外观相同。

## 组件

- 控件用 `src/web/components/ui/` 里的组件,底层是 Base UI,图标用 lucide,图表用 ECharts。
- 额度窗口(5 小时、7 天、Fable、月度)在首页卡片、账户列表和账户页共用 `QuotaWindow`,一个窗口
一行,写名称、已用比例与命中率、重置时间,下面是进度条和本期花费。窗口之间用细线分开。有两个额度池的
账户(Antigravity 的 Gemini 与 Claude)在同一张卡片里按池列出,窗口名前加池名,不拆成两张卡片。
- 动效只用于展开、切换与进度变化,时长短、不弹跳。系统开启减少动态效果时去掉位移与缩放。

## 文案

- 界面用中文,标签短,状态用一个词说清(已用尽、已重置、估算)。
- 未知的数字显示为未知或留空,不补 0,不用演示数据冒充真实数据。
- 截图与演示数据不出现真实账户、域名或地址。

## 验收

界面改动在演示界面里看(`bun run dev:demo`),桌面宽度与手机宽度都要看过,浅色与深色、三套配色
至少各看一眼受影响的地方。浏览器测试用 `bun run test:browser`,截图放在已忽略的 `test-results`。
视觉验收由维护者确认。
4 changes: 2 additions & 2 deletions docs/collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ Codex 的 `bo` 等其他通道属于网关后面的账户,读取时跳过并
采集器默认读取本机 Antigravity CLI(`agy`)的额度缓存与本地会话数据库,目录不存在时本轮为空,不报错。需要停用或修改目录时,在采集器 `config.json` 中配置 `agy` 的 `enabled`、`conversationsDir` 和 `statuslineDir`。

- 额度缓存来自 `~/.cache/agy-statusline/quota-<label>.tsv`,由状态栏脚本与 Stop hook 写出,采样时间取文件修改时间。每行用制表符分隔窗口名、已用百分比整数和重置时间的 Unix 秒,窗口名为 `gemini_5h`、`gemini_weekly`、`3p_5h` 或 `3p_weekly`,另可带一行 `plan\t<套餐名>\t-`。`<label>` 是 Google 账号邮箱本地部分转小写并把非 `[a-z0-9_-]` 字符替换为 `-` 后的标签,采集器不读取也不还原完整邮箱。同目录下的 `current-account` 保存当前活跃账号标签。
- 每个 Google 账号在 Antigravity 中有两套独立额度池,采集器为每个 `<label>` 建立两个订阅账户,分别是 `Antigravity (<label>) · Gemini`(标识 `agy-<label>-gemini`,接收 `gemini_5h` 与 `gemini_weekly`)和 `Antigravity (<label>) · Claude/GPT`(标识 `agy-<label>-3p`,接收 `3p_5h` 与 `3p_weekly`)。当某个窗口的重置时间不晚于采样时间或当前时间时,该窗口按已过期处理,旧百分比不会带入新周期。百分比或重置时间为 `-` 时保留未知,不补零。
- 每个 Google 账号在 Antigravity 中有两套独立额度池,采集器为每个 `<label>` 建立一个订阅账户 `Antigravity (<label>)`(标识 `agy-<label>`)。`gemini_5h` 与 `gemini_weekly` 上报为 `five-hour` 与 `seven-day` 窗口,`3p_5h` 与 `3p_weekly` 上报为 `five-hour-3p` 与 `seven-day-3p` 窗口,服务端按窗口计算各池的本期花费,Gemini 窗口只计 `gemini-` 开头的模型,另两个窗口只计其他模型。旧版采集器按池分别建立的 `agy-<label>-gemini` 与 `agy-<label>-3p` 两个账户仍随每轮同步上报,并指向新账户,它们名下的历史用量与额度在报表里归入新账户。原来在旧账户上手动设置的套餐不会自动带过来,需要在新账户上再选一次。当某个窗口的重置时间不晚于采样时间或当前时间时,该窗口按已过期处理,旧百分比不会带入新周期。百分比或重置时间为 `-` 时保留未知,不补零。
- Token 用量来自 `~/.gemini/antigravity-cli/conversations/*.db`。`agy` 运行中会锁住会话库,采集器对无 WAL 的库以只读不可变模式打开,对存在 `.db-wal` 的库先把 `.db` 与 `.db-wal` 复制到系统临时目录再只读打开,不在 `~/.gemini` 下写入、加锁或创建附属文件。
- 采集器解码 `gen_metadata` 表中的 `CortexStepGeneratorMetadata` protobuf,提取每次生成的未缓存输入、缓存读取、缓存写入、输出和属于输出子集的思考 Tokens,并关联 `steps` 表中的完成时间。任一基础桶缺失时保留未知,不补零。模型名与思考档位从上游响应模型、显示名和模型枚举归一化得到,Gemini 系列模型归入该账号的 Gemini 池账户,Claude 与 GPT-OSS 模型归入 Claude/GPT 池账户。早于 `2026-09-20` 的记录在读取时丢弃,已读取记录按会话、步骤与执行标识幂等去重。
- 采集器解码 `gen_metadata` 表中的 `CortexStepGeneratorMetadata` protobuf,提取每次生成的未缓存输入、缓存读取、缓存写入、输出和属于输出子集的思考 Tokens,并关联 `steps` 表中的完成时间。任一基础桶缺失时保留未知,不补零。模型名与思考档位从上游响应模型、显示名和模型枚举归一化得到,同一账号的 Gemini、Claude 与 GPT-OSS 模型都记在这个账号的账户下。早于 `2026-09-20` 的记录在读取时丢弃,已读取记录按会话、步骤与执行标识幂等去重。

## 多个 Claude 账户

Expand Down
2 changes: 1 addition & 1 deletion docs/reporting.md
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

## 额度与金额

5 小时和 7 天窗口独立显示,过期窗口不沿用上一周期的百分比和统计。Claude 账户如果上报了 Fable 周额度,会在 7 天窗口下方单独显示一行。它只统计 Fable 模型的请求,费用和预估都不含其他模型;没有上报时不显示,也不按套餐推断有没有这项额度。账户整体 7 天额度用满时所有模型都不可用,这时只显示 7 天窗口。7 天额度有效时,5 小时窗口过期或暂时没有数据会保留位置并显示“等待更新”,收到新周期的额度数据后恢复;7 天额度用满时只显示 7 天窗口。页面停留或从后台恢复时也会检查到期时间。
5 小时和 7 天窗口独立显示,过期窗口不沿用上一周期的百分比和统计。Claude 账户如果上报了 Fable 周额度,会在 7 天窗口下方单独显示一行。它只统计 Fable 模型的请求,费用和预估都不含其他模型;没有上报时不显示,也不按套餐推断有没有这项额度。Antigravity 的 Google 账户有 Gemini 与 Claude/GPT 两个额度池,同一张卡片里先列 Gemini 的 5 小时与 7 天窗口,再列 Claude 的两个窗口,每个池的费用只计自己的模型。一个池的 7 天额度用满只收起这个池的 5 小时窗口,两个池都用满才算账户不可用。桌面卡片右侧的 7 天预估是 Gemini 池的,Claude 池的预估在账户详情里。其他账户整体 7 天额度用满时所有模型都不可用,这时只显示 7 天窗口。7 天额度有效时,5 小时窗口过期或暂时没有数据会保留位置并显示“等待更新”,收到新周期的额度数据后恢复;7 天额度用满时只显示 7 天窗口。页面停留或从后台恢复时也会检查到期时间。

七天预估表示用满本周期额度时预计的总花费,等于本周期已花金额加上剩余百分比按每 1% 花费推算的金额。用得越多,需要推算的部分越少,用满时预估就是已花金额。同样的 1% 折算成美元并不固定,例如上下文较长、缓存读取较多的请求按 API 价格更贵,所以周期初期的预估会随用法变化。每 1% 花费比较当前周期内多个额度快照之间的费用与百分比变化,每段至少跨越 5 个百分点,积累至少 3 个有效点对后取中位数。同一百分比的重复采样不会增加权重。上游百分比是四舍五入后的整数,刚从 k-1 变为 k 的时刻按 k-0.5 计算,避免周期初期低估。5 小时窗口不做此预估。

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "meterleaf",
"version": "0.14.0",
"version": "0.15.0",
8BD0 "private": true,
"type": "module",
"license": "Apache-2.0",
Expand Down
91 changes: 43 additions & 48 deletions src/collector/agy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const agyWindows: Record<
> = {
gemini_5h: { pool: "gemini", window: "five-hour", minutes: 300 },
gemini_weekly: { pool: "gemini", window: "seven-day", minutes: 10080 },
"3p_5h": { pool: "3p", window: "five-hour", minutes: 300 },
"3p_weekly": { pool: "3p", window: "seven-day", minutes: 10080 },
"3p_5h": { pool: "3p", window: "five-hour-3p", minutes: 300 },
"3p_weekly": { pool: "3p", window: "seven-day-3p", minutes: 10080 },
};

/**
Expand All @@ -44,36 +44,51 @@ export function isValidAgyLabel(value: string): boolean {
return /^[a-z0-9][a-z0-9_-]{0,63}$/.test(value);
}

export function agyAccountId(label: string, pool: AgyPool): string {
export function agyAccountId(label: string): string {
return `agy-${label}`;
}

/** 早期版本按额度池拆成两个账户时用的标识,只为把那段历史归到合并后的账户。 */
function agyPoolAccountId(label: string, pool: AgyPool): string {
return `agy-${label}-${pool}`;
}

/**
* 每个 Google 账号在 Antigravity 中有两套独立配额池:Gemini 池(gemini_5h / gemini_weekly)
* 与 Claude/GPT 第三方模型池(3p_5h / 3p_weekly)。拆成两个账本账户后,各自的 5 小时与
* 周额度窗口、本期花费和周额度推算都能只统计消耗对应池子的模型请求。
* 每个 Google 账号在 Antigravity 中有两套独立额度池:Gemini 池与 Claude/GPT 第三方模型池。
* 两个池记在同一个账户下,Gemini 池上报为 five-hour 与 seven-day,第三方池上报为
* five-hour-3p 与 seven-day-3p,服务端按模型分别统计各池的本期花费与推算。
* legacy 是早期按池拆开的两个账户,指向合并后的账户,使它们名下的历史用量与额度一并归过去。
*/
export function agyAccountsForLabel(
label: string,
plan: string | null,
): { gemini: IngestAccount; thirdParty: IngestAccount } {
): { account: IngestAccount; legacy: IngestAccount[] } {
const base = {
platform: "google",
kind: "subscription" as const,
plan,
subjectKey: null,
};
return {
gemini: {
externalId: agyAccountId(label, "gemini"),
name: `Antigravity (${label}) · Gemini`,
platform: "google",
kind: "subscription",
plan,
subjectKey: null,
},
thirdParty: {
externalId: agyAccountId(label, "3p"),
name: `Antigravity (${label}) · Claude/GPT`,
platform: "google",
kind: "subscription",
plan,
subjectKey: null,
account: {
externalId: agyAccountId(label),
name: `Antigravity (${label})`,
...base,
},
legacy: [
{
externalId: agyPoolAccountId(label, "gemini"),
name: `Antigravity (${label}) · Gemini`,
...base,
parentExternalId: agyAccountId(label),
},
{
externalId: agyPoolAccountId(label, "3p"),
name: `Antigravity (${label}) · Claude/GPT`,
...base,
parentExternalId: agyAccountId(label),
},
],
};
}

Expand Down Expand Up @@ -217,7 +232,7 @@ export function readAgyQuotaFile(
entries.set(key, {
key,
pool: spec.pool,
accountExternalId: agyAccountId(label, spec.pool),
accountExternalId: agyAccountId(label),
window: spec.window,
windowMinutes: spec.minutes,
percent: expired ? null : rawPercent,
Expand Down Expand Up @@ -713,22 +728,6 @@ export function normalizeAgyModel(input: {
return null;
}

/** Gemini 系列模型归入 Gemini 池,Claude 与 GPT-OSS 等第三方模型归入 Claude/GPT 池。 */
export function agyPoolOfModel(
model: string,
customMetadata?: Record<string, string>,
): AgyPool {
if (model.startsWith("gemini-")) return "gemini";
if (
model.startsWith("claude-") ||
model.startsWith("gpt-") ||
customMetadata?.used_non_gemini_model === "true"
) {
return "3p";
}
return customMetadata?.used_non_gemini_model === "false" ? "gemini" : "3p";
}

export type ExtractedAgyTokens =
| {
kind: "usage";
Expand Down Expand Up @@ -1043,8 +1042,8 @@ export function readAgy(
const effectivePlan = plans[label] ?? null;
const accounts = agyAccountsForLabel(label, effectivePlan);
state.db.transaction(() => {
state.upsertAccount(accounts.gemini);
state.upsertAccount(accounts.thirdParty);
state.upsertAccount(accounts.account);
for (const legacy of accounts.legacy) state.upsertAccount(legacy);
if (cache.entries.length > 0) {
const snapshot = agyQuotaSnapshot(cache);
if (state.recordQuota(snapshot, `agy:${label}`)) {
Expand All @@ -1066,8 +1065,8 @@ export function readAgy(
activeAccount,
plans[activeAccount] ?? null,
);
state.upsertAccount(accounts.gemini);
state.upsertAccount(accounts.thirdParty);
state.upsertAccount(accounts.account);
for (const legacy of accounts.legacy) state.upsertAccount(legacy);
}

if (!existsSync(sources.conversationsDir)) return report;
Expand Down Expand Up @@ -1218,11 +1217,7 @@ export function readAgy(
continue;
}

const pool = agyPoolOfModel(
normalized.model,
decoded.chatModel?.customMetadata,
);
const accountExternalId = agyAccountId(activeAccount, pool);
const accountExternalId = agyAccountId(activeAccount);
const uniqueKey = JSON.stringify([
conversationId,
row.idx,
Expand Down
10 changes: 9 additions & 1 deletion src/domain/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,17 @@ export interface QuotaFact extends SourceRef {
accountExternalId: string;
/**
* seven-day-fable 是只计 Fable 请求的独立周额度,与账户整体周额度分开消耗。
* five-hour-3p 与 seven-day-3p 是 Antigravity 的 Claude/GPT 额度池,与同一 Google 账户的
* Gemini 池(five-hour、seven-day)分开消耗。
* monthly 是按订阅日起算的月额度,目前只有服务端读取的 OpenCode Go 提供。
*/
window: "five-hour" | "seven-day" | "seven-day-fable" | "monthly";
window:
| "five-hour"
| "seven-day"
| "seven-day-fable"
| "five-hour-3p"
| "seven-day-3p"
| "monthly";
percent: number | null;
sampledAt: string | null;
resetsAt: string | null;
Expand Down
Loading
Loading
0