refactor(guide): 重构教程文档页面结构和布局

- 将 ProviderGuide/ModelGuide/UserKeyGuide 替换为 ArchitectureGuide/ConceptsGuide/StrategyGuide
- GuideLayout 迁移至 AppShell 组件,统一侧边栏导航和移动端菜单
- Overview 页面重写内容结构
- 更新路由配置和导航配置项
- homeGuard 添加 /guide 路径支持
This commit is contained in:
fawney19
2026-02-18 18:17:14 +08:00
parent 63870931af
commit 18b1dea8cf
11 changed files with 4173 additions and 1700 deletions

View File

@@ -20,7 +20,7 @@ export function resolveHomeRedirect(
// 已登录用户如果是从dashboard返回首页、刷新首页、或者有returnTo参数,允许访问首页
const isFromApp =
from.path.startsWith('/dashboard') || from.path.startsWith('/admin') || from.path === '/'
from.path.startsWith('/dashboard') || from.path.startsWith('/admin') || from.path.startsWith('/guide') || from.path === '/'
if (to.query.returnTo || isFromApp) {
return ''
}

View File

@@ -31,21 +31,21 @@ const routes: RouteRecordRaw[] = [
meta: { requiresAuth: false }
},
{
path: 'provider',
name: 'GuideProvider',
component: () => importWithRetry(() => import('@/views/public/guide/ProviderGuide.vue')),
path: 'architecture',
name: 'GuideArchitecture',
component: () => importWithRetry(() => import('@/views/public/guide/ArchitectureGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'model',
name: 'GuideModel',
component: () => importWithRetry(() => import('@/views/public/guide/ModelGuide.vue')),
path: 'concepts',
name: 'GuideConcepts',
component: () => importWithRetry(() => import('@/views/public/guide/ConceptsGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'user-key',
name: 'GuideUserKey',
component: () => importWithRetry(() => import('@/views/public/guide/UserKeyGuide.vue')),
path: 'strategy',
name: 'GuideStrategy',
component: () => importWithRetry(() => import('@/views/public/guide/StrategyGuide.vue')),
meta: { requiresAuth: false }
},
{

View File

@@ -0,0 +1,466 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import {
ArrowRight,
ChevronRight,
Monitor,
Shield,
Zap,
Database,
Info,
Shuffle
} from 'lucide-vue-next'
import { panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
const { siteName } = useSiteInfo()
// 系统分层
const layers = [
{
name: 'API 层',
path: 'api/ + middleware/',
description: '协议路由、格式适配、认证鉴权、请求管道'
},
{
name: '编排层',
path: 'services/orchestration/',
description: '候选排序、请求分发、错误分类、故障转移'
},
{
name: '调度层',
path: 'services/scheduling/',
description: '候选构建、优先级排序、缓存亲和性、并发检查'
},
{
name: '供应商适配层',
path: 'services/provider/',
description: '上游 HTTP 调用、认证管理、流式传输'
}
]
// 请求处理流程
const requestFlow = [
{ step: '请求进入', detail: 'ASGI 中间件处理限流、监控、DB Session 管理' },
{ step: '路由匹配', detail: '根据请求路径选择对应的格式适配器Claude/OpenAI/Gemini' },
{ step: '认证鉴权', detail: 'API Key / JWT / Management Token 认证,配额与权限检查' },
{ step: '候选构建', detail: '查询可用的 Provider + Endpoint + Key 组合,构建候选列表' },
{ step: '候选排序', detail: '按调度模式排序(优先级/全局Key结合缓存亲和性调整顺序' },
{ step: '故障转移执行', detail: '按序尝试候选,失败时根据错误分类决定重试或放弃' },
{ step: '上游调用', detail: '构建上游请求,处理流式/非流式响应' },
{ step: '响应后处理', detail: '用量统计、计费、缓存亲和性更新、审计记录' }
]
// 数据存储
const dataStores = [
{
name: 'PostgreSQL',
role: '主存储',
items: ['用户与认证', '供应商/端点/密钥', '模型与路由', '用量与配额', '统计聚合', '审计日志']
},
{
name: 'Redis',
role: '缓存与协调',
items: ['缓存亲和性 (affinity)', 'RPM 限流计数', '任务协调锁', 'Usage 队列 (Streams)', '通用缓存 (Provider/Model/User)']
}
]
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
架构说明
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 的系统架构请求处理流程和数据流向
</p>
</div>
<!-- 系统概览 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
系统概览
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center justify-center gap-3 sm:gap-6 flex-wrap text-sm">
<div class="text-center">
<Monitor class="h-4 w-4 text-[#cc785c] mx-auto mb-1.5" />
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
客户端
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
SDK / CLI / Web
</div>
</div>
<ChevronRight class="h-4 w-4 text-[#999]" />
<div class="text-center">
<Shield class="h-4 w-4 text-[#cc785c] mx-auto mb-1.5" />
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
{{ siteName }}
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
认证 / 路由 / 编排
</div>
</div>
<ChevronRight class="h-4 w-4 text-[#999]" />
<div class="text-center">
<Zap class="h-4 w-4 text-[#cc785c] mx-auto mb-1.5" />
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
上游供应商
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
Claude / OpenAI / Gemini
</div>
</div>
</div>
<div class="mt-3 flex items-center justify-center gap-6 text-xs text-[#666663] dark:text-[#a3a094]">
<div class="flex items-center gap-1.5">
<Database class="h-3 w-3" />
<span>PostgreSQL</span>
</div>
<div class="flex items-center gap-1.5">
<Zap class="h-3 w-3" />
<span>Redis</span>
</div>
</div>
</div>
</section>
<!-- 分层架构 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
分层架构
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
系统采用严格的分层架构依赖方向从上到下禁止反向依赖
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094] w-10">
#
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
层级
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
路径
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
职责
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(layer, index) in layers"
:key="layer.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
{{ index + 1 }}
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
{{ layer.name }}
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
{{ layer.path }}
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
{{ layer.description }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
依赖规则
</p>
<ul class="mt-1 space-y-1">
<li><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">api/</code> 可以依赖 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">services/</code> <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">core/</code></li>
<li><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">services/</code> 可以依赖 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">core/</code><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">models/</code><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">clients/</code>禁止反向依赖 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">api/</code></li>
<li><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">core/</code> 保持纯工具与协议尽量无副作用</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 请求处理流程 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
请求处理流程
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
一个 API 请求从进入到响应的完整处理链路
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094] w-10">
#
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
步骤
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in requestFlow"
:key="item.step"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
{{ index + 1 }}
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
{{ item.step }}
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
{{ item.detail }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 格式转换流程 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
格式转换原理
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
当客户端使用的 API 格式与上游端点不同时系统会自动执行格式转换
</p>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center justify-center gap-3 sm:gap-6 flex-wrap text-sm">
<div class="text-center">
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
入站格式
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
OpenAI / Claude / Gemini
</div>
</div>
<ChevronRight class="h-4 w-4 text-[#999]" />
<div class="text-center">
<Shuffle class="h-4 w-4 text-[#cc785c] mx-auto mb-1" />
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
归一化
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
统一内部格式
</div>
</div>
<ChevronRight class="h-4 w-4 text-[#999]" />
<div class="text-center">
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
目标格式
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
匹配上游端点
</div>
</div>
</div>
<p class="text-center text-xs text-[#666663] dark:text-[#a3a094] mt-3">
例如 OpenAI SDK 发送请求 -> 归一化为内部格式 -> 转换为 Claude API 格式发送给上游
</p>
</div>
</section>
<!-- 数据存储 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
数据存储
</h2>
<div class="grid gap-3 md:grid-cols-2">
<div
v-for="store in dataStores"
:key="store.name"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2.5">
<Database class="h-4 w-4 text-[#cc785c]" />
<div>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
{{ store.name }}
</h3>
<span class="text-xs text-[#666663] dark:text-[#a3a094]">{{ store.role }}</span>
</div>
</div>
<ul class="space-y-1">
<li
v-for="item in store.items"
:key="item"
class="flex items-start gap-2 text-sm text-[#666663] dark:text-[#a3a094]"
>
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>{{ item }}</span>
</li>
</ul>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
Redis 降级
</p>
<p class="mt-1">
Redis 客户端内置熔断机制连续 3 次失败触发熔断60 秒后尝试恢复开发模式下可完全降级为内存模式运行
</p>
</div>
</div>
</div>
</section>
<!-- 适配器体系 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
适配器体系
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 通过适配器模式支持多种 API 格式每种格式有独立的请求构建和响应解析逻辑
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
类型
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
基类
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
实现
</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8]">
聊天 API
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
ChatAdapterBase
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
Claude / OpenAI / Gemini
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8]">
CLI 透传
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
CliAdapterBase
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
Claude CLI / OpenAI CLI (Codex) / Gemini CLI
</td>
</tr>
<tr class="last:border-0">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8]">
视频/图片
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
VideoAdapterBase
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
OpenAI Video / Gemini Video / Gemini Image
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 下一步 -->
<section class="pt-2">
<RouterLink
to="/guide/concepts"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
下一步相关概念
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
深入了解供应商端点模型等核心概念
</div>
</div>
<ArrowRight class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

View File

@@ -0,0 +1,408 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import {
ArrowRight,
Server,
Box,
Key,
Link,
Layers,
Users,
Info,
Tag
} from 'lucide-vue-next'
import { providerExamples, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
const { siteName } = useSiteInfo()
// 核心概念列表
const concepts = [
{
name: '供应商 (Provider)',
icon: Server,
description: '上游 AI 服务商的逻辑抽象,用于组织和管理多个端点。',
details: [
'一个供应商代表一个 AI 服务提供商(如 OpenAI、Anthropic',
'供应商下可以有多个端点(不同区域、不同账号)',
'供应商级别可以配置格式转换开关',
'供应商的优先级影响候选排序'
]
},
{
name: '端点 (ProviderEndpoint)',
icon: Box,
description: '实际的 API 配置单元,包含调用上游所需的全部信息。',
details: [
'包含 URL、API 格式api_family:endpoint_kind 签名)',
'可配置代理、超时、请求头/请求体规则',
'端点下挂载一个或多个 API KeyProviderAPIKey',
'端点是系统实际发起调用的最小单元'
]
},
{
name: '供应商密钥 (ProviderAPIKey)',
icon: Key,
description: '端点的鉴权凭据,是调度系统操作的核心对象。',
details: [
'包含 internal_priority内部优先级影响候选排序',
'可设置能力标签(如 1M 上下文、1H 缓存支持)',
'可限定模型白名单,只允许特定模型使用此 Key',
'包含上游配额元信息RPM 等)'
]
},
{
name: '候选 (ProviderCandidate)',
icon: Link,
description: 'Provider + Endpoint + Key 的组合,是调度和故障转移的基本单位。',
details: [
'每次请求会构建一组候选列表',
'候选按调度策略排序后依次尝试',
'失败时自动切换到下一个候选(故障转移)',
'候选上附带并发检查、缓存亲和性等元数据'
]
}
]
// 模型相关概念
const modelConcepts = [
{
name: '全局模型 (GlobalModel)',
description: '用户请求中使用的模型名定义。用户在 API 调用中指定的 model 参数值必须是已注册的 GlobalModel。',
example: 'claude-sonnet-4-20250514、gpt-4o、gemini-2.5-pro'
},
{
name: '模型 (Model)',
description: '供应商侧的模型实现。一个 GlobalModel 可以关联到多个 Provider 的 Model每个 Model 可以映射不同的上游模型名。',
example: '用户请求 gpt-4 -> 端点 A 发送 gpt-4-turbo端点 B 发送 gpt-4o'
},
{
name: '模型别名',
description: '一个 GlobalModel 可以设置多个别名,用户可以用任意别名调用。适合版本迁移时保持向后兼容。',
example: 'claude-3-5-sonnet -> claude-3.5-sonnet (别名)'
}
]
// 用户与密钥
const userConcepts = [
{
name: 'API Key用户密钥',
description: '用户访问系统的凭证,与上游 ProviderAPIKey 不同。每个 API Key 归属一个用户,可以设置:',
features: ['允许访问的模型列表', '请求/Token 配额(日/月)', 'IP 白名单', '有效期', '1H 缓存策略']
},
{
name: '亲和性键 (affinity_key)',
description: '缓存亲和性的维度标识,通常取值为用户 API Key ID。系统会尝试为同一个 affinity_key 稳定选择相同的 ProviderAPIKey以最大化利用上游的 Prompt Caching。',
features: []
}
]
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
相关概念
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
深入理解 {{ siteName }} 中的核心概念及其关系
</p>
</div>
<!-- 概念关系 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
概念关系
</h2>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
关系
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
供应商 端点 <span class="text-[#999] font-normal text-xs ml-1">1:N</span>
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
一个供应商包含多个端点不同区域不同账号等
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
端点 供应商密钥 <span class="text-[#999] font-normal text-xs ml-1">1:N</span>
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
一个端点下挂载多个 API Key
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
全局模型 端点 <span class="text-[#999] font-normal text-xs ml-1">N:M</span>
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
全局模型与端点是多对多关系通过 Model 关联
</td>
</tr>
<tr class="last:border-0">
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
用户 API Key 全局模型 <span class="text-[#999] font-normal text-xs ml-1">N:M</span>
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
用户 API Key 可限制可访问的模型列表
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 供应商侧核心概念 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
供应商侧概念
</h2>
<div class="space-y-3">
<div
v-for="concept in concepts"
:key="concept.name"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<component
:is="concept.icon"
class="h-4 w-4 text-[#cc785c]"
/>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
{{ concept.name }}
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-2.5">
{{ concept.description }}
</p>
<ul class="space-y-1">
<li
v-for="detail in concept.details"
:key="detail"
class="flex items-start gap-2 text-sm text-[#666663] dark:text-[#a3a094]"
>
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>{{ detail }}</span>
</li>
</ul>
</div>
</div>
</section>
<!-- 常见供应商配置 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
常见供应商配置
</h2>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
供应商
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
URL
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
格式
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
备注
</th>
</tr>
</thead>
<tbody>
<tr
v-for="example in providerExamples"
:key="example.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
{{ example.name }}
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
{{ example.url }}
</td>
<td class="px-4 py-2.5 whitespace-nowrap">
<span :class="panelClasses.badgeBlue">{{ example.format }}</span>
</td>
<td class="px-4 py-2.5 text-xs text-[#999]">
{{ example.note }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 模型概念 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
模型体系
</h2>
<div class="space-y-3">
<div
v-for="model in modelConcepts"
:key="model.name"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<Layers class="h-4 w-4 text-[#cc785c]" />
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
{{ model.name }}
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ model.description }}
</p>
<div class="mt-2 flex items-center gap-2 text-xs text-[#666663] dark:text-[#a3a094]">
<Tag class="h-3 w-3 text-[#999]" />
<span>示例: </span>
<code class="text-[#262624] dark:text-[#f1ead8] bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">{{ model.example }}</code>
</div>
</div>
</div>
<!-- 模型名映射示例 -->
<div
class="p-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8] mb-3">
模型名映射示例
</h3>
<div class="space-y-2">
<div class="flex items-center gap-3 text-sm flex-wrap">
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-2 py-1 rounded text-[#262624] dark:text-[#f1ead8]">用户请求 gpt-4</code>
<ArrowRight class="h-3.5 w-3.5 text-[#999]" />
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-2 py-1 rounded text-[#262624] dark:text-[#f1ead8]">端点 A: gpt-4-turbo</code>
</div>
<div class="flex items-center gap-3 text-sm flex-wrap">
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-2 py-1 rounded text-[#262624] dark:text-[#f1ead8]">用户请求 gpt-4</code>
<ArrowRight class="h-3.5 w-3.5 text-[#999]" />
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-2 py-1 rounded text-[#262624] dark:text-[#f1ead8]">端点 B: gpt-4o</code>
</div>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-2.5">
同一个模型名可以映射到不同端点的不同上游模型名系统根据调度策略选择端点后使用对应的映射
</p>
</div>
</section>
<!-- 用户与密钥 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
用户与 API Key
</h2>
<div class="space-y-3">
<div
v-for="concept in userConcepts"
:key="concept.name"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<Users class="h-4 w-4 text-[#cc785c]" />
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
{{ concept.name }}
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ concept.description }}
</p>
<ul
v-if="concept.features.length > 0"
class="mt-2 space-y-1"
>
<li
v-for="feature in concept.features"
:key="feature"
class="flex items-start gap-2 text-sm text-[#666663] dark:text-[#a3a094]"
>
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>{{ feature }}</span>
</li>
</ul>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
两种 Key 的区别
</p>
<p class="mt-1">
<strong>用户 API Key</strong>用户用来访问 {{ siteName }} 的凭证外部面向用户<br>
<strong>ProviderAPIKey</strong>{{ siteName }} 用来调用上游服务的凭证内部面向供应商
两者是独立的概念用户 API Key 的请求会被路由到合适的 ProviderAPIKey
</p>
</div>
</div>
</div>
</section>
<!-- 下一步 -->
<section class="pt-2">
<RouterLink
to="/guide/strategy"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
下一步关键策略
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
了解调度缓存亲和性故障转移等核心策略
</div>
</div>
<ArrowRight class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

View File

@@ -1,204 +1,308 @@
<template>
<div class="min-h-screen literary-grid literary-paper">
<!-- Header -->
<header class="sticky top-0 z-50 border-b border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-xl transition-all">
<div class="h-14 sm:h-16 flex items-center px-3 sm:px-4 md:px-8">
<!-- Left: Logo & Brand -->
<AppShell
:main-class="mainClasses"
:sidebar-class="sidebarClasses"
:content-class="contentClasses"
>
<!-- GLOBAL TEXTURE (Paper Noise) -->
<div
class="absolute inset-0 pointer-events-none z-0 opacity-[0.03] mix-blend-multiply fixed"
:style="{ backgroundImage: `url(\&quot;data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\&quot;)` }"
/>
<template #sidebar>
<!-- HEADER (Brand) -->
<div class="shrink-0 flex items-center px-6 h-20">
<RouterLink
to="/"
class="flex items-center gap-2 sm:gap-3 group/logo cursor-pointer shrink-0"
class="flex items-center gap-3 group transition-opacity hover:opacity-80"
>
<HeaderLogo
size="h-7 w-7 sm:h-9 sm:w-9"
size="h-9 w-9"
class-name="text-[#191919] dark:text-white"
/>
<div class="flex flex-col justify-center">
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
{{ siteName }}
</h1>
<span class="text-[9px] sm:text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1 sm:mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
</div>
</RouterLink>
<!-- Center: Breadcrumb -->
<div class="hidden md:flex items-center gap-2 ml-8">
<RouterLink
to="/guide"
class="text-sm text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white transition"
>
教程文档
</RouterLink>
<ChevronRight
v-if="currentNavItem && currentNavItem.id !== 'overview'"
class="h-4 w-4 text-[#91918d]"
/>
<span
v-if="currentNavItem && currentNavItem.id !== 'overview'"
class="text-sm font-medium text-[#191919] dark:text-white"
>
{{ currentNavItem.name }}
</span>
</div>
<!-- Spacer -->
<div class="flex-1" />
<!-- Right: Actions -->
<div class="flex items-center gap-1 sm:gap-2 shrink-0">
<!-- Mobile menu button -->
<button
class="md:hidden flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
@click="showMobileNav = !showMobileNav"
>
<Menu class="h-4 w-4" />
</button>
<!-- Theme Toggle + GitHub Icons -->
<div class="flex items-center gap-0.5 sm:gap-1">
<button
class="flex h-8 w-8 sm:h-9 sm:w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
@click="toggleDarkMode"
>
<SunMoon
v-if="themeMode === 'system'"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
/>
<Sun
v-else-if="themeMode === 'light'"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
/>
<Moon
v-else
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
/>
</button>
<a
href="https://github.com/fawney19/Aether"
target="_blank"
rel="noopener noreferrer"
class="flex h-8 w-8 sm:h-9 sm:w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
title="GitHub 仓库"
>
<GithubIcon class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</a>
</div>
</div>
</div>
</header>
<!-- Mobile Nav Overlay -->
<Transition name="fade">
<div
v-if="showMobileNav"
class="fixed inset-0 z-40 bg-black/50 md:hidden"
@click="showMobileNav = false"
/>
</Transition>
<!-- Mobile Nav Drawer -->
<Transition name="slide-left">
<div
v-if="showMobileNav"
class="fixed left-0 top-14 bottom-0 z-50 w-64 bg-[#fafaf7] dark:bg-[#191714] border-r border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] md:hidden overflow-y-auto"
>
<nav class="p-4 space-y-1">
<RouterLink
v-for="item in guideNavItems"
:key="item.id"
:to="item.path"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors"
:class="isActive(item.path)
? 'bg-[#cc785c]/10 text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:bg-[#f0f0eb] dark:hover:bg-[#262624]'"
@click="showMobileNav = false"
>
<component
:is="item.icon"
class="h-4 w-4 shrink-0"
/>
<span class="text-sm font-medium">{{ item.name }}</span>
</RouterLink>
</nav>
</div>
</Transition>
<!-- Main Content -->
<div class="flex">
<!-- Desktop Sidebar -->
<aside class="hidden md:block w-64 shrink-0 border-r border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#191714]/50">
<div class="sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto">
<nav class="p-4 space-y-1">
<RouterLink
<!-- NAVIGATION -->
<div class="flex-1 overflow-y-auto py-2 scrollbar-none">
<nav class="w-full px-3">
<div class="space-y-0.5">
<template
v-for="item in guideNavItems"
:key="item.id"
:to="item.path"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group"
:class="isActive(item.path)
? 'bg-[#cc785c]/10 text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:bg-[#f0f0eb] dark:hover:bg-[#262624]'"
>
<component
:is="item.icon"
class="h-4 w-4 shrink-0"
/>
<div class="flex flex-col">
<span class="text-sm font-medium">{{ item.name }}</span>
<span
v-if="item.description"
class="text-xs text-[#91918d] dark:text-muted-foreground/70"
<RouterLink
:to="item.path"
class="group relative flex items-center justify-between px-2.5 py-2 rounded-lg transition-all duration-200"
:class="[
isNavActive(item.path)
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
]"
>
<div class="flex items-center gap-2.5">
<component
:is="item.icon"
class="h-4 w-4 transition-colors duration-200"
:class="isNavActive(item.path) ? 'text-primary' : 'text-muted-foreground/70 group-hover:text-foreground'"
:stroke-width="isNavActive(item.path) ? 2 : 1.75"
/>
<span class="text-[13px] tracking-tight">{{ item.name }}</span>
</div>
<div
v-if="isNavActive(item.path)"
class="w-1 h-1 rounded-full bg-primary"
/>
</RouterLink>
<!-- 快速开始子导航 -->
<div
v-if="item.id === 'overview' && isNavActive(item.path)"
class="ml-7 space-y-0.5 mt-0.5"
>
<a
v-for="sub in overviewSubItems"
:key="sub.hash"
:href="sub.hash"
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[12px] transition-colors"
:class="activeHash === sub.hash
? 'text-primary font-medium'
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted/30'"
@click.prevent="scrollToHash(sub.hash)"
>
{{ item.description }}
</span>
<span
class="w-1 h-1 rounded-full flex-shrink-0"
:class="activeHash === sub.hash ? 'bg-primary' : 'bg-muted-foreground/30'"
/>
{{ sub.name }}
</a>
</div>
</template>
</div>
</nav>
</div>
<!-- FOOTER (Base URL) -->
<div class="p-4 border-t border-[#3d3929]/5 dark:border-white/5">
<label class="block text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.1em] mb-2">
Base URL
</label>
<input
v-model="baseUrl"
type="text"
class="w-full px-3 py-2 text-sm rounded-lg border border-[#3d3929]/5 dark:border-white/5 bg-white/50 dark:bg-white/5 text-[#191919] dark:text-white placeholder-[#91918d] focus:outline-none focus:ring-2 focus:ring-[#cc785c]/30 transition"
placeholder="https://your-aether.com"
>
<p class="mt-1.5 text-[10px] text-muted-foreground/50">
代码示例将使用此 URL
</p>
</div>
</template>
<template #header>
<!-- Mobile Header -->
<header class="lg:hidden fixed top-0 left-0 right-0 z-50 border-b border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl transition-all">
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="flex items-center justify-between">
<RouterLink
to="/"
class="flex items-center gap-3 group"
>
<HeaderLogo
size="h-9 w-9"
class-name="text-[#191919] dark:text-white"
/>
<div class="flex flex-col justify-center">
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
{{ siteName }}
</h1>
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
</div>
</RouterLink>
</nav>
<!-- Base URL Input -->
<div class="p-4 border-t border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]">
<label class="block text-xs font-medium text-[#666663] dark:text-muted-foreground mb-2">
{{ siteName }} Base URL
</label>
<input
v-model="baseUrl"
type="text"
class="w-full px-3 py-2 text-sm rounded-lg border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-white dark:bg-[#1f1d1a] text-[#191919] dark:text-white placeholder-[#91918d] focus:outline-none focus:ring-2 focus:ring-[#cc785c]/30"
placeholder="https://your-aether.com"
>
<p class="mt-1.5 text-xs text-[#91918d]">
代码示例将使用此 URL
</p>
<div class="flex items-center gap-3">
<button
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
@click="toggleDarkMode"
>
<SunMoon
v-if="themeMode === 'system'"
class="h-4 w-4"
/>
<Sun
v-else-if="themeMode === 'light'"
class="h-4 w-4"
/>
<Moon
v-else
class="h-4 w-4"
/>
</button>
<button
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
@click="mobileMenuOpen = !mobileMenuOpen"
>
<div class="relative w-5 h-5">
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 rotate-90 scale-75"
enter-to-class="opacity-100 rotate-0 scale-100"
leave-active-class="transition-all duration-150 ease-in absolute inset-0"
leave-from-class="opacity-100 rotate-0 scale-100"
leave-to-class="opacity-0 -rotate-90 scale-75"
mode="out-in"
>
<Menu
v-if="!mobileMenuOpen"
class="h-5 w-5"
/>
<X
v-else
class="h-5 w-5"
/>
</Transition>
</div>
</button>
</div>
</div>
</div>
</aside>
<!-- Page Content -->
<main class="flex-1 min-w-0">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-12">
<RouterView v-slot="{ Component }">
<component
:is="Component"
:base-url="baseUrl"
/>
</RouterView>
<!-- Mobile Dropdown Menu -->
<Transition
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-[500px]"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
leave-from-class="opacity-100 max-h-[500px]"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="mobileMenuOpen"
class="border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
>
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="space-y-4">
<div
v-for="group in navigation"
:key="group.title"
>
<div
v-if="group.title"
class="text-[10px] font-semibold text-[#91918d] dark:text-muted-foreground uppercase tracking-wider mb-2"
>
{{ group.title }}
</div>
<div class="grid grid-cols-2 gap-2">
<RouterLink
v-for="item in group.items"
:key="item.href"
:to="item.href"
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all"
:class="isNavActive(item.href)
? 'bg-[#cc785c]/10 dark:bg-[#cc785c]/20 text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 hover:text-[#191919] dark:hover:text-white'"
@click="mobileMenuOpen = false"
>
<component
:is="item.icon"
class="h-4 w-4 shrink-0"
/>
<span class="truncate">{{ item.name }}</span>
</RouterLink>
</div>
</div>
</div>
</div>
</div>
</Transition>
</header>
<!-- Desktop Page Header -->
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<RouterLink
to="/guide"
class="hover:text-foreground transition-colors"
>
教程文档
</RouterLink>
<template v-if="currentNavItem && currentNavItem.id !== 'overview'">
<ChevronRight class="w-3 h-3 opacity-50" />
<span class="text-foreground font-medium">
{{ currentNavItem.name }}
</span>
</template>
</div>
</div>
</main>
<div class="flex items-center gap-2">
<button
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
@click="toggleDarkMode"
>
<SunMoon
v-if="themeMode === 'system'"
class="h-4 w-4"
/>
<Sun
v-else-if="themeMode === 'light'"
class="h-4 w-4"
/>
<Moon
v-else
class="h-4 w-4"
/>
</button>
<a
href="https://github.com/fawney19/Aether"
target="_blank"
rel="noopener noreferrer"
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
title="GitHub 仓库"
>
<GithubIcon class="h-4 w-4" />
</a>
</div>
</header>
</template>
<div class="max-w-4xl mx-auto">
<RouterView
v-slot="{ Component }"
>
<component
:is="Component"
:base-url="baseUrl"
/>
</RouterView>
</div>
</div>
</AppShell>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
ChevronRight,
Menu,
Moon,
Sun,
SunMoon
SunMoon,
ChevronRight,
X
} from 'lucide-vue-next'
import GithubIcon from '@/components/icons/GithubIcon.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
import AppShell from '@/components/layout/AppShell.vue'
import { useDarkMode } from '@/composables/useDarkMode'
import { useSiteInfo } from '@/composables/useSiteInfo'
import { guideNavItems } from './guide-config'
@@ -207,57 +311,68 @@ const route = useRoute()
const { themeMode, toggleDarkMode } = useDarkMode()
const { siteName, siteSubtitle } = useSiteInfo()
const showMobileNav = ref(false)
const mobileMenuOpen = ref(false)
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
const activeHash = ref('#production')
// 快速开始子导航
const overviewSubItems = [
{ name: '部署', hash: '#production' },
{ name: '配置流程', hash: '#config-steps' },
{ name: 'API 格式', hash: '#api-formats' },
{ name: '推荐帖子', hash: '#recommended-posts' }
]
function scrollToHash(hash: string) {
activeHash.value = hash
const el = document.querySelector(hash)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
// 路由变化时关闭移动端菜单
watch(() => route.path, () => {
mobileMenuOpen.value = false
})
const currentNavItem = computed(() => {
return guideNavItems.find(item => item.path === route.path)
})
function isActive(path: string): boolean {
if (path === '/guide') {
function isNavActive(href: string) {
if (href === '/guide') {
return route.path === '/guide'
}
return route.path.startsWith(path)
return route.path === href || route.path.startsWith(`${href}/`)
}
// 移动端菜单用的导航数据
const navigation = computed(() => [
{
items: guideNavItems.map(item => ({
name: item.name,
href: item.path,
icon: item.icon
}))
}
])
// 样式类 - 与 MainLayout 保持一致
const sidebarClasses = computed(() => {
return 'w-[260px] flex flex-col hidden lg:flex border-r border-[#3d3929]/5 dark:border-white/5 bg-[#faf9f5] dark:bg-[#1e1c19] h-screen sticky top-0'
})
const contentClasses = computed(() => {
return 'flex-1 min-w-0 bg-[#faf9f5] dark:bg-[#191714] text-[#3d3929] dark:text-[#d4a27f]'
})
const mainClasses = computed(() => {
return 'pt-24 lg:pt-6'
})
</script>
<style scoped>
/* Typography */
h1, h2, h3 {
font-family: var(--serif);
letter-spacing: -0.02em;
font-weight: 500;
}
p {
font-family: var(--serif);
letter-spacing: 0.01em;
line-height: 1.7;
}
button, nav, a, .inline-flex, input, label {
font-family: var(--sans-serif);
}
/* Transitions */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform 0.2s ease;
}
.slide-left-enter-from,
.slide-left-leave-to {
transform: translateX(-100%);
}
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
</style>

View File

@@ -1,374 +0,0 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { ArrowRight, Layers, Check, Info, Shuffle, TrendingUp, Gauge, Clock } from 'lucide-vue-next'
import { loadBalanceModes, panelClasses } from './guide-config'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
// 模型配置字段
const modelFields = [
{ name: '模型名称', description: '用户调用时使用的模型名,如 gpt-4、claude-3', required: true },
{ name: '别名', description: '模型的其他名称,用户可以用别名调用', required: false },
{ name: '关联端点', description: '该模型使用的端点列表,可多选', required: true },
{ name: '负载均衡模式', description: '多端点时的请求分发策略', required: false },
{ name: '目标模型名', description: '发送给端点的实际模型名(如端点模型名不同)', required: false },
{ name: '是否启用', description: '禁用后用户无法使用该模型', required: false }
]
// 负载均衡图标
const lbIcons = {
priority: TrendingUp,
random: Shuffle,
round_robin: Clock,
weighted: Gauge,
latency: Clock
}
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-4">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
模型管理
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
模型是用户实际调用的对象通过模型配置你可以定义模型名称关联端点配置负载均衡策略
</p>
</div>
<!-- 模型概念 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
什么是模型
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-purple-500/10">
<Layers class="h-5 w-5 text-purple-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
模型 (Model)
</h3>
</div>
<ul class="space-y-3 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>模型是用户在请求中指定的 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">model</code> 参数值</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>一个模型可以关联多个端点实现负载均衡和故障转移</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>可以设置别名让用户用多个名称访问同一个模型</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>支持模型名映射请求中的模型名可以与发给端点的不同</span>
</li>
</ul>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
示例场景
</p>
<p class="mt-1">
用户请求模型 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">gpt-4</code>
你可以配置它同时使用 OpenAI 官方端点和某个代理端点
系统会根据负载均衡策略选择一个端点如果失败则自动尝试其他端点
</p>
</div>
</div>
</div>
</section>
<!-- 创建模型 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
创建模型
</h2>
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
1
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
进入模型管理页面
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
在管理后台左侧菜单点击模型管理
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
2
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
添加模型
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
点击添加模型填写模型名称这是用户调用时使用的名称
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
3
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
关联端点
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
选择该模型要使用的端点可以选择多个如果端点的模型名与你定义的不同需要设置目标模型名映射
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
4
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
配置负载均衡
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
如果选择了多个端点可以配置负载均衡策略
</p>
</div>
</div>
</div>
</section>
<!-- 模型配置字段 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
配置字段说明
</h2>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
字段
</th>
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
<th class="px-4 py-3 text-center font-medium text-[#666663] dark:text-[#a3a094]">
必填
</th>
</tr>
</thead>
<tbody>
<tr
v-for="field in modelFields"
:key="field.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 font-medium text-[#262624] dark:text-[#f1ead8]">
{{ field.name }}
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ field.description }}
</td>
<td class="px-4 py-3 text-center">
<span
v-if="field.required"
:class="panelClasses.badgeGreen"
>必填</span>
<span
v-else
class="text-[#999]"
>可选</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 负载均衡模式 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
负载均衡模式
</h2>
<p class="text-[#666663] dark:text-[#a3a094]">
当模型关联多个端点时系统会根据负载均衡模式选择端点
</p>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div
v-for="lb in loadBalanceModes"
:key="lb.mode"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-2">
<component
:is="lbIcons[lb.mode as keyof typeof lbIcons] || Shuffle"
class="h-5 w-5 text-[#cc785c]"
/>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
{{ lb.name }}
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ lb.description }}
</p>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
推荐配置
</p>
<p class="mt-1">
<strong>主备切换场景</strong>使用优先级模式将官方端点设为高优先级代理端点设为低优先级<br>
<strong>分摊负载场景</strong>使用轮询加权模式将多个同质端点平均分配请求
</p>
</div>
</div>
</div>
</section>
<!-- 模型名映射 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
模型名映射
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
有时候你希望用户调用的模型名与实际发给端点的模型名不同例如
</p>
<div class="space-y-3">
<div class="flex items-center gap-4 text-sm">
<div class="px-3 py-2 rounded-lg bg-purple-500/10 border border-purple-500/20">
<span class="text-purple-600 dark:text-purple-400">用户请求</span>
<code class="ml-2 text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">gpt-4</code>
</div>
<ArrowRight class="h-4 w-4 text-[#999]" />
<div class="px-3 py-2 rounded-lg bg-green-500/10 border border-green-500/20">
<span class="text-green-600 dark:text-green-400">发给端点</span>
<code class="ml-2 text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">gpt-4-turbo-preview</code>
</div>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="px-3 py-2 rounded-lg bg-purple-500/10 border border-purple-500/20">
<span class="text-purple-600 dark:text-purple-400">用户请求</span>
<code class="ml-2 text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">claude-3</code>
</div>
<ArrowRight class="h-4 w-4 text-[#999]" />
<div class="px-3 py-2 rounded-lg bg-green-500/10 border border-green-500/20">
<span class="text-green-600 dark:text-green-400">发给端点</span>
<code class="ml-2 text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">claude-3-opus-20240229</code>
</div>
</div>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-4">
在模型的端点关联配置中可以为每个端点设置目标模型名来实现这种映射
</p>
</div>
</section>
<!-- 别名功能 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
模型别名
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
别名让用户可以用多个名称访问同一个模型适用于
</p>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>兼容旧的模型名 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">gpt-4-turbo</code> <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">gpt-4-turbo-preview</code> 指向同一个模型</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>提供简短易记的名称如用 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">claude</code> 代替完整版本号</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>版本迁移时保持向后兼容</span>
</li>
</ul>
</div>
</section>
<!-- 下一步 -->
<section class="pt-4">
<RouterLink
to="/guide/user-key"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-[#262624] dark:text-[#f1ead8]">
下一步用户与密钥
</div>
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
管理用户和 API Key
</div>
</div>
<ArrowRight class="h-5 w-5 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -1,409 +0,0 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { ArrowRight, Server, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
import { apiFormats, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
const { siteName } = useSiteInfo()
// 端点配置字段说明
const endpointFields = [
{ name: '名称', description: '端点的显示名称,用于区分不同的端点', required: true },
{ name: 'URL', description: 'API 的基础 URL如 https://api.openai.com', required: true },
{ name: 'API Key', description: '调用该 API 需要的密钥', required: true },
{ name: 'API 格式', description: '该端点支持的 API 格式OpenAI/Claude/Gemini 等)', required: true },
{ name: '优先级', description: '数字越大优先级越高,用于负载均衡', required: false },
{ name: '权重', description: '加权负载均衡时使用的权重值', required: false },
{ name: '代理', description: '如需通过代理访问,填写代理地址', required: false },
{ name: '超时', description: '请求超时时间(秒)', required: false }
]
// 常见供应商配置示例
const providerExamples = [
{
name: 'OpenAI',
url: 'https://api.openai.com',
format: 'OpenAI Chat',
note: '官方 API需要国际信用卡或通过代理访问'
},
{
name: 'Anthropic',
url: 'https://api.anthropic.com',
format: 'Claude Chat',
note: '官方 Claude API'
},
{
name: 'Google AI',
url: 'https://generativelanguage.googleapis.com',
format: 'Gemini Chat',
note: '官方 Gemini API'
},
{
name: 'Azure OpenAI',
url: 'https://{resource}.openai.azure.com',
format: 'OpenAI Chat',
note: '需要替换 {resource} 为你的资源名'
},
{
name: 'OpenRouter',
url: 'https://openrouter.ai/api',
format: 'OpenAI Chat',
note: '聚合多家供应商的 API 代理'
},
{
name: '自托管 / 其他',
url: 'https://your-api.com',
format: 'OpenAI Chat',
note: '大多数 OpenAI 兼容服务选择 OpenAI Chat 格式'
}
]
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-4">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
供应商管理
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
供应商和端点是 {{ siteName }} 的基础配置决定了系统可以调用哪些 AI 服务
</p>
</div>
<!-- 概念说明 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
供应商 vs 端点
</h2>
<div class="grid gap-4 md:grid-cols-2">
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-blue-500/10">
<Server class="h-5 w-5 text-blue-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
供应商 (Provider)
</h3>
</div>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>逻辑分组用于组织管理多个端点</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>一个供应商可以有多个端点</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>例如OpenAI 供应商下可以有官方端点和多个代理端点</span>
</li>
</ul>
</div>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-green-500/10">
<Settings class="h-5 w-5 text-green-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
端点 (Endpoint)
</h3>
</div>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>实际的 API 配置单元</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>包含 URL密钥API 格式等信息</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>系统实际调用的是端点而非供应商</span>
</li>
</ul>
</div>
</div>
</section>
<!-- 添加供应商步骤 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
添加供应商
</h2>
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
1
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
进入供应商管理页面
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
登录管理后台在左侧菜单点击供应商管理
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
2
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
创建供应商
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
点击添加供应商按钮填写供应商名称 OpenAIAnthropic
</p>
</div>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-[#cc785c] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
3
</div>
<div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
添加端点
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
在供应商下点击添加端点填写 API URL密钥等配置
</p>
</div>
</div>
</div>
</section>
<!-- 端点配置字段 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
端点配置字段
</h2>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
字段
</th>
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
<th class="px-4 py-3 text-center font-medium text-[#666663] dark:text-[#a3a094]">
必填
</th>
</tr>
</thead>
<tbody>
<tr
v-for="field in endpointFields"
:key="field.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 font-medium text-[#262624] dark:text-[#f1ead8]">
{{ field.name }}
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ field.description }}
</td>
<td class="px-4 py-3 text-center">
<span
v-if="field.required"
:class="panelClasses.badgeGreen"
>必填</span>
<span
v-else
class="text-[#999]"
>可选</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- API 格式选择 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
API 格式选择
</h2>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
如何选择正确的 API 格式
</p>
<p class="mt-1">
根据目标 API 服务的实际格式选择大多数第三方 API 代理 OpenRouter都兼容 OpenAI 格式
如果不确定可以先尝试 OpenAI 格式
</p>
</div>
</div>
</div>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
格式
</th>
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
端点路径
</th>
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
认证方式
</th>
</tr>
</thead>
<tbody>
<tr
v-for="format in apiFormats"
:key="format.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 font-medium text-[#262624] dark:text-[#f1ead8]">
{{ format.name }}
</td>
<td class="px-4 py-3 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
{{ format.endpoint }}
</td>
<td class="px-4 py-3 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
{{ format.auth }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 常见供应商示例 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
常见供应商配置
</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div
v-for="example in providerExamples"
:key="example.name"
class="p-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
{{ example.name }}
</h3>
<div class="mt-2 space-y-1 text-sm">
<div>
<span class="text-[#666663] dark:text-[#a3a094]">URL: </span>
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">{{ example.url }}</code>
</div>
<div>
<span class="text-[#666663] dark:text-[#a3a094]">格式: </span>
<span :class="panelClasses.badgeBlue">{{ example.format }}</span>
</div>
</div>
<p class="mt-2 text-xs text-[#999]">
{{ example.note }}
</p>
</div>
</div>
</section>
<!-- 注意事项 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
注意事项
</h2>
<div
class="p-4 space-y-3"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<AlertTriangle class="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
API Key 安全
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
端点的 API Key 会被加密存储但仍建议使用子账号或限定范围的 Key而非主账号 Key
</p>
</div>
</div>
<div class="flex items-start gap-3">
<AlertTriangle class="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
测试端点
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
添加端点后可以在健康监控页面测试连通性如果显示不健康请检查 URL Key 是否正确
</p>
</div>
</div>
</div>
</section>
<!-- 下一步 -->
<section class="pt-4">
<RouterLink
to="/guide/model"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-[#262624] dark:text-[#f1ead8]">
下一步模型管理
</div>
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
配置模型映射和负载均衡
</div>
</div>
<ArrowRight class="h-5 w-5 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

View File

@@ -0,0 +1,610 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import {
ArrowRight,
TrendingUp,
Hash,
Info,
Activity
} from 'lucide-vue-next'
import { panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
const { siteName } = useSiteInfo()
// 调度模式
const schedulingModes = [
{
name: '提供商优先',
description: '按 Provider 优先级排序,同优先级内按 Key 优先级排序,相同优先级哈希分散。适合优先使用特定供应商。',
icon: TrendingUp,
badge: '默认'
},
{
name: '全局 Key 优先',
description: '忽略 Provider 层级,所有 Key 按全局优先级统一排序,相同优先级哈希分散。适合跨 Provider 统一调度,最大化利用所有 Key。',
icon: Hash,
badge: ''
}
]
// 错误分类
const errorCategories = [
{
type: 'RETRYABLE',
name: '可重试',
description: '超时、限流 (429)、5xx 服务端错误',
action: 'FailoverEngine 尝试下一个候选'
},
{
type: 'FATAL',
name: '致命',
description: '认证失败、模型不存在',
action: '立即返回错误,不再重试'
},
{
type: 'CLIENT',
name: '客户端',
description: '400 系列客户端错误',
action: '透传原始错误给调用方'
}
]
// 错误处理副作用
const errorSideEffects = [
{ name: '缓存失效', description: '通过 CacheAwareScheduler 失效亲和性缓存' },
{ name: '健康记录', description: '通过 health_monitor 记录失败事件' },
{ name: 'RPM 调整', description: '429 错误时自适应调整 RPM 限制' },
{ name: 'OAuth 标记', description: '403 VALIDATION_REQUIRED 时标记 Key 为账号封禁' }
]
// 格式转换三层开关
const conversionLayers = [
{
level: '全局',
setting: 'enable_format_conversion',
description: '系统设置中的全局开关。开启后允许所有跨格式路由。'
},
{
level: 'Provider',
setting: 'Provider.enable_format_conversion',
description: '全局关闭时,可在 Provider 级别单独开启格式转换。'
},
{
level: 'Endpoint',
setting: 'format_acceptance_config',
description: '以上两级都关闭时,通过端点的 accept/reject 规则精细控制接受哪些入站格式。'
}
]
// 配额类型
const quotaTypes = [
{
name: '请求次数配额',
description: '限制每日/每月的 API 调用次数,超过后请求会被拒绝',
scope: '用户级 / Key 级'
},
{
name: 'Token 用量配额',
description: '限制每日/每月的 Token 消耗量,适合控制成本',
scope: '用户级 / Key 级'
}
]
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
关键策略
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 的调度缓存亲和性故障转移并发控制等核心策略机制
</p>
</div>
<!-- 调度策略 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
调度策略
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
当一个模型关联了多个端点和密钥时系统需要决定使用哪个 Provider + Endpoint + Key 组合调度模式决定了候选的排序方式
</p>
<div class="grid gap-3 md:grid-cols-2">
<div
v-for="mode in schedulingModes"
:key="mode.name"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<component
:is="mode.icon"
class="h-4 w-4 text-[#cc785c]"
/>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
{{ mode.name }}
</h3>
<span
v-if="mode.badge"
:class="panelClasses.badge"
>{{ mode.badge }}</span>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ mode.description }}
</p>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
哈希分散
</p>
<p class="mt-1">
当多个候选具有相同优先级时系统使用哈希算法将请求均匀分散到各个候选上避免所有请求集中到同一个 Key
哈希因子结合了 affinity_key通常为用户 API Key ID使得同一用户的请求倾向于命中相同的候选
</p>
</div>
</div>
</div>
</section>
<!-- 缓存亲和性 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
缓存亲和性
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
缓存亲和性是 {{ siteName }} 的核心优化策略用于最大化利用上游供应商的 Prompt Caching 机制
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094] w-10">
#
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
步骤
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
1
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
提取亲和性键
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
用户发起请求时系统提取 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">affinity_key</code>通常为用户 API Key ID
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
2
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
查询缓存
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
查找该 affinity_key 上次成功使用的 ProviderAPIKey Redis 缓存读取
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
3
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
候选置顶
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
如果找到且该 Key 仍可用将其置顶到候选列表最前面
</td>
</tr>
<tr class="last:border-0">
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
4
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
更新记录
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
请求成功后更新缓存记录请求失败时失效该缓存条目
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div
class="p-4"
:class="[panelClasses.section]"
>
<h4 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8] mb-2">
为什么重要
</h4>
<ul class="space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>Claude/OpenAI 等供应商支持 Prompt Caching重复前缀可免费/降价</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>同一用户的连续请求通常有大量重复上下文</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>稳定路由到同一个 Key 可以最大化缓存命中率</span>
</li>
</ul>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<h4 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8] mb-2">
缓存层级
</h4>
<ul class="space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span><strong>L1 内存缓存</strong>3 TTL减少 Redis 访问</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span><strong>L2 Redis 缓存</strong>持久化亲和性映射</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span><strong>降级</strong>Redis 不可用时禁用亲和性使用普通排序</span>
</li>
</ul>
</div>
</div>
</section>
<!-- 故障转移 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
故障转移
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
当某个候选失败时系统会根据错误类型决定是否尝试下一个候选错误分类由 ErrorClassifier 负责
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
类型
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
触发条件
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
处理方式
</th>
</tr>
</thead>
<tbody>
<tr
v-for="error in errorCategories"
:key="error.type"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">{{ error.type }}</code>
<span class="ml-1.5 text-[#666663] dark:text-[#a3a094] font-normal text-xs">{{ error.name }}</span>
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
{{ error.description }}
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
{{ error.action }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 错误处理副作用 -->
<div
class="p-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8] mb-2">
错误处理副作用
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-2.5">
错误分类后ErrorHandlerService 负责执行副作用操作
</p>
<div class="grid gap-2 sm:grid-cols-2">
<div
v-for="effect in errorSideEffects"
:key="effect.name"
class="p-3 rounded-lg bg-[#f5f5f0]/50 dark:bg-[#1f1d1a]/50"
>
<h4 class="font-medium text-xs text-[#262624] dark:text-[#f1ead8]">
{{ effect.name }}
</h4>
<p class="text-xs text-[#666663] dark:text-[#a3a094] mt-0.5">
{{ effect.description }}
</p>
</div>
</div>
</div>
</section>
<!-- 并发控制 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
并发控制
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
系统通过多层并发控制保护上游服务不被过载
</p>
<div class="grid gap-3 md:grid-cols-2">
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<Activity class="h-4 w-4 text-[#cc785c]" />
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
RPM 限流
</h3>
</div>
<ul class="space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span> ProviderAPIKey 维度限制每分钟请求数</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>超过 RPM 限制的候选在构建阶段被跳过</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>429 错误时自适应降低 RPM 限制</span>
</li>
</ul>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2.5 mb-2">
<Activity class="h-4 w-4 text-[#cc785c]" />
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8]">
动态预留
</h3>
</div>
<ul class="space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>为缓存亲和性用户预留一定的 RPM 额度</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>避免非缓存用户占满配额导致缓存用户无法命中</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>预留比例根据负载情况自适应调整</span>
</li>
</ul>
</div>
</div>
</section>
<!-- 格式转换开关 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
格式转换控制
</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
格式转换跨格式路由由三层开关从高到低控制
</p>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094] w-10">
#
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
层级
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
配置项
</th>
<th class="px-4 py-2.5 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(layer, index) in conversionLayers"
:key="layer.level"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-2.5 text-[#cc785c] font-bold">
{{ index + 1 }}
</td>
<td class="px-4 py-2.5 font-medium text-[#262624] dark:text-[#f1ead8] whitespace-nowrap">
{{ layer.level }}
</td>
<td class="px-4 py-2.5 font-mono text-xs text-[#666663] dark:text-[#a3a094]">
{{ layer.setting }}
</td>
<td class="px-4 py-2.5 text-[#666663] dark:text-[#a3a094]">
{{ layer.description }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
优先级降级
</p>
<p class="mt-1">
需要格式转换的候选默认会被排到列表后面降级排序通过全局 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">keep_priority_on_conversion</code>
Provider 级别的同名配置可以保持原有优先级不降级
</p>
</div>
</div>
</div>
</section>
<!-- 配额管理 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
配额管理
</h2>
<div class="grid gap-3 md:grid-cols-2">
<div
v-for="quota in quotaTypes"
:key="quota.name"
class="p-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-sm text-[#262624] dark:text-[#f1ead8] mb-1.5">
{{ quota.name }}
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
{{ quota.description }}
</p>
<div class="mt-2">
<span :class="panelClasses.badgeBlue">{{ quota.scope }}</span>
</div>
</div>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Info class="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
配额继承
</p>
<p class="mt-1">
可以在用户级别设置默认配额新创建的 API Key 会自动继承也可以在创建 Key 时覆盖默认配额
</p>
</div>
</div>
</div>
</section>
<!-- 健康监控 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
健康监控
</h2>
<div
class="p-4"
:class="[panelClasses.section]"
>
<ul class="space-y-1.5 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>系统持续记录每个端点的成功/失败请求</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>连续失败超过阈值的端点会被标记为不健康</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>不健康的端点在候选构建时被跳过请求自动路由到其他端点</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>定期对不健康端点进行探测恢复</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[#cc785c] mt-1.5 flex-shrink-0 text-[6px]">&#9679;</span>
<span>可以在管理后台的健康监控页面查看实时状态并手动触发检查</span>
</li>
</ul>
</div>
</section>
<!-- 下一步 -->
<section class="pt-2">
<RouterLink
to="/guide/advanced"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
下一步高级功能
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
格式转换操作请求头/请求体规则系统设置
</div>
</div>
<ArrowRight class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

View File

@@ -1,443 +0,0 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { ArrowRight, Users, Key, Shield, Check, Info, AlertTriangle, Clock } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
}
)
const { siteName } = useSiteInfo()
// API Key 字段说明
const keyFields = [
{ name: '名称', description: 'Key 的描述性名称,方便识别', required: true },
{ name: '所属用户', description: 'Key 归属的用户', required: true },
{ name: '有效期', description: '过期时间,不设置则永久有效', required: false },
{ name: '允许的模型', description: '限制该 Key 可访问的模型列表,不设置则可访问所有模型', required: false },
{ name: '请求配额', description: '每日/每月的请求次数限制', required: false },
{ name: 'Token 配额', description: '每日/每月的 Token 用量限制', required: false },
{ name: 'IP 白名单', description: '限制只有特定 IP 可以使用该 Key', required: false },
{ name: '状态', description: '启用/禁用该 Key', required: false }
]
// 权限对比
const roleComparison = [
{ feature: '查看仪表盘', user: true, admin: true },
{ feature: '使用 API', user: true, admin: true },
{ feature: '管理自己的 API Key', user: true, admin: true },
{ feature: '查看用量统计', user: true, admin: true },
{ feature: '管理供应商/端点', user: false, admin: true },
{ feature: '管理模型', user: false, admin: true },
{ feature: '管理其他用户', user: false, admin: true },
{ feature: '管理其他用户的 Key', user: false, admin: true },
{ feature: '系统设置', user: false, admin: true }
]
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-4">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
用户与密钥
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
用户和 API Key {{ siteName }} 的访问控制核心通过用户管理分配角色权限通过 Key 管理控制 API 访问
</p>
</div>
<!-- 用户管理 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
用户管理
</h2>
<div class="grid gap-4 md:grid-cols-2">
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-blue-500/10">
<Users class="h-5 w-5 text-blue-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
普通用户
</h3>
</div>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>可以使用 API 调用模型</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>管理自己的 API Key</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>查看自己的用量统计</span>
</li>
</ul>
</div>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-orange-500/10">
<Shield class="h-5 w-5 text-orange-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
管理员
</h3>
</div>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>管理供应商端点模型</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>管理所有用户和 Key</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>系统设置和监控</span>
</li>
</ul>
</div>
</div>
<!-- 权限对比表 -->
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
功能
</th>
<th class="px-4 py-3 text-center font-medium text-[#666663] dark:text-[#a3a094]">
普通用户
</th>
<th class="px-4 py-3 text-center font-medium text-[#666663] dark:text-[#a3a094]">
管理员
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in roleComparison"
:key="item.feature"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 text-[#262624] dark:text-[#f1ead8]">
{{ item.feature }}
</td>
<td class="px-4 py-3 text-center">
<Check
v-if="item.user"
class="h-5 w-5 text-green-500 mx-auto"
/>
<span
v-else
class="text-[#999]"
></span>
</td>
<td class="px-4 py-3 text-center">
<Check
v-if="item.admin"
class="h-5 w-5 text-green-500 mx-auto"
/>
<span
v-else
class="text-[#999]"
></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- API Key 管理 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
API Key 管理
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-orange-500/10">
<Key class="h-5 w-5 text-orange-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
什么是 API Key
</h3>
</div>
<ul class="space-y-3 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>API Key 是用户调用 {{ siteName }} API 的凭证</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>一个用户可以有多个 Key用于不同场景如开发生产测试</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>可以为 Key 设置细粒度的权限和配额限制</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>Key 的用量会记录到所属用户</span>
</li>
</ul>
</div>
</section>
<!-- Key 配置字段 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
Key 配置选项
</h2>
<div
class="overflow-hidden"
:class="[panelClasses.section]"
>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
选项
</th>
<th class="px-4 py-3 text-left font-medium text-[#666663] dark:text-[#a3a094]">
说明
</th>
<th class="px-4 py-3 text-center font-medium text-[#666663] dark:text-[#a3a094]">
必填
</th>
</tr>
</thead>
<tbody>
<tr
v-for="field in keyFields"
:key="field.name"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 font-medium text-[#262624] dark:text-[#f1ead8]">
{{ field.name }}
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ field.description }}
</td>
<td class="px-4 py-3 text-center">
<span
v-if="field.required"
:class="panelClasses.badgeGreen"
>必填</span>
<span
v-else
class="text-[#999]"
>可选</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 配额设置 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
配额设置
</h2>
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3">
<Clock class="h-5 w-5 text-[#cc785c]" />
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
配额类型
</h3>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="p-4 rounded-lg bg-[#f5f5f0]/50 dark:bg-[#1f1d1a]/50">
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8]">
请求次数配额
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
限制每日/每月的 API 调用次数超过后请求会被拒绝
</p>
</div>
<div class="p-4 rounded-lg bg-[#f5f5f0]/50 dark:bg-[#1f1d1a]/50">
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8]">
Token 用量配额
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
限制每日/每月的 Token 消耗量适合控制成本
</p>
</div>
</div>
<div class="flex items-start gap-3">
<Info class="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
配额继承
</p>
<p class="mt-1">
可以在用户级别设置默认配额新创建的 Key 会自动继承该配额
也可以在创建 Key 时覆盖默认配额
</p>
</div>
</div>
</div>
</section>
<!-- 模型访问控制 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
模型访问控制
</h2>
<div
class="p-5"
:class="[panelClasses.section]"
>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
通过允许的模型字段可以限制 Key 只能访问特定模型
</p>
<ul class="space-y-2 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>不设置</strong>Key 可以访问所有启用的模型</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>设置模型列表</strong>Key 只能访问列表中的模型</span>
</li>
</ul>
<div class="mt-4 p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<div class="flex items-start gap-2">
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-yellow-700 dark:text-yellow-300">
使用场景
</p>
<p class="text-yellow-600 dark:text-yellow-400 mt-1">
比如限制免费用户只能使用 gpt-3.5付费用户可以使用 gpt-4
或者为不同项目创建只能访问特定模型的 Key
</p>
</div>
</div>
</div>
</div>
</section>
<!-- 安全建议 -->
<section class="space-y-4">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
安全建议
</h2>
<div
class="p-4 space-y-3"
:class="[panelClasses.section]"
>
<div class="flex items-start gap-3">
<Shield class="h-5 w-5 text-[#cc785c] flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
设置 Key 有效期
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
为临时使用的 Key 设置过期时间避免遗忘造成安全风险
</p>
</div>
</div>
<div class="flex items-start gap-3">
<Shield class="h-5 w-5 text-[#cc785c] flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
按场景分配 Key
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
为开发测试生产环境分别创建 Key便于追踪和管理
</p>
</div>
</div>
<div class="flex items-start gap-3">
<Shield class="h-5 w-5 text-[#cc785c] flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
启用 IP 白名单
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
对于生产环境的 Key配置 IP 白名单可以防止 Key 泄露后被滥用
</p>
</div>
</div>
<div class="flex items-start gap-3">
<Shield class="h-5 w-5 text-[#cc785c] flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
定期审计
</p>
<p class="text-[#666663] dark:text-[#a3a094] mt-1">
定期检查用量统计和审计日志发现异常及时禁用相关 Key
</p>
</div>
</div>
</div>
</section>
<!-- 下一步 -->
<section class="pt-4">
<RouterLink
to="/guide/advanced"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="flex-1">
<div class="font-medium text-[#262624] dark:text-[#f1ead8]">
下一步高级功能
</div>
<div class="text-sm text-[#666663] dark:text-[#a3a094]">
格式转换请求头规则等高级配置
</div>
</div>
<ArrowRight class="h-5 w-5 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
</section>
</div>
</template>

View File

@@ -1,9 +1,9 @@
import type { Component } from 'vue'
import {
Rocket,
Network,
BookOpen,
Server,
Layers,
Users,
Target,
Settings,
HelpCircle
} from 'lucide-vue-next'
@@ -20,38 +20,38 @@ export interface GuideNavItem {
export const guideNavItems: GuideNavItem[] = [
{
id: 'overview',
name: '概览',
name: '快速开始',
path: '/guide',
icon: Rocket,
description: '部署后的配置指南'
},
{
id: 'architecture',
name: '架构说明',
path: '/guide/architecture',
icon: Network,
description: '系统架构与请求流程'
},
{
id: 'concepts',
name: '相关概念',
path: '/guide/concepts',
icon: BookOpen,
description: '系统架构与核心概念'
description: '核心概念深入解释'
},
{
id: 'provider',
name: '供应商管理',
path: '/guide/provider',
icon: Server,
description: '添加供应商与端点配置'
},
{
id: 'model',
name: '模型管理',
path: '/guide/model',
icon: Layers,
description: '模型映射与负载均衡'
},
{
id: 'user-key',
name: '用户与密钥',
path: '/guide/user-key',
icon: Users,
description: 'API Key 与权限管理'
id: 'strategy',
name: '关键策略',
path: '/guide/strategy',
icon: Target,
description: '调度、缓存与故障转移'
},
{
id: 'advanced',
name: '高级功能',
path: '/guide/advanced',
icon: Settings,
description: '格式转换、请求规则等'
description: '格式转换、请求规则等'
},
{
id: 'faq',
@@ -75,6 +75,7 @@ export const panelClasses = {
badgeBlue: 'inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/20 dark:border-blue-500/40 px-2 py-0.5 text-xs font-medium text-blue-600 dark:text-blue-400',
badgeGreen: 'inline-flex items-center gap-1.5 rounded-full bg-green-500/10 dark:bg-green-500/20 border border-green-500/20 dark:border-green-500/40 px-2 py-0.5 text-xs font-medium text-green-600 dark:text-green-400',
badgeYellow: 'inline-flex items-center gap-1.5 rounded-full bg-yellow-500/10 dark:bg-yellow-500/20 border border-yellow-500/20 dark:border-yellow-500/40 px-2 py-0.5 text-xs font-medium text-yellow-600 dark:text-yellow-400',
badgePurple: 'inline-flex items-center gap-1.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20 border border-purple-500/20 dark:border-purple-500/40 px-2 py-0.5 text-xs font-medium text-purple-600 dark:text-purple-400',
iconButtonSmall: [
'flex items-center justify-center rounded-lg border h-7 w-7',
'border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]',
@@ -84,30 +85,6 @@ export const panelClasses = {
].join(' ')
} as const
// 核心概念数据
export const coreConcepts = [
{
name: '供应商 (Provider)',
description: '代表一个 AI 服务提供商,如 OpenAI、Anthropic、Google 等',
color: 'blue'
},
{
name: '端点 (Endpoint)',
description: '供应商下的具体 API 端点,包含 URL、密钥、API 格式等配置',
color: 'green'
},
{
name: '模型 (Model)',
description: '可供用户使用的模型,可关联多个端点实现负载均衡',
color: 'purple'
},
{
name: 'API Key',
description: '用户访问系统的凭证,可设置权限、配额、允许的模型等',
color: 'orange'
}
]
// API 格式说明
export const apiFormats = [
{
@@ -118,10 +95,16 @@ export const apiFormats = [
},
{
name: 'OpenAI CLI',
endpoint: '/v1/responses (Codex: /responses)',
endpoint: '/v1/responses',
auth: 'Authorization: Bearer xxx',
clients: ['Codex CLI']
},
{
name: 'OpenAI Video',
endpoint: '/v1/videos',
auth: 'Authorization: Bearer xxx',
clients: ['Sora']
},
{
name: 'Claude Chat',
endpoint: '/v1/messages',
@@ -138,36 +121,148 @@ export const apiFormats = [
name: 'Gemini Chat',
endpoint: '/v1beta/models/{model}:generateContent',
auth: 'x-goog-api-key: xxx',
clients: ['Gemini SDK', 'Gemini CLI']
clients: ['Gemini SDK']
},
{
name: 'Gemini CLI',
endpoint: '/v1beta/models/{model}:generateContent',
auth: 'x-goog-api-key: xxx',
clients: ['Gemini CLI']
},
{
name: 'Gemini Video',
endpoint: '/v1beta/models/{model}:predictLongRunning',
auth: 'x-goog-api-key: xxx',
clients: ['Veo']
}
]
// 负载均衡模式
export const loadBalanceModes = [
// 配置流程步骤
export const configSteps = [
{
mode: 'priority',
name: '优先级',
description: '按端点优先级顺序调用,高优先级的先用,失败后降级到低优先级'
step: 1,
title: '添加供应商',
description: '创建供应商并配置端点URL、API Key、API 格式)',
path: '/admin/providers'
},
{
mode: 'random',
name: '随机',
description: '随机选择一个可用端点,适合多个同质端点'
step: 2,
title: '创建模型',
description: '定义用户可用的模型名,关联到端点',
path: '/admin/models'
},
{
mode: 'round_robin',
name: '轮询',
description: '依次轮流使用各个端点,分摊负载'
step: 3,
title: '发放密钥',
description: '为用户创建 API Key设置权限和配额',
path: '/admin/keys'
},
{
mode: 'weighted',
name: '加权',
description: '按权重比例分配请求,权重高的端点处理更多请求'
step: 4,
title: '开始使用',
description: '配置客户端,开始调用 API',
path: '/guide'
}
]
// 客户端配置示例
export const clientExamples = [
{
name: 'Claude Code',
configKey: 'ANTHROPIC_BASE_URL',
code: (baseUrl: string) => `# 设置环境变量
export ANTHROPIC_BASE_URL="${baseUrl}"
export ANTHROPIC_API_KEY="your-api-key"
# 启动 Claude Code
claude`,
note: '使用 Claude CLI 格式 (Authorization: Bearer)'
},
{
mode: 'latency',
name: '最低延迟',
description: '优先使用历史延迟最低的端点'
name: 'Codex CLI',
configKey: 'OPENAI_BASE_URL',
code: (baseUrl: string) => `# 设置环境变量
export OPENAI_BASE_URL="${baseUrl}"
export OPENAI_API_KEY="your-api-key"
# 启动 Codex
codex`,
note: '使用 OpenAI CLI 格式 (Responses API)'
},
{
name: 'Cursor',
configKey: 'Base URL',
code: (baseUrl: string) => `# Cursor Settings > Models > OpenAI API Key
Base URL: ${baseUrl}/v1
API Key: your-api-key`,
note: '使用 OpenAI Chat 格式'
},
{
name: 'OpenAI SDK (Python)',
configKey: 'base_url',
code: (baseUrl: string) => `from openai import OpenAI
client = OpenAI(
base_url="${baseUrl}/v1",
api_key="your-api-key"
)
response = client.chat.completions.create(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "Hello"}]
)`,
note: '使用 OpenAI Chat 格式,支持格式转换调用 Claude/Gemini'
},
{
name: 'Gemini CLI',
configKey: 'GEMINI_API_BASE',
code: (baseUrl: string) => `# 设置环境变量
export GEMINI_API_BASE="${baseUrl}"
export GEMINI_API_KEY="your-api-key"
# 启动 Gemini CLI
gemini`,
note: '使用 Gemini Chat 格式'
}
]
// 常见供应商配置
export const providerExamples = [
{
name: 'OpenAI',
url: 'https://api.openai.com',
format: 'OpenAI Chat',
note: '官方 API'
},
{
name: 'Anthropic',
url: 'https://api.anthropic.com',
format: 'Claude Chat',
note: '官方 Claude API'
},
{
name: 'Google AI',
url: 'https://generativelanguage.googleapis.com',
format: 'Gemini Chat',
note: '官方 Gemini API'
},
{
name: 'Azure OpenAI',
url: 'https://{resource}.openai.azure.com',
format: 'OpenAI Chat',
note: '替换 {resource} 为你的资源名'
},
{
name: 'OpenRouter',
url: 'https://openrouter.ai/api',
format: 'OpenAI Chat',
note: '聚合多家供应商的 API 代理'
},
{
name: '自托管/其他',
url: 'https://your-api.com',
format: 'OpenAI Chat',
note: '大多数兼容服务选择 OpenAI Chat 格式'
}
]
@@ -234,31 +329,3 @@ export const faqItems = [
answer: '表示上游服务不可用。检查1) 端点健康状态2) 供应商 API 是否正常3) 网络连接是否正常。可以在健康监控页面查看端点状态。'
}
]
// 配置流程步骤
export const configSteps = [
{
step: 1,
title: '添加供应商',
description: '创建一个供应商来组织你的 API 端点',
path: '/admin/providers'
},
{
step: 2,
title: '配置端点',
description: '在供应商下添加 API 端点,填写 URL、密钥等',
path: '/admin/providers'
},
{
step: 3,
title: '创建模型',
description: '创建用户可用的模型,关联到端点',
path: '/admin/models'
},
{
step: 4,
title: '发放密钥',
description: '为用户创建 API Key设置权限和配额',
path: '/admin/keys'
}
]