Skip to content

Commit 00e665b

Browse files
committed
feat(ui): 模型卡片分开标注 Go / GOAT 档位可用性
卡片头部此前只看压扁的 onGoPlan(= availability['individual-go']), GOAT 独有的 6 个模型(gpt-5.6-sol、xai/grok-4.6、google/gemini-3.7-flash、 google/gemini-3.8-flash、meta/muse-spark-1.2/1.3)与其它模型一样只显示 中性的「可用」,无法区分。 - 每张卡片底部新增「档位」行:GO / GOAT 两枚药丸始终占位,可用为实心 强调色 + ✓(GO 靛蓝、GOAT 金色带皇冠),不可用为灰底 + ✗;固定占位 让卡片等高、整列对齐,便于纵向比较。 - 判定改读完整 availability(新增 isPlanOn),仅在整表缺失时回退 onGoPlan。 - 两档都不含的 19 个模型改标「更高档位」,悬停给出实际可用档位清单。 - 筛选条新增 GOAT 标签;计数扩为 Go/GOAT 各自可用数;补一行图例。 - 不可用药丸用 slate-400(对比度约 5.3:1)替代偏暗的 slate-500。 - README 中英双语同步;docs/screenshots/dashboard-models.png 重拍。 测试:新增 dashboard-spa 回归断言;全量 237 项通过,tsc --noEmit 无错误; Chromium 实机验证 79 卡 ×2 药丸、GO 命中 54 / GOAT 命中 60 与目录一致。
1 parent a3b2bca commit 00e665b

6 files changed

Lines changed: 98 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
所有主要版本更新都记录在此文件。
44

5+
## [4.13.0] - 2026-09-18
6+
7+
### 改进
8+
- **模型卡片现在把 Go 与 GOAT 两个档位分开标注** — 此前卡片头部只判断压扁后的 `onGoPlan`(即 `availability['individual-go']`),GOAT 独有的模型因此完全没有档位标识:`gpt-5.6-sol`、`xai/grok-4.6`、`google/gemini-3.7-flash`、`google/gemini-3.8-flash`、`meta/muse-spark-1.2`、`meta/muse-spark-1.3` 这 6 个模型在 Go 档不可用、只在 GOAT 可用,却和其它模型一样只显示一枚中性的「可用」,无法区分。
9+
- 每张卡片底部新增固定的「档位」行:`GO`(靛蓝)与 `GOAT`(金色 + 皇冠)两枚药丸**始终占位**,可用为实心强调色 + ✓、不可用为灰底 + ✗。固定占位让卡片等高、整列纵向对齐,扫一眼即可比较两个档位,而不是"只在可用时才冒出一枚标签"。
10+
- 判定改为读上游完整的 `availability` 映射(前端新增 `isPlanOn(m, key)`),仅在 `availability` 整体缺失时才回退到旧的 `onGoPlan`;档位键与显示名的映射(`PLAN_LABELS`)与 `src/utils/plans.ts` 的 `PLAN_TIERS` 保持一致。
11+
- GO / GOAT 都不含的 19 个模型(含 claude-sonnet-5、claude-opus-5 等)不再显示笼统的「可用」,改标中性徽章「更高档位」,悬停可见其实际可用档位清单(如 `Pro · Pro (v1) · Provider · Max · Ultra · Team Pro`)。
12+
- 筛选条新增 `GOAT` 标签(与原 `GO` 并列),两者均走 `isPlanOn`;结果计数扩为「共 N 个 · 命中 M 个 · Go 档可用 X 个 · GOAT 档可用 Y 个」,并在筛选条下补一行图例说明药丸读法。
13+
- 可读性:不可用药丸文字用 `slate-400`(卡片底色上对比度约 5.3:1),而非更暗的 `slate-500`(约 3.2:1)——10px 字号下后者偏暗。
14+
- 版本号 `4.12.1` → `4.13.0`。README 中英双语「模型目录 / Dashboard」小节同步更新,`docs/screenshots/dashboard-models.png` 按新界面重拍(1440×900@2x)。
15+
16+
### 测试
17+
- 新增 `dashboard-spa.test.ts` 一项回归:断言 `GOAT` 筛选标签、`isPlanOn(m, 'individual-goat')` 与两枚 `planPill` 均已接入,且旧的 `if (m.onGoPlan) tags +=` 渲染路径已移除。
18+
- 全量 **237 项通过**,`tsc --noEmit` 无错误。
19+
- 实机验证(Chromium,1440×900):79 张卡片全部渲染出 2 枚档位药丸;`gpt-5.6-luna` = GO ✓/GOAT ✓、`gpt-5.6-sol` = GO ✗/GOAT ✓、`claude-sonnet-5` = 双 ✗ + 「更高档位」徽章;`GO` 筛选命中 54、`GOAT` 筛选命中 60,与目录数据一致;图例行不换行、与计数不重叠,药丸文字无溢出。
20+
521
## [4.12.1] - 2026-09-17
622

723
### 修复

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
| ![控制台总览](./docs/screenshots/dashboard-overview.png) | ![模型目录](./docs/screenshots/dashboard-models.png) | ![账号与鉴权](./docs/screenshots/dashboard-accounts.png) |
4949

5050
- **总览** — 一屏掌握运行状态:引擎启停、端口、运行时长、当前账号、绑定地址、鉴权开关、账号数与可用模型数;顶部一键切换引擎。
51-
- **模型目录** — 官方定价目录(上下文 / 输入 / 输出 / 缓存读 / 缓存写 / 能力 / Deal)实时刷新;支持关键词搜索、GO / FREE / DEAL / 视觉 / 推理标签筛选与多列排序。
51+
- **模型目录** — 官方定价目录(上下文 / 输入 / 输出 / 缓存读 / 缓存写 / 能力 / Deal)实时刷新;支持关键词搜索、GOAT / GO / FREE / DEAL / 视觉 / 推理标签筛选与多列排序;每张卡片底部的「档位」行固定标注该模型在 **Go** 与 **GOAT** 两个套餐下是否可用(GO 靛蓝、GOAT 金色带皇冠;不可用为灰底 ✗),两个档位都不含的模型另标「更高档位」并附可用档位清单。
5252
- **账号** — 浏览器 OAuth 登录或粘贴 Key;多账号管理与 5 小时额度轮换(≥90% 自动切换);密钥一律**脱敏显示**。
5353

5454
---
@@ -310,6 +310,7 @@ Point any OpenAI-style client (Cursor, Continue, Aider, OpenWebUI, Hermes, your
310310

311311
**Dashboard & usage insight**
312312

313+
- **Per-plan availability on every model card** — the badge row reads the upstream `availability` map instead of a single flattened Go flag, so each card states whether that model is usable on **Go** and on **GOAT** (GO indigo, GOAT gold with a crown; unavailable is a grey ✗). Models on neither plan are tagged *higher tiers* with the list of plans that do include them, and the filter row plus the result counter give Go/GOAT totals
313314
- Per-request session detail (tokens, cache hits, latency, cost, model, status) with daily trend, model doughnut and today/week/month cards; persisted to `~/.commandcode/usage-history.jsonl` (rotated at 20MB)
314315
- **Cost reconciled with the official bill** — prefers the authoritative `provider-metadata` amount (peak/off-peak and cache discounts included); local fallback prices cache read/write separately by time-of-day; estimates carry a `~` prefix
315316
- **Cache savings visualization** — how much cache hits saved versus full input price, and the multiple relative to billed cost
123 KB
Loading

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "commandcode-proxy-v4",
3-
"version": "4.12.1",
3+
"version": "4.13.0",
44
"description": "Local OpenAI Chat Completions & Anthropic Messages compatible proxy for Command Code AI. Chinese dashboard, hardened translation, retries, idle timeouts.",
55
"main": "dist/index.js",
66
"type": "module",

‎public/index.html‎

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,23 @@ <h3 class="font-bold text-xs text-white flex items-center gap-2">
317317
<div class="flex items-center gap-1.5 flex-wrap" id="modelTagRow">
318318
<button class="mfilter-chip on" data-tag="all">全部</button>
319319
<button class="mfilter-chip" data-tag="go">GO</button>
320+
<button class="mfilter-chip" data-tag="goat">GOAT</button>
320321
<button class="mfilter-chip" data-tag="free">FREE</button>
321322
<button class="mfilter-chip" data-tag="deal">DEAL</button>
322323
<button class="mfilter-chip" data-tag="vision">视觉</button>
323324
<button class="mfilter-chip" data-tag="reason">推理</button>
324325
</div>
325-
<p id="modelsCount" class="text-[11px] text-slate-500 text-right"></p>
326+
<div class="flex items-end justify-between gap-x-4 gap-y-1 flex-wrap">
327+
<p class="text-[11px] text-slate-500 leading-relaxed">
328+
每张卡片底部「档位」行标注该模型在两个套餐下的可用性:
329+
<span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded border font-semibold bg-indigo-500/15 text-indigo-300 border-indigo-500/40">GO<i class="fa-solid fa-check text-[9px] opacity-80"></i></span>
330+
/ <span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded border font-semibold bg-amber-500/15 text-amber-300 border-amber-500/40"><i class="fa-solid fa-crown text-[9px]"></i>GOAT<i class="fa-solid fa-check text-[9px] opacity-80"></i></span>
331+
为可用,
332+
<span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded border font-semibold bg-slate-800/40 text-slate-400 border-slate-700/70">GO<i class="fa-solid fa-xmark text-[9px] opacity-80"></i></span>
333+
为不可用。
334+
</p>
335+
<p id="modelsCount" class="text-[11px] text-slate-500 text-right"></p>
336+
</div>
326337
</div>
327338
<div id="modelsList" class="grid grid-cols-1 md:grid-cols-3 gap-3"></div>
328339
</section>
@@ -635,9 +646,43 @@ <h3 class="text-base font-bold text-white flex items-center gap-2"><i class="fa-
635646
let modelTagFilter = 'all';
636647
let modelSortMode = 'default';
637648
let modelQueryTimer = null;
649+
// 档位键名与显示名,需与 src/utils/plans.ts 的 PLAN_TIERS 保持一致。
650+
const PLAN_LABELS = {
651+
'individual-free': 'Free', 'individual-go': 'Go', 'individual-goat': 'GOAT',
652+
'individual-pro': 'Pro', 'individual-pro-v1': 'Pro (v1)', 'individual-provider': 'Provider',
653+
'individual-max': 'Max', 'individual-ultra': 'Ultra', 'teams-pro': 'Team Pro',
654+
};
655+
// 判定「档位可用」只看该档位自己的布尔值;availability 缺失时退回旧的 onGoPlan。
656+
function isPlanOn(m, key) {
657+
const a = m && m.availability;
658+
if (a && Object.keys(a).length) return a[key] === true;
659+
return key === 'individual-go' ? !!m.onGoPlan : false;
660+
}
661+
function higherPlanNames(avail) {
662+
return ['individual-pro', 'individual-pro-v1', 'individual-provider', 'individual-max', 'individual-ultra', 'teams-pro']
663+
.filter(k => avail[k] === true).map(k => PLAN_LABELS[k] || k);
664+
}
665+
const BADGE_TONES = {
666+
rose: 'bg-rose-500/15 text-rose-300 border-rose-500/30',
667+
amber: 'bg-amber-500/15 text-amber-300 border-amber-500/30',
668+
slate: 'bg-slate-500/15 text-slate-300 border-slate-500/30',
669+
};
670+
function badge(text, tone, title) {
671+
return '<span class="text-[10px] px-2 py-0.5 rounded border font-semibold ' + BADGE_TONES[tone] + '"' +
672+
(title ? ' title="' + esc(title) + '"' : '') + '>' + text + '</span>';
673+
}
674+
// 档位药丸:可用=实心强调色,不可用=灰底。两枚固定占位,卡片高度一致,
675+
// 整列纵向对齐后即可一眼比较 GO / GOAT 的差别。
676+
function planPill(key, label, on, onTone) {
677+
const cls = on ? onTone : 'bg-slate-800/40 text-slate-400 border-slate-700/70';
678+
return '<span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded border font-semibold ' + cls +
679+
'" title="' + (PLAN_LABELS[key] || key) + ' 档位:' + (on ? '可用' : '不可用') + '">' +
680+
label + '<i class="fa-solid ' + (on ? 'fa-check' : 'fa-xmark') + ' text-[9px] opacity-80"></i></span>';
681+
}
638682
function modelMatchesTag(m, tag) {
639683
const caps = m.caps || {};
640-
if (tag === 'go') return !!m.onGoPlan;
684+
if (tag === 'go') return isPlanOn(m, 'individual-go');
685+
if (tag === 'goat') return isPlanOn(m, 'individual-goat');
641686
if (tag === 'free') return !!(m.deal && m.deal.free);
642687
if (tag === 'deal') return !!(m.deal && m.deal.discountPercent);
643688
if (tag === 'vision') return !!(caps.vision || m.supports_vision);
@@ -678,7 +723,12 @@ <h3 class="text-base font-bold text-white flex items-center gap-2"><i class="fa-
678723
if (clearBtn) clearBtn.classList.toggle('hidden', !q);
679724
const filtered = sortModels(allModelsCache.filter(m => modelMatchesTag(m, modelTagFilter) && modelMatchesQuery(m, q)));
680725
const meta = document.getElementById('modelsCount');
681-
if (meta) meta.innerText = '共 ' + allModelsCache.length + ' 个 · 命中 ' + filtered.length + ' 个';
726+
if (meta) {
727+
const goCount = allModelsCache.filter(m => isPlanOn(m, 'individual-go')).length;
728+
const goatCount = allModelsCache.filter(m => isPlanOn(m, 'individual-goat')).length;
729+
meta.innerText = '共 ' + allModelsCache.length + ' 个 · 命中 ' + filtered.length +
730+
' 个 · Go 档可用 ' + goCount + ' 个 · GOAT 档可用 ' + goatCount + ' 个';
731+
}
682732
const c = document.getElementById('modelsList');
683733
if (!filtered.length) {
684734
c.innerHTML = '<div class="col-span-full text-center py-12 text-slate-500 text-xs"><i class="fa-solid fa-magnifying-glass text-2xl mb-3 block text-slate-600"></i>没有匹配的模型,换个关键词或标签试试</div>';
@@ -687,11 +737,17 @@ <h3 class="text-base font-bold text-white flex items-center gap-2"><i class="fa-
687737
c.innerHTML = filtered.map(m => {
688738
const p = m.pricing || {};
689739
const caps = m.caps || {};
740+
const avail = m.availability || {};
741+
const goOn = isPlanOn(m, 'individual-go');
742+
const goatOn = isPlanOn(m, 'individual-goat');
743+
const higher = higherPlanNames(avail);
690744
let tags = '';
691-
if (m.onGoPlan) tags += '<span class="text-[10px] px-2 py-0.5 rounded bg-indigo-500/15 text-indigo-300 border border-indigo-500/30 font-semibold">GO</span>';
692-
if (m.deal && m.deal.free) tags += '<span class="text-[10px] px-2 py-0.5 rounded bg-rose-500/15 text-rose-300 border border-rose-500/30 font-semibold">FREE</span>';
693-
else if (m.deal && m.deal.discountPercent) tags += '<span class="text-[10px] px-2 py-0.5 rounded bg-amber-500/15 text-amber-300 border border-amber-500/30 font-semibold">DEAL ' + m.deal.discountPercent + '%</span>';
694-
if (!tags) tags = '<span class="text-[10px] px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold">可用</span>';
745+
if (m.deal && m.deal.free) tags += badge('FREE', 'rose');
746+
else if (m.deal && m.deal.discountPercent) tags += badge('DEAL ' + m.deal.discountPercent + '%', 'amber');
747+
// GO/GOAT 都不可用时,用一枚中性徽章说明它属于更高的付费档位,避免误读成"不可调用"。
748+
if (!goOn && !goatOn && higher.length) {
749+
tags += badge('更高档位', 'slate', '仅在这些档位可用:' + higher.join(' · '));
750+
}
695751
const capsStr = '文字' + (caps.text ? '✓' : '✗') + ' · 视觉' + ((caps.vision || m.supports_vision) ? '✓' : '✗') + ' · 推理' + (caps.reasoning ? '✓' : '✗');
696752
return '<div class="p-3 bg-slate-900 border border-slate-800 rounded-lg hover:border-indigo-500/40 transition">' +
697753
'<div class="flex items-start justify-between gap-2">' +
@@ -707,6 +763,11 @@ <h3 class="text-base font-bold text-white flex items-center gap-2"><i class="fa-
707763
'<span>缓存写:<span class="text-slate-200">' + fmtPrice(p.cacheWrite) + '</span></span>' +
708764
'<span class="text-slate-500">' + capsStr + '</span>' +
709765
'</div>' +
766+
'<div class="mt-2 pt-2 border-t border-slate-800/70 flex items-center gap-1.5">' +
767+
'<span class="text-[10px] text-slate-500">档位</span>' +
768+
planPill('individual-go', 'GO', goOn, 'bg-indigo-500/15 text-indigo-300 border-indigo-500/40') +
769+
planPill('individual-goat', goatOn ? '<i class="fa-solid fa-crown text-[9px]"></i>GOAT' : 'GOAT', goatOn, 'bg-amber-500/15 text-amber-300 border-amber-500/40') +
770+
'</div>' +
710771
'</div>';
711772
}).join('');
712773
}

‎tests/dashboard-spa.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,15 @@ describe('dashboard SPA (public/index.html)', () => {
4040
it('does not reference external CDNs (assets are localized)', () => {
4141
expect(html).not.toMatch(/https?:\/\/cdn\.|https?:\/\/cdnjs\.cloudflare\.com|https?:\/\/cdn\.jsdelivr\.net/);
4242
});
43+
44+
// 模型卡片必须能把 GO 与 GOAT 两个档位分开标注:只看 onGoPlan 会让
45+
// GOAT-only 的模型(如 gpt-5.6-sol)显示成没有档位区别的「可用」。
46+
it('renders both GO and GOAT plan badges from the per-plan availability map', () => {
47+
expect(html).toMatch(/data-tag="goat"/);
48+
expect(html).toMatch(/isPlanOn\(m, 'individual-go'\)/);
49+
expect(html).toMatch(/isPlanOn\(m, 'individual-goat'\)/);
50+
expect(html).toMatch(/planPill\('individual-go'/);
51+
expect(html).toMatch(/planPill\('individual-goat'/);
52+
expect(html).not.toMatch(/if \(m\.onGoPlan\) tags \+=/);
53+
});
4354
});

0 commit comments

Comments
 (0)