M0-M4: 推倒重来基线(基建+用户/密钥/核心代理+前端+管理后台+三协议互转)

- 后端 Go+Gin+GORM: 配置(OT_ env)/SQLite/Postgres 双驱动、用户体系(argon2id+JWT access/refresh)、
  API Key(sk- 48位, 仅存 SHA-256 哈希)
- 代理网关: /v1/chat/completions、/v1/responses、/v1/messages、/v1/models;错误按客户端协议返回
- 三协议互转(convert 包): Chat↔Messages↔Responses 请求/响应 + 流式 SSE 逐事件转换(直通优先)
- 用量计费: 异步批量记账、余额扣减、balance_logs、usage_daily 日聚合
- 管理 API: 用户/渠道 CRUD+测试+模型导入/模型定价+绑定/统计/系统配置
- 前端 Vue3+TS+Tailwind(taste-skill 设计 tokens): Landing/登录注册/控制台/管理后台,
  自建组件+Phosphor 图标+自建 SVG 趋势图, 已过 web-design-guidelines 复查
- mock 上游: OpenAI+Anthropic 双协议模拟(含流式)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 15:34:06 +08:00
co-authored by Claude
parent b25e9ec8a7
commit ec4de8d913
92 changed files with 6203 additions and 3064 deletions
+145 -76
View File
@@ -1,105 +1,174 @@
<script setup lang="ts">
import { useAuthStore } from '../stores/auth'
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const primaryAction = computed(() => (auth.isAuthed ? '/console/dashboard' : '/register'))
const protocols = [
{ name: 'POST /v1/chat/completions', desc: 'OpenAI Chat Completions · 流式 + 工具调用' },
{ name: 'POST /v1/responses', desc: 'OpenAI Responses API · 新一代生态' },
{ name: 'POST /v1/messages', desc: 'Anthropic Messages · Claude 原生格式' },
{ name: 'GET /v1/models', desc: 'OpenAI 风格模型列表' },
]
</script>
<template>
<div class="min-h-[100dvh] bg-ink-950 text-paper-100">
<!-- 顶部导航 -->
<header class="sticky top-0 z-40 border-b border-ink-800 bg-ink-950/90 backdrop-blur">
<div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<router-link to="/" class="flex items-center gap-2.5" aria-label="openteam 首页">
<span class="flex h-7 w-7 items-center justify-center rounded-md bg-signal-400" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 8h3l2-5 3 10 2-5h2" stroke="#0c0d0f" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span class="text-[15px] font-semibold tracking-tight" translate="no">openteam</span>
<span class="ml-1 rounded border border-ink-700 px-1.5 py-px font-mono text-[10px] text-paper-500" translate="no">relay</span>
</router-link>
<nav class="flex items-center gap-2" aria-label="站内导航">
<div class="min-h-[100dvh] bg-zinc-950 text-zinc-100">
<!-- 导航 -->
<header class="sticky top-0 z-40 border-b border-zinc-800/60 bg-zinc-950/80 backdrop-blur">
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<div class="flex items-center gap-2">
<img src="/favicon.svg" alt="" class="size-5" />
<span class="text-sm font-semibold tracking-tight">openteam</span>
</div>
<nav class="flex items-center gap-2">
<router-link
v-if="!auth.isAuthed"
to="/login"
class="rounded-md px-3 py-2 text-sm text-paper-500 transition-colors hover:bg-ink-800 hover:text-paper-100 focus-visible:ring-2 focus-visible:ring-signal-400/60 focus:outline-none"
>登录</router-link>
class="rounded-md px-3 py-1.5 text-sm text-zinc-400 transition hover:text-zinc-100"
>
登录
</router-link>
<router-link v-else to="/console/dashboard" class="rounded-md px-3 py-1.5 text-sm text-zinc-400 transition hover:text-zinc-100">
控制台
</router-link>
<router-link
:to="primaryAction"
class="inline-flex h-9 touch-manipulation items-center rounded-md bg-signal-400 px-4 text-sm font-medium text-ink-950 transition-colors hover:bg-signal-300 focus-visible:ring-2 focus-visible:ring-signal-400/60 focus:outline-none"
>开始使用</router-link>
v-if="!auth.isAuthed"
to="/register"
class="rounded-md bg-accent px-3.5 py-1.5 text-sm font-medium text-zinc-950 transition hover:bg-accent-strong"
>
免费注册
</router-link>
</nav>
</div>
</header>
<main id="landing-main">
<!-- Hero:左对齐,信号路径示意 -->
<section class="mx-auto grid max-w-6xl grid-cols-1 items-center gap-14 px-6 pt-16 pb-20 lg:grid-cols-[1.1fr_0.9fr]">
<div>
<p class="font-mono text-xs uppercase tracking-[0.2em] text-signal-400" translate="no">self-hosted llm relay</p>
<h1 class="mt-4 text-4xl leading-[1.05] font-bold tracking-tight text-balance md:text-5xl">
一个 Key,<br />接入全部模型
<!-- Hero:左文案 + 右真实调用演示 -->
<section class="relative overflow-hidden">
<div class="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-emerald-500/40 to-transparent" />
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 pt-20 pb-16 lg:grid-cols-2 lg:pt-24">
<div class="max-w-xl">
<p class="mb-3 font-mono text-xs tracking-wide text-emerald-400/80">LLM API 中转网关</p>
<h1 class="text-4xl leading-none font-semibold tracking-tight md:text-5xl">
一个 Key,调用所有主流模型
</h1>
<p class="mt-5 max-w-[52ch] text-base leading-relaxed text-paper-500">
自托管 LLM API 中转网关。统一 OpenAI 与 Anthropic 协议入口,背后对接任意上游渠道,用量计费一目了然。
<p class="mt-5 max-w-md text-base leading-relaxed text-zinc-400">
统一 OpenAI 与 Anthropic 协议入口,三套 API 自动互转,用量、计费与 API Key 管理开箱即用。
</p>
<div class="mt-8 flex items-center gap-3">
<router-link
to="/register"
class="inline-flex h-10 touch-manipulation items-center rounded-md bg-signal-400 px-5 text-sm font-medium text-ink-950 transition-[background-color,transform] hover:bg-signal-300 active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-signal-400/60 focus:outline-none"
>立即开始</router-link>
<a
href="#protocols"
class="inline-flex h-10 touch-manipulation items-center rounded-md border border-ink-600 px-5 text-sm text-paper-300 transition-colors hover:border-signal-400 hover:text-signal-300 focus-visible:ring-2 focus-visible:ring-signal-400/60 focus:outline-none"
>查看端点</a>
class="inline-flex h-10 items-center rounded-md bg-accent px-5 text-sm font-medium text-zinc-950 transition hover:bg-accent-strong active:scale-[0.98]"
>
免费注册
</router-link>
<router-link
to="/login"
class="inline-flex h-10 items-center rounded-md border border-zinc-700 px-5 text-sm text-zinc-200 transition hover:border-zinc-500"
>
登录
</router-link>
</div>
<p class="mt-5 font-mono text-xs text-zinc-600">不用换 SDK,改一行 base_url 即可接入</p>
</div>
<!-- 信号路径:client → gateway → upstream -->
<div class="rounded-lg border border-ink-700 bg-ink-900 p-5 font-mono text-[13px]" aria-label="请求链路示意">
<div class="flex items-center gap-3 pb-4">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-mint-400" aria-hidden="true" />
<span class="text-xs text-paper-500" translate="no">request path · live</span>
<!-- 调用演示(真实格式,非伪截图) -->
<div class="card overflow-hidden font-mono text-xs">
<div class="flex items-center gap-1.5 border-b border-zinc-800 px-4 py-2.5">
<span class="size-2.5 rounded-full bg-zinc-700" />
<span class="size-2.5 rounded-full bg-zinc-700" />
<span class="ml-2 text-zinc-500">curl api.openteam.dev</span>
</div>
<div class="space-y-2">
<div class="rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-paper-300">client <span class="text-signal-400" aria-hidden="true">──▶</span> <span class="text-paper-500" translate="no">/v1/chat/completions</span></div>
<div class="rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-paper-300">gateway <span class="text-signal-400" aria-hidden="true">──▶</span> <span class="text-paper-500" translate="no">auth · quota · route</span></div>
<div class="rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-paper-300">upstream <span class="text-mint-400" aria-hidden="true">◀──</span> <span class="text-paper-500" translate="no">openai / anthropic</span></div>
<div class="rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-paper-300">billing <span class="text-mint-400" aria-hidden="true">──▶</span> <span class="num text-paper-500" translate="no">usage · cost · ledger</span></div>
</div>
</div>
</section>
<!-- 协议端点 -->
<section id="protocols" class="scroll-mt-16 border-t border-ink-800 bg-ink-900/50">
<div class="mx-auto max-w-6xl px-6 py-16">
<h2 class="text-xl font-semibold tracking-tight">三套协议,一个入口</h2>
<p class="mt-2 max-w-[60ch] text-sm leading-relaxed text-paper-500">
OpenAI 与 Anthropic 生态的 SDK 与客户端无需改动,直接指向本网关。协议间自动互转。
</p>
<div class="mt-8 grid grid-cols-1 gap-3 md:grid-cols-2">
<div v-for="p in protocols" :key="p.name" class="rounded-lg border border-ink-700 bg-ink-900 p-4">
<code class="font-mono text-[13px] text-signal-300" translate="no">{{ p.name }}</code>
<p class="mt-1.5 text-[13px] text-paper-500">{{ p.desc }}</p>
<div class="space-y-3 p-4 leading-relaxed">
<div>
<p class="text-zinc-400"><span class="text-emerald-400">$</span> curl https://api.openteam.dev/v1/chat/completions</p>
<p class="text-zinc-400"> -H <span class="text-emerald-300">"Authorization: Bearer sk-..."</span> \</p>
<p class="text-zinc-400"> -d <span class="text-zinc-300">'{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "你好"}]}'</span></p>
</div>
<div class="border-t border-zinc-800 pt-3 text-zinc-500">
<p class="text-zinc-600"># OpenAI 格式请求,网关自动转 Anthropic 协议</p>
<p class="text-zinc-300">data: {"id":"resp_1","model":"claude-sonnet-5","choices":[{</p>
<p class="text-zinc-300">&nbsp;&nbsp;"delta":{"content":"你好,这是流式回复"}</p>
<p class="text-zinc-300">}]}</p>
<p class="text-zinc-300">data: [DONE]</p>
</div>
</div>
</div>
</section>
</div>
</section>
<!-- 页脚 -->
<footer class="border-t border-ink-800">
<div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-6 text-xs text-paper-600">
<span>openteam · 自托管 LLM 中转网关</span>
<span class="font-mono" translate="no">v0.1 · M1</span>
<!-- 协议入口 -->
<section class="border-t border-zinc-800/60">
<div class="mx-auto max-w-6xl px-4 py-16">
<h2 class="text-2xl font-semibold tracking-tight">三套协议,一个入口</h2>
<p class="mt-2 max-w-xl text-sm text-zinc-500">
对客户端暴露统一的 OpenAI 兼容入口;客户端协议与上游渠道不匹配时自动转换,无需关心背后接的是哪家。
</p>
<div class="card mt-8 divide-y divide-zinc-800/70">
<div v-for="p in [
{ path: 'POST /v1/chat/completions', desc: 'OpenAI Chat,兼容面最广,SDK 与工具链最全', tag: 'OpenAI' },
{ path: 'POST /v1/responses', desc: 'OpenAI Responses,新一代 SDK 与 Agents 首选', tag: 'OpenAI' },
{ path: 'POST /v1/messages', desc: 'Anthropic Messages,Claude 生态原生格式', tag: 'Anthropic' },
{ path: 'GET /v1/models', desc: 'OpenAI 风格模型列表', tag: 'List' },
]" :key="p.path" class="grid gap-1 px-5 py-3.5 sm:grid-cols-3 sm:items-center">
<code class="font-mono text-sm text-emerald-300">{{ p.path }}</code>
<p class="text-sm text-zinc-400 sm:col-span-1">{{ p.desc }}</p>
<span class="hidden justify-self-end font-mono text-[11px] text-zinc-600 sm:block">{{ p.tag }}</span>
</div>
</div>
</footer>
</main>
</div>
</section>
<!-- 网关能力 -->
<section class="border-t border-zinc-800/60">
<div class="mx-auto max-w-6xl px-4 py-16">
<h2 class="text-2xl font-semibold tracking-tight">网关替你处理的事</h2>
<div class="mt-8 grid gap-4 lg:grid-cols-3">
<div class="card p-5 lg:col-span-2">
<p class="font-mono text-xs text-emerald-400/80">responses → claude-sonnet-5</p>
<h3 class="mt-2 text-lg font-semibold">协议自动转换</h3>
<p class="mt-2 text-sm leading-relaxed text-zinc-500">
客户端按 Responses 调用 Claude 模型,网关转成 Anthropic 协议打给上游,再以 Responses 事件流式返回。直通优先,能力无损时零转换。
</p>
</div>
<div class="card p-5">
<p class="font-mono text-xs text-zinc-600">usage · daily</p>
<h3 class="mt-2 text-lg font-semibold">用量与计费</h3>
<p class="mt-2 text-sm leading-relaxed text-zinc-500">
请求级 token 统计,按模型价格自动扣费,日粒度报表与余额流水可追溯。
</p>
</div>
<div class="card p-5">
<p class="font-mono text-xs text-zinc-600">sk-…</p>
<h3 class="mt-2 text-lg font-semibold">API Key 管理</h3>
<p class="mt-2 text-sm leading-relaxed text-zinc-500">
密钥仅存哈希,支持配额、过期与模型白名单,创建时一次性展示。
</p>
</div>
<div class="card p-5 lg:col-span-2">
<p class="font-mono text-xs text-zinc-600">channels · lb · health</p>
<h3 class="mt-2 text-lg font-semibold">多渠道接入</h3>
<p class="mt-2 text-sm leading-relaxed text-zinc-500">
一个模型绑定多个上游渠道,按权重与健康状态选择,故障自动转移。管理员在后台一键接入新渠道、导入模型并定价。
</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="border-t border-zinc-800/60">
<div class="mx-auto max-w-6xl px-4 py-20 text-center">
<h2 class="text-2xl font-semibold tracking-tight">自托管,密钥在自己手里</h2>
<p class="mx-auto mt-3 max-w-md text-sm text-zinc-500">Docker Compose 一键部署,PostgreSQL 存账,渠道密钥加密存储。</p>
<router-link
to="/register"
class="mt-8 inline-flex h-10 items-center rounded-md bg-accent px-6 text-sm font-medium text-zinc-950 transition hover:bg-accent-strong active:scale-[0.98]"
>
开始使用
</router-link>
</div>
</section>
<footer class="border-t border-zinc-800/60">
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-6 text-xs text-zinc-600">
<div class="flex items-center gap-2">
<img src="/favicon.svg" alt="" class="size-4" />
<span>openteam · LLM API 中转站</span>
</div>
<span class="font-mono">© 2026</span>
</div>
</footer>
</div>
</template>