Skip to content

Commit f7aab88

Browse files
committed
style(web): 全站图标、凭证池百分比与 favicon
- PageHeader/Metric/Empty/图表指标筛选补图标(与顶栏导航一致性) - 凭证卡新增配额已用百分比;顶栏加项目仓库链接(Globe) - 新增 favicon(品牌汇聚节点同款) - README「上游」统一为「渠道」;移除凭证账号切换入口的死代码
1 parent 3c0a73d commit f7aab88

15 files changed

Lines changed: 174 additions & 193 deletions

‎README.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Coding2API
22

3-
把 **CodeBuddy** 与 **TRAE SOLO** 两个 coding agent 上游通道,统一封装为 OpenAI 兼容 API,
3+
把 **CodeBuddy** 与 **TRAE SOLO** 两个 coding agent 渠道,统一封装为 OpenAI 兼容 API,
44
并提供公共凭证池、统一调度与按人用量统计。
55

66
> [!WARNING]
@@ -10,17 +10,17 @@
1010
## 特性
1111

1212
- **OpenAI 兼容出口**:`/v1/chat/completions`(流式 + 非流式)、`/v1/models`
13-
- **双上游统一调度**:同一扁平模型名可按健康度自动选号,`model@provider` 可强制指定
13+
- **双渠道统一调度**:同一扁平模型名可按健康度自动选号,`model@provider` 可强制指定
1414
- **三态健康度 + 三级冷却**:权益耗尽 12h / 限流 60s / 连续错误 10m / 会话失效硬禁用
1515
- **公共凭证池**:管理员集中维护,全员共享;按人统计用量
1616
- **凭证加密入库**:Fernet(AES-128-CBC + HMAC),密钥走 `APP_SECRET`
1717
- **脱敏统计**:不保存提示词、回答、请求头、Token、工具参数;明细 90 天、小时汇总永久
1818
- **完整凭证运维**:OAuth 设备码登录、多账号切换、额度探测、每日签到、token 预刷新
19-
- **用量可视化**:请求量趋势(按上游)、按模型趋势(Top N)、按上游分组、逐请求明细
19+
- **用量可视化**:请求量趋势(按渠道)、按模型趋势(Top N)、按渠道分组、逐请求明细
2020
(保留 90 天,游标翻页);8 项总览指标合并为 4 张卡片;品牌 logo 标注渠道
2121
- **管理台安全加固**:登录三级限流(全局/IP/用户名)+ PBKDF2 并发上限、写操作 CSRF 校验、
2222
请求体上限(登录 8KB / 其余 16MB)、安全响应头(CSP `frame-ancestors`)与 Host 白名单
23-
- **模型目录治理**:`MODEL_BLOCKLIST` 过滤内部/老模型;上游拉取失败用缓存兜底;
23+
- **模型目录治理**:`MODEL_BLOCKLIST` 过滤内部/老模型;渠道拉取失败用缓存兜底;
2424
消耗倍率与 token 上限/能力字段透传到列表,Playground 选中即览
2525

2626
## 快速开始
@@ -95,7 +95,7 @@ curl http://127.0.0.1:8000/v1/chat/completions \
9595
-d '{"model":"glm-5.2","messages":[{"role":"user","content":"你好"}]}'
9696
```
9797

98-
强制走某个上游:
98+
强制走某个渠道:
9999

100100
```bash
101101
-d '{"model":"glm-5.2@trae", ...}' # 只走 TRAE
@@ -114,7 +114,7 @@ curl http://127.0.0.1:8000/v1/chat/completions \
114114

115115
「Playground」页用登录会话直接测试,无需 API Key;用量计入当前用户。
116116
选中模型后下方显示**消耗倍率、最大输入/输出、图片与工具调用支持**,
117-
双上游倍率不同时会按渠道(前置 icon 标注)分别显示。
117+
双渠道倍率不同时会按渠道(前置 icon 标注)分别显示。
118118

119119
## 功能对照表
120120

@@ -128,11 +128,11 @@ curl http://127.0.0.1:8000/v1/chat/completions \
128128
| 看懂冷却时间 | 凭证管理 → 状态列 | 到期自动恢复,不需要手动处理 |
129129
| 固定用某个账号 | 凭证管理 → 「指定」 | 全局唯一,请求只走它 |
130130
| 切换个人/企业账号 | 凭证管理 → 「账号」 | 仅 CodeBuddy 支持 |
131-
| 强制走某个上游 | 模型名写 `glm-5.2@trae` | 不写则自动选健康的 |
131+
| 强制走某个渠道 | 模型名写 `glm-5.2@trae` | 不写则自动选健康的 |
132132
| 接入第三方客户端 | 客户端设置 Base URL | 地址加 `/v1`,Key 用 `sk-…` |
133133
| 查某个人用了多少 | 用量统计 → 用户名筛选 | 仅管理员;普通用户只见自己 |
134-
| 统计里 credit 是 — | 用量统计 | 上游可选字段,经常不返回;主指标是 token |
135-
| Token 卡片里命中/未命中是 — | 用量统计 | 上游未上报缓存命中(cached_tokens)时无法拆分输入 |
134+
| 统计里 credit 是 — | 用量统计 | 渠道可选字段,经常不返回;主指标是 token |
135+
| Token 卡片里命中/未命中是 — | 用量统计 | 渠道未上报缓存命中(cached_tokens)时无法拆分输入 |
136136
| 看某个模型消耗多快 | Playground → 选中模型 | 下方显示倍率 / token 上限 / 图片与工具支持 |
137137
| 列表里老模型/内部模型太多 | `MODEL_BLOCKLIST` env | glob 黑名单;只影响列表展示,直连指定不受影响 |
138138

@@ -158,7 +158,7 @@ curl http://127.0.0.1:8000/v1/chat/completions \
158158
| `USERS_FILE` | `secrets/users.txt` | 用户文件路径 |
159159
| `DATA_DIR` | `./data` | SQLite 与运行数据目录 |
160160
| `PUBLIC_BASE_URL` | `http://127.0.0.1:8000` | 浏览器可达地址;TRAE 登录回调依赖它 |
161-
| `CODEBUDDY_API_ENDPOINT` | 中国站 | 上游地址,只接受 `CODEBUDDY_ALLOWED_ENDPOINTS` 白名单内地址 |
161+
| `CODEBUDDY_API_ENDPOINT` | 中国站 | 渠道地址,只接受 `CODEBUDDY_ALLOWED_ENDPOINTS` 白名单内地址 |
162162
| `CODEBUDDY_ALLOWED_ENDPOINTS` | 中/国际站 | 逗号分隔白名单;不在其中启动即失败 |
163163
| `DEFAULT_MODEL` | `glm-5.2` | 模型为空或 `auto` 时的目标 |
164164
| `QUOTA_PROBE_MINUTES` | `60` | 额度探测周期 |

‎web/index.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
67
<title>Coding2API 管理台</title>
78
<script>
89
// 挂载前读取主题,避免暗色下白屏闪烁;默认跟随系统偏好

‎web/public/favicon.svg‎

Lines changed: 9 additions & 0 deletions
Loading

‎web/src/Layout.tsx‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NavLink, Outlet, useOutletContext } from "react-router-dom";
22
import {
33
BarChart3,
44
Database,
5+
Globe,
56
KeyRound,
67
LayoutDashboard,
78
TerminalSquare,
@@ -19,6 +20,26 @@ const NAV = [
1920
{ to: "/playground", label: "Playground", icon: TerminalSquare },
2021
];
2122

23+
/** 品牌标记:多路渠道管道汇聚进单一出口(Coding2API 的产品故事)。 */
24+
export function BrandMark({ className }: { className?: string }) {
25+
return (
26+
<span
27+
className={cn(
28+
"grid place-items-center rounded-lg bg-primary text-primary-foreground",
29+
className,
30+
)}
31+
aria-hidden
32+
>
33+
<svg viewBox="0 0 16 16" className="size-[60%]" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
34+
<path d="M2.5 3.5 6.5 8 2.5 12.5" />
35+
<path d="M13.5 3.5 9.5 8l4 4.5" />
36+
<path d="M6.5 8h4" />
37+
<circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none" />
38+
</svg>
39+
</span>
40+
);
41+
}
42+
2243
export function Layout({ session }: { session: SessionInfo }) {
2344
const logout = async () => {
2445
await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin" });
@@ -30,9 +51,7 @@ export function Layout({ session }: { session: SessionInfo }) {
3051
<header className="sticky top-0 z-20 border-b border-border bg-[color:color-mix(in_oklch,var(--background)_88%,transparent)] backdrop-blur">
3152
<div className="mx-auto flex w-full max-w-7xl items-center gap-3 px-4 py-2.5 sm:gap-4 sm:px-6">
3253
<span className="flex items-center gap-2 font-semibold tracking-tight">
33-
<span className="grid size-7 place-items-center rounded-lg bg-primary text-primary-foreground">
34-
<TerminalSquare className="size-4" />
35-
</span>
54+
<BrandMark className="size-7" />
3655
Coding2API
3756
</span>
3857
<nav className="flex flex-1 gap-1">
@@ -45,16 +64,30 @@ export function Layout({ session }: { session: SessionInfo }) {
4564
cn(
4665
"flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm transition-colors sm:px-3",
4766
isActive
48-
? "bg-primary/12 font-medium text-primary"
67+
? "bg-primary/10 font-medium text-primary"
4968
: "text-muted-foreground hover:bg-muted hover:text-foreground",
5069
)
5170
}
5271
>
53-
<item.icon className="size-4 shrink-0" />
54-
<span className="hidden sm:inline">{item.label}</span>
72+
{() => (
73+
<>
74+
<item.icon className="size-4 shrink-0" />
75+
<span className="hidden sm:inline">{item.label}</span>
76+
</>
77+
)}
5578
</NavLink>
5679
))}
5780
</nav>
81+
<a
82+
href="https://github.com/RobbsLuo/coding2api"
83+
target="_blank"
84+
rel="noopener noreferrer"
85+
aria-label="项目仓库"
86+
title="项目仓库"
87+
className="grid size-8 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
88+
>
89+
<Globe className="size-4" />
90+
</a>
5891
<ThemeToggle />
5992
<UserMenu
6093
username={session.username}

‎web/src/api/__tests__/client.test.tsx‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,11 @@ describe("api client", () => {
112112
expect.objectContaining({ method: "POST" }),
113113
);
114114

115-
await api.selectAccount("c1", "a1");
116-
expect(spy).toHaveBeenCalledWith(
117-
"/api/credentials/c1/accounts/select",
118-
expect.objectContaining({ method: "POST" }),
119-
);
120-
121115
await api.upstreamStart("codebuddy");
122116
await api.upstreamPoll("codebuddy", "s");
123117
await api.upstreamCancel("codebuddy", "s");
124118
await api.probeCredential("c1");
125119
await api.checkinCredential("c1");
126-
await api.accounts("c1");
127120
await api.apiKeys();
128121
await api.createApiKey("n");
129122
await api.deleteApiKey("k1");
@@ -173,7 +166,7 @@ describe("display helpers", () => {
173166
expect(healthView(undefined as unknown as null).kind).toBe("unknown");
174167
});
175168

176-
it("周期语义按上游类型判定,而不是看 cycle_end 是否存在", () => {
169+
it("周期语义按渠道类型判定,而不是看 cycle_end 是否存在", () => {
177170
// CodeBuddy 有重置时间
178171
const cycle = quotaSemantics(makeCredential({
179172
provider: "codebuddy", quota_cycle_end: 1_800_000_000 }) as never);

‎web/src/components/PageHeader.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import type { ReactNode } from "react";
44
export function PageHeader({
55
title,
66
description,
7+
icon,
78
}: {
89
title: string;
910
description?: ReactNode;
11+
icon?: ReactNode;
1012
}) {
1113
return (
1214
<div className="space-y-1">
13-
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
15+
<h1 className="flex items-center gap-2 text-xl font-semibold tracking-tight">
16+
{icon && <span className="text-primary">{icon}</span>}
17+
{title}
18+
</h1>
1419
{description && <p className="text-sm leading-relaxed text-muted-foreground">{description}</p>}
1520
</div>
1621
);

‎web/src/components/ui/card.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function Card({
1111
data-slot="card"
1212
data-size={size}
1313
className={cn(
14-
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
14+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-border [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
1515
className
1616
)}
1717
{...props}

‎web/src/pages/ApiKeysPage.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Check, Copy, Plus, Trash2 } from "lucide-react";
1+
import { Check, Copy, KeyRound, Plus, Trash2 } from "lucide-react";
22
import { useState } from "react";
33
import { api } from "../api/client";
44
import { useApiKeys, useQueryClient } from "../api/hooks";
@@ -192,6 +192,7 @@ export function ApiKeysPage() {
192192
<PageHeader
193193
title="API Key 管理"
194194
description="创建外部客户端(ChatGPT Next Web、LobeChat、Cursor 等)接入用的 Key,协议与 OpenAI 兼容。Key 仅在创建时完整显示一次,请立即保存;用量按 Key 归属用户统计。"
195+
icon={<KeyRound className="size-5" />}
195196
/>
196197
<OpenAIEntry apiKey={created?.api_key ?? ""} />
197198

0 commit comments

Comments
 (0)