597665f3c8
计费需 token↔真钱比率,配置归管理端。本次落地"按模型·分输入/输出"粒度: 后端(gateway): - store.Pricing 模型(BaseModel + model_id 唯一 + input_per_1k/output_per_1k + currency), AutoMigrate 建 sundynix_pricing;ListPricing/UpsertPricing(OnConflict model_id 覆盖)。 - admin handler:GET /admin/pricing 列表、PUT /admin/pricing 设置(校验非负,币种默认 CNY), 挂在 RequireAdmin 组下。 前端(admin): - api:listPricing/savePricing(带 Bearer)。 - PricingPage:列出所有已登记模型(chat+embedding),每行可编辑 输入/输出每1K单价 + 币种,逐行保存。 - routes 新增「计价」页(配置组)。 实测:PUT→ok;GET 返回正确行;重复 PUT 同 model_id 仍 1 行且值更新(upsert 生效);表自动迁移。 前端 tsc 干净。下一步可做用量计量 × 单价折算(真正计费)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
1.9 KiB
TypeScript
69 lines
1.9 KiB
TypeScript
import { lazy, type ReactNode } from "react";
|
|
import { Soon } from "./components/Soon";
|
|
|
|
// 路由注册表 —— 控制台的单一事实源:导航 + 内容都从这里派生。
|
|
// 新增页面 = 在此加一条;real 页面用 lazy 懒加载(代码分割)。
|
|
const ModelsPage = lazy(() => import("./pages/ModelsPage").then((m) => ({ default: m.ModelsPage })));
|
|
const DatasourcesPage = lazy(() => import("./pages/DatasourcesPage").then((m) => ({ default: m.DatasourcesPage })));
|
|
const PricingPage = lazy(() => import("./pages/PricingPage").then((m) => ({ default: m.PricingPage })));
|
|
|
|
export interface RouteDef {
|
|
path: string;
|
|
label: string;
|
|
group: string;
|
|
ready?: boolean;
|
|
element: ReactNode;
|
|
}
|
|
|
|
export const routes: RouteDef[] = [
|
|
{
|
|
path: "/models",
|
|
label: "模型",
|
|
group: "配置",
|
|
ready: true,
|
|
element: <ModelsPage />,
|
|
},
|
|
{
|
|
path: "/datasources",
|
|
label: "数据源",
|
|
group: "配置",
|
|
ready: true,
|
|
element: <DatasourcesPage />,
|
|
},
|
|
{
|
|
path: "/pricing",
|
|
label: "计价",
|
|
group: "配置",
|
|
ready: true,
|
|
element: <PricingPage />,
|
|
},
|
|
{
|
|
path: "/tenants",
|
|
label: "租户",
|
|
group: "平台",
|
|
element: <Soon title="租户 / 工作区" desc="多租户隔离、配额、用户与计费。垂直行业平台级复制的基座。" />,
|
|
},
|
|
{
|
|
path: "/guardrails",
|
|
label: "护栏",
|
|
group: "平台",
|
|
element: <Soon title="护栏" desc="输入/输出 Guardrail 规则(脱敏 / 免责 / 强制引用)。受监管垂直必备。" />,
|
|
},
|
|
];
|
|
|
|
export const defaultPath = "/models";
|
|
|
|
// 派生分组导航(保持注册顺序)。
|
|
export function navGroups(): Array<{ group: string; items: RouteDef[] }> {
|
|
const out: Array<{ group: string; items: RouteDef[] }> = [];
|
|
for (const r of routes) {
|
|
let g = out.find((x) => x.group === r.group);
|
|
if (!g) {
|
|
g = { group: r.group, items: [] };
|
|
out.push(g);
|
|
}
|
|
g.items.push(r);
|
|
}
|
|
return out;
|
|
}
|