refactor(guide): 重构指南页面为 Markdown 内容驱动架构

- 将各指南页面的内联硬编码内容抽取为独立 Markdown 文件
- 新增 MarkdownViewer 组件统一渲染 Markdown 内容
- 新增 ArchitectureDiagram 组件和架构演示组件
- 新增模块指南页面 (ModulesGuide)
- GuideLayout 支持子导航、页面过渡动画和图片 Lightbox
- guide-config 重构为支持 subItems 的导航结构
- 添加指南相关截图资源
This commit is contained in:
fawney19
2026-02-23 15:50:24 +08:00
parent c43d22b0e4
commit e18a7ee435
49 changed files with 2988 additions and 4735 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -59,6 +59,12 @@ const routes: RouteRecordRaw[] = [
name: 'GuideFaq',
component: () => importWithRetry(() => import('@/views/public/guide/GuideFaq.vue')),
meta: { requiresAuth: false }
},
{
path: 'modules',
name: 'GuideModules',
component: () => importWithRetry(() => import('@/views/public/guide/ModulesGuide.vue')),
meta: { requiresAuth: false }
}
]
},

View File

@@ -1,786 +1,136 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { ArrowRight, Shuffle, FileCode, Globe, Shield, Check, Info, AlertTriangle, Settings } 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 conversionExamples = [
{
from: 'OpenAI SDK',
to: 'Claude API',
description: '用 OpenAI SDK 调用 Claude 模型',
icon: 'openai'
},
{
from: 'Anthropic SDK',
to: 'OpenAI API',
description: '用 Claude SDK 调用 GPT 模型',
icon: 'claude'
},
{
from: 'Claude Code',
to: 'OpenAI API',
description: 'Claude Code 使用 GPT 模型',
icon: 'cli'
}
]
// 请求头规则类型
const headerRuleTypes = [
{ type: 'add', name: '添加', description: '添加新的请求头,如果已存在则跳过' },
{ type: 'set', name: '设置', description: '设置请求头的值,会覆盖已有值' },
{ type: 'remove', name: '删除', description: '删除指定的请求头' }
]
// 请求体规则类型
const bodyRuleTypes = [
{ action: 'set', name: '覆写', description: '设置或覆盖指定路径的字段值' },
{ action: 'drop', name: '删除', description: '删除指定路径的字段' },
{ action: 'rename', name: '重命名', description: '将字段从一个路径移动到另一个路径' },
{ action: 'insert', name: '插入', description: '在数组的指定位置插入元素,位置留空则追加到末尾' },
{ action: 'regex_replace', name: '正则替换', description: '对字符串字段执行正则表达式替换' },
]
// 请求体规则示例
const bodyRuleExamples = [
{
title: '注入系统提示词',
description: '在 messages 数组开头插入一条 system 消息index: 0',
rule: `{
"action": "insert",
"path": "messages",
"index": 0,
"value": {
"role": "system",
"content": "你是一个专业助手"
}
}`,
},
{
title: '追加消息到末尾',
description: '不指定 index自动追加到数组末尾',
rule: `{
"action": "insert",
"path": "messages",
"value": {
"role": "user",
"content": "请用中文回答"
}
}`,
},
{
title: '设置自定义元数据',
description: '覆写嵌套字段,不存在时自动创建中间层级',
rule: `{
"action": "set",
"path": "metadata.source",
"value": "internal-app"
}`,
},
{
title: '删除不需要的字段',
description: '移除请求体中的敏感或多余字段',
rule: `{
"action": "drop",
"path": "user_info.ip_address"
}`,
},
{
title: '内容脱敏',
description: '用正则替换 messages 中的手机号',
rule: `{
"action": "regex_replace",
"path": "messages[-1].content",
"pattern": "1[3-9]\\\\d{9}",
"replacement": "[手机号已隐藏]",
"flags": ""
}`,
},
{
title: '重命名字段',
description: '将字段从旧路径移动到新路径',
rule: `{
"action": "rename",
"from": "extra.custom_id",
"to": "metadata.trace_id"
}`,
},
]
// 路径语法示例
const pathSyntaxExamples = [
{ path: 'metadata.user', desc: '嵌套 dict 字段' },
{ path: 'messages[0].content', desc: '数组第一个元素的 content 字段' },
{ path: 'messages[-1]', desc: '数组最后一个元素' },
{ path: 'matrix[0][1]', desc: '多维数组访问' },
{ path: 'config\\.v1.enabled', desc: '\\. 转义为字面量点号 → key "config.v1"' },
]
// 系统设置分类
const systemSettings = [
{
category: '基础设置',
items: [
{ name: '系统名称', description: '显示在页面标题和 Logo 旁边的名称' },
{ name: '注册开关', description: '是否允许新用户注册' },
{ name: '默认用户角色', description: '新注册用户的默认角色' }
]
},
{
category: 'API 设置',
items: [
{ name: '格式转换', description: '是否启用跨格式 API 调用' },
{ name: '请求超时', description: '默认的 API 请求超时时间' },
{ name: '流式响应', description: '是否默认启用流式响应' }
]
},
{
category: '安全设置',
items: [
{ name: 'IP 限流', description: '单 IP 的请求频率限制' },
{ name: 'Key 限流', description: '单 Key 的请求频率限制' },
{ name: 'IP 黑名单', description: '全局 IP 黑名单' }
]
}
]
import { Settings } from 'lucide-vue-next'
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-12 pb-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<Settings class="h-3 w-3" />
高级使用
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
高级功能
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 提供了一些高级功能帮助你实现更灵活的 API 管理
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
解锁 Aether 的自定义请求修改智能转换机制以及进阶配置
</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">
<Shuffle class="h-5 w-5 text-purple-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
什么是格式转换
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
格式转换允许你用一种 SDK 调用另一种格式的 API例如用 OpenAI SDK 调用 Claude 模型
系统会自动转换请求格式和响应格式
</p>
<div class="grid gap-3 md:grid-cols-3">
<div
v-for="example in conversionExamples"
:key="example.from"
class="p-4 rounded-lg bg-[#f5f5f0]/50 dark:bg-[#1f1d1a]/50"
>
<div class="flex items-center gap-2 mb-2">
<span :class="panelClasses.badgeBlue">{{ example.from }}</span>
<ArrowRight class="h-4 w-4 text-[#999]" />
<span :class="panelClasses.badgeGreen">{{ example.to }}</span>
</div>
<p class="text-xs text-[#666663] dark:text-[#a3a094]">
{{ example.description }}
</p>
</div>
</div>
</div>
<!-- 如何启用 -->
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
如何启用格式转换
</h3>
<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>
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
开启系统设置
</p>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
系统设置中开启 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">ENABLE_API_FORMAT_CONVERSION</code>
</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>
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
配置端点
</p>
<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>
<p class="font-medium text-[#262624] dark:text-[#f1ead8]">
使用
</p>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
用户现在可以用 OpenAI SDK 调用配置在 Claude 格式端点上的模型
</p>
</div>
</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">
格式转换会增加少量延迟通常 &lt;10ms部分特有功能 Claude thinkingOpenAI function calling
可能无法完美转换建议在实际场景中测试
</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]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-green-500/10">
<FileCode class="h-5 w-5 text-green-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
什么是请求头规则
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
请求头规则允许你在转发请求时修改 HTTP 请求头可以添加修改或删除特定的请求头
</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-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="rule in headerRuleTypes"
:key="rule.type"
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]">
<span :class="panelClasses.badgeBlue">{{ rule.name }}</span>
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ rule.description }}
</td>
</tr>
</tbody>
</table>
</div>
</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>
<ul class="mt-1 space-y-1">
<li> 添加额外的认证信息 API 版本号</li>
<li> 添加跟踪标记如请求 ID来源标识</li>
<li> 删除敏感信息如用户原始 IP</li>
</ul>
</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]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-orange-500/10">
<FileCode class="h-5 w-5 text-orange-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
什么是请求体规则
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
请求体规则允许你在转发请求时修改请求体JSON Body的内容可以覆写字段删除字段向数组追加/插入元素甚至用正则替换字符串值
规则按顺序依次执行受保护的顶层字段<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">model</code><code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">stream</code>不可修改
</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-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="rule in bodyRuleTypes"
:key="rule.action"
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]">
<span :class="panelClasses.badge">{{ rule.name }}</span>
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ rule.description }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 路径语法 -->
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
路径语法
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
使用点号<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">.</code>分隔层级方括号<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">[N]</code>访问数组元素
</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-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="ex in pathSyntaxExamples"
:key="ex.path"
class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] last:border-0"
>
<td class="px-4 py-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
{{ ex.path }}
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
{{ ex.desc }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 实战示例 -->
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
实战示例
</h3>
<div class="grid gap-4 md:grid-cols-2">
<div
v-for="example in bodyRuleExamples"
:key="example.title"
class="rounded-lg border border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] overflow-hidden"
>
<div class="px-4 py-2.5 border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50">
<p class="font-medium text-sm text-[#262624] dark:text-[#f1ead8]">
{{ example.title }}
</p>
<p class="text-xs text-[#666663] dark:text-[#a3a094] mt-0.5">
{{ example.description }}
</p>
</div>
<pre class="p-4 text-xs font-mono text-[#262624] dark:text-[#f1ead8] overflow-x-auto bg-[#fafaf7]/30 dark:bg-[#1a1816]/30"><code>{{ example.rule }}</code></pre>
</div>
</div>
</div>
<!-- 正则替换说明 -->
<div
class="p-5 space-y-4"
:class="[panelClasses.section]"
>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
正则替换详解
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094]">
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">regex_replace</code> 对指定路径的字符串值执行正则表达式替换
</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-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 class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
path
</td>
<td class="px-4 py-3">
<Check class="h-4 w-4 text-green-500" />
</td>
<td class="px-4 py-3 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-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
pattern
</td>
<td class="px-4 py-3">
<Check class="h-4 w-4 text-green-500" />
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
正则表达式Python re 语法保存时会校验合法性
</td>
</tr>
<tr class="border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)]">
<td class="px-4 py-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
replacement
</td>
<td class="px-4 py-3">
<Check class="h-4 w-4 text-green-500" />
</td>
<td class="px-4 py-3 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-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
flags
</td>
<td class="px-4 py-3 text-xs text-[#999]">
可选
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">i</code> 忽略大小写 /
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">m</code> 多行模式 /
<code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1 py-0.5 rounded">s</code> dotall. 匹配换行
</td>
</tr>
<tr class="last:border-0">
<td class="px-4 py-3 font-mono text-xs text-[#262624] dark:text-[#f1ead8]">
count
</td>
<td class="px-4 py-3 text-xs text-[#999]">
可选
</td>
<td class="px-4 py-3 text-[#666663] dark:text-[#a3a094]">
替换次数默认 0 = 全部替换
</td>
</tr>
</tbody>
</table>
</div>
</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>
<ul class="mt-1 space-y-1">
<li>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">注入 System Prompt</span> 在所有请求前插入统一的系统提示词
</li>
<li>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">请求增强</span> 自动追加上下文metadata 等字段
</li>
<li>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">内容过滤</span> 用正则替换脱敏敏感信息手机号邮箱等
</li>
<li>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">字段清理</span> 删除不需要的自定义字段避免上游报错
</li>
<li>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">字段适配</span> 将客户端的字段名重命名为上游期望的格式
</li>
</ul>
</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]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-blue-500/10">
<Globe class="h-5 w-5 text-blue-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
HTTP 代理
</h3>
</div>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
如果你的服务器无法直接访问某些 API如在国内访问 OpenAI可以在端点配置中设置代理
</p>
<div class="space-y-3">
<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">
在端点配置中填写代理地址格式如 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">http://proxy:8080</code>
</p>
</div>
<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">
也可以通过环境变量 <code class="text-xs bg-[#f5f5f0] dark:bg-[#1f1d1a] px-1.5 py-0.5 rounded">HTTP_PROXY</code> 设置全局代理
</p>
</div>
</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]">
系统设置页面可以配置全局参数
<!-- 1. 格式转换 -->
<section
id="format-conversion"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>1. 格式转换</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
格式转换机制允许非原生接口的请求通过 Gateway 转为下游需要的格式这一转换发生在多个层级
</p>
<div class="grid gap-4 md:grid-cols-3">
<div
v-for="category in systemSettings"
:key="category.category"
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-2 mb-3">
<Settings class="h-4 w-4 text-[#cc785c]" />
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
{{ category.category }}
</h3>
</div>
<ul class="space-y-2">
<li
v-for="item in category.items"
:key="item.name"
class="text-sm"
>
<span class="font-medium text-[#262624] dark:text-[#f1ead8]">{{ item.name }}</span>
<span class="text-[#666663] dark:text-[#a3a094]"> - {{ item.description }}</span>
</li>
</ul>
</div>
</div>
<ul class="list-decimal pl-5 space-y-2 mt-2 text-[#666663] dark:text-[#a3a094] text-sm">
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">全局格式转换</strong> 在系统层面全局开启或关闭的默认转换规则</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">提供商级转换</strong> 针对某个特定提供商的所有请求进行特定的格式转换配置</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">端点级转换</strong> 针对某个具体端点的细粒度转换支持</li>
</ul>
</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]"
>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-green-500/10">
<Shield class="h-5 w-5 text-green-500" />
</div>
<h3 class="font-semibold text-[#262624] dark:text-[#f1ead8]">
端点健康检查
</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>系统会定期检测端点的可用性</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">
<AlertTriangle class="h-5 w-5 text-yellow-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">
检查1) API URL 是否正确2) API Key 是否有效3) 网络是否可达4) 是否需要配置代理
</p>
</div>
</div>
</div>
<!-- 2. 请求上游固定非流/流式 -->
<section
id="stream-policy"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>2. 请求上游固定非流/流式</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
部分提供商在流式或非流式表现上可能存在差异此功能可强制在请求上游时使用指定的流式/非流式格式并在前端按用户请求还原
</p>
</section>
<!-- 下一步 -->
<section class="pt-4">
<RouterLink
to="/guide/faq"
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>
<!-- 3. 请求头/体编辑 -->
<section
id="header-body-edit"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>3. 请求头/体编辑</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
可在端点设置中添加规则Rules使用条件匹配与动作 Delete, Set, Insert来动态修改发往上游的 Headers JSON Payload
</p>
</section>
<!-- 4. 模型映射 -->
<section
id="model-mapping"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>4. 模型映射</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
解决提供商特定模型名称与平台标准名称不一致的问题将请求中的统一模型名动态转换为上游真实需要的模型名
</p>
</section>
<!-- 5. 正则映射 -->
<section
id="regex-mapping"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>5. 正则映射</h2>
<ul class="list-decimal pl-5 space-y-2 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">模型权限</strong> 使用正则表达式来匹配一类模型给予批量授权</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">自动获取上游模型</strong> 在自动获取的模型列表中应用正则过滤与清洗从而快速导入符合规则的标准模型列表</li>
</ul>
</section>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-12 pt-8 border-t border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]">
<!-- 6. 能力标签 -->
<section
id="capabilities"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h3 class="mt-0 text-xl text-[#262624] dark:text-[#f1ead8]">
6. 能力标签
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-2">
为特定的 Key 或模型添加自定义标签 Vision, Function Calling, Long Context通过标签约束路由只选择具备该能力的可用通道
</p>
</section>
<!-- 7. 余额监控 -->
<section
id="balance-monitor"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h3 class="mt-0 text-xl text-[#262624] dark:text-[#f1ead8]">
7. 余额监控
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-2">
针对各大提供商的官方接口或常见聚合平台自动抓取并记录剩余额度在余额低于阈值时触发报警或禁用策略
</p>
</section>
<!-- 8. 配置导入/ -->
<section
id="config-export"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h3 class="mt-0 text-xl text-[#262624] dark:text-[#f1ead8]">
8. 配置导入/
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-2">
支持将统一模型配置提供商端点及网关路由策略一键导出为 JSON并在其他部署实例中迁移导入
</p>
</section>
<!-- 9. 锁定用户密钥 -->
<section
id="lock-key"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h3 class="mt-0 text-xl text-[#262624] dark:text-[#f1ead8]">
9. 锁定用户密钥
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-2">
若监控发现恶意使用异常调用或高频报错管理员可以临时或永久锁定特定密钥以阻断攻击源头
</p>
</section>
</div>
</div>
</template>

View File

@@ -1,466 +1,159 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import {
ArrowRight,
ChevronRight,
Monitor,
Shield,
Zap,
Database,
Info,
Shuffle
} from 'lucide-vue-next'
import { Network, ArrowRight } 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)']
}
]
import ArchitectureDiagram from './components/ArchitectureDiagram.vue'
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<div class="space-y-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<Network class="h-3 w-3" />
系统架构
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
架构说明
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 的系统架构请求处理流程和数据流向
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
Aether 作为一个高性能的 AI API Gateway其核心在于统一的接入层与灵活的路由调度我们在下方提供了一个架构的数据流向动画演示帮助您直观地理解它的工作原理
</p>
</div>
<!-- 系统概览 -->
<section class="space-y-3">
<h2 class="text-xl font-semibold text-[#262624] dark:text-[#f1ead8]">
系统概览
</h2>
<!-- 动态架构图 -->
<div class="mt-10 mb-6">
<ArchitectureDiagram />
</div>
<section>
<div
class="p-5"
:class="[panelClasses.section]"
:class="[panelClasses.card]"
class="p-8 mt-6"
>
<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]">
客户端
<ul class="space-y-6">
<li class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-[#cc785c]/10 text-[#cc785c] flex items-center justify-center shrink-0 mt-1 shadow-sm">
<span class="font-bold font-mono">1</span>
</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 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0 mb-1">
多维度支持
</h3>
<span class="text-xs text-[#666663] dark:text-[#a3a094]">{{ store.role }}</span>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
全面兼容并支持多种 API 格式端点配置及不同的认证方式保障不同客户端的平滑接入
</p>
</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>
</li>
<li class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-[#cc785c]/10 text-[#cc785c] flex items-center justify-center shrink-0 mt-1 shadow-sm">
<span class="font-bold font-mono">2</span>
</div>
<div>
<h3 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0 mb-1">
统一的模型入口
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
对外暴露统一的模型名称对内自动映射到各提供商可能不同的内部模型名及其变体支持多变体优先级选择和基于亲和性的稳定路由结合负载均衡故障转移等智能调度策略
</p>
</div>
</li>
<li class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-[#cc785c]/10 text-[#cc785c] flex items-center justify-center shrink-0 mt-1 shadow-sm">
<span class="font-bold font-mono">3</span>
</div>
<div class="w-full">
<h3 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0 mb-3">
格式转换流程 (跨平台转发)
</h3>
<div class="flex flex-col md:flex-row items-stretch md:items-center gap-3 bg-[#f5f5f0]/50 dark:bg-[rgba(227,224,211,0.05)] p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)]">
<div class="flex-1 bg-white dark:bg-[#191714] border border-[#e5e4df] dark:border-white/10 rounded-lg p-3 text-center text-sm font-medium text-[#262624] dark:text-white shadow-sm">
多API格式兼容入口
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#cc785c] mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#cc785c] rotate-90" />
</div>
<div class="flex-1 bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 rounded-lg p-3 text-center text-sm font-medium text-[#cc785c] dark:text-[#d4a27f]">
格式转换
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#91918d] dark:text-white/30 mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#91918d] dark:text-white/30 rotate-90" />
</div>
<div class="flex-1 bg-white dark:bg-[#191714] border border-[#e5e4df] dark:border-white/10 rounded-lg p-3 text-center text-sm font-medium text-[#262624] dark:text-white shadow-sm">
上游提供商
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#91918d] dark:text-white/30 mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#91918d] dark:text-white/30 rotate-90" />
</div>
<div class="flex-1 bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 rounded-lg p-3 text-center text-sm font-medium text-[#cc785c] dark:text-[#d4a27f]">
格式转换
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#cc785c] mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#cc785c] rotate-90" />
</div>
<div class="flex-1 bg-white dark:bg-[#191714] border border-[#e5e4df] dark:border-white/10 rounded-lg p-3 text-center text-sm font-medium text-[#262624] dark:text-white shadow-sm">
兼容出口响应
</div>
</div>
</div>
</li>
<li class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-[#cc785c]/10 text-[#cc785c] flex items-center justify-center shrink-0 mt-1 shadow-sm">
<span class="font-bold font-mono">4</span>
</div>
<div class="w-full">
<h3 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0 mb-3">
透传流程 (原生同生态)
</h3>
<div class="flex flex-col md:flex-row items-stretch md:items-center gap-3 bg-[#f5f5f0]/50 dark:bg-[rgba(227,224,211,0.05)] p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)]">
<div class="flex-1 bg-white dark:bg-[#191714] border border-[#e5e4df] dark:border-white/10 rounded-lg p-3 text-center text-sm font-medium text-[#262624] dark:text-white shadow-sm">
原生格式 API 入口
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#cc785c] mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#cc785c] rotate-90" />
</div>
<div class="flex-[2] bg-blue-500/5 dark:bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 text-center text-sm font-medium text-blue-600 dark:text-blue-400">
同格式请求原生透传 上游提供商 原生响应透传
</div>
<ArrowRight class="hidden md:block w-5 h-5 text-[#cc785c] mx-1 shrink-0" />
<div class="md:hidden flex justify-center py-1">
<ArrowRight class="w-5 h-5 text-[#cc785c] rotate-90" />
</div>
<div class="flex-1 bg-white dark:bg-[#191714] border border-[#e5e4df] dark:border-white/10 rounded-lg p-3 text-center text-sm font-medium text-[#262624] dark:text-white shadow-sm">
原生 API 出口
</div>
</div>
</div>
</li>
<li class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center shrink-0 mt-1 shadow-sm">
<span class="font-bold font-mono">5</span>
</div>
<div>
<h3 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0 mb-1">
响应回路
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
上游提供商返回的响应会经过逆向格式转换跨平台场景或直接透传同生态场景同时将提供商内部的模型名还原为客户端请求时使用的统一模型名确保客户端收到格式和字段完全兼容的响应
</p>
</div>
</li>
</ul>
</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

@@ -1,408 +1,398 @@
<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: []
}
]
import { BookOpen } from 'lucide-vue-next'
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<div class="space-y-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<BookOpen class="h-3 w-3" />
文档核心
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
相关概念
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
深入理解 {{ siteName }} 中的核心概念及其关系
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
深入理解 Aether 运行时的核心模块和层级结构
</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>
<!-- 1. 创建统一模型 -->
<section
id="create-model"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>1. 创建统一模型</h2>
<div class="space-y-4 mt-4">
<div>
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8]">
1. 左侧模型选择搜索区域
</h4>
<ul class="list-disc pl-5 mt-2 space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li>模型来源opencode 维护的 https://models.dev</li>
<li>仅显示几个官方模型提供商可以通过搜索获取更多模型</li>
</ul>
</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>
<div>
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8]">
2. 右侧参数配置区域
</h4>
<ul class="list-disc pl-5 mt-2 space-y-1 text-sm text-[#666663] dark:text-[#a3a094]">
<li>手动添加模型 选择模型后自动填写</li>
<li>模型偏好标定这个模型是否支持这些能力一般情况下可不选</li>
<li>
价格配置
<ul class="list-[circle] pl-5 mt-1 space-y-1">
<li>计费方式Token价格 + 按次价格 + 视频计费 = 最终计费</li>
<li>价格阶梯以总Token数落在哪个区间为准并非按阶梯溢出补全式计费</li>
</ul>
</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]"
<img
loading="lazy"
src="/guide/concepts-create-model.webp"
alt="创建统一模型"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm mt-4 w-full"
>
<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]"
<!-- 2. 添加提供商 -->
<section
id="add-provider"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>2. 添加提供商</h2>
<div class="space-y-4 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
<ul class="list-decimal pl-5 space-y-2">
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">提供商类型</strong>自定义或反代一般自定义即可反代请进入反代章节</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">计费类型</strong>
<ul class="list-disc pl-5 mt-1 space-y-1">
<li>按量付费持续使用</li>
<li>月卡额度按周期()限额</li>
<li>免费套餐不计入成本即倍率为0</li>
</ul>
</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">最大重试次数</strong>在缓存亲和调度模式下首次请求失败后的重试次数</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">超时时间</strong>
<ul class="list-disc pl-5 mt-1 space-y-1">
<li>流式首字超时时间流式请求收到首字前的超时时间</li>
<li>非流请求超时时间非流请求的总超时时间</li>
</ul>
</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">保持优先级</strong>通过格式转换的请求是否保持当前优先级</li>
</ul>
<img
loading="lazy"
src="/guide/concepts-add-provider.webp"
alt="添加提供商"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm mt-4 w-full"
>
<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"
</div>
</section>
<!-- 3. 添加端点 -->
<section
id="add-endpoint"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>3. 添加端点</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
添加端点是添加上游支持的端点并非你需要使用的端点比如 Anyrouter 只支持 Claude Code 接入那么就应该仅添加 Claude CLI 端点
</p>
<ul class="list-decimal pl-5 space-y-2 text-[#666663] dark:text-[#a3a094] text-sm">
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">选择格式</strong>选择上游支持的端点格式</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">自定义 Base URLPath</strong>根据上游提供的信息填写为空则使用提示的默认值</li>
</ul>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<img
loading="lazy"
src="/guide/concepts-add-endpoint-1.webp"
alt="添加端点选择格式"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-add-endpoint-2.webp"
alt="端点自定义"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
</div>
</section>
<!-- 4. 添加密钥 -->
<section
id="add-key"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>4. 添加密钥</h2>
<ul class="list-decimal pl-5 space-y-3 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">认证类型</strong>
<ul class="list-disc pl-5 mt-1">
<li>API Key: 传统密钥</li>
<li>Vertex AI: 用于 Google Cloud</li>
</ul>
</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">支持的API格式</strong>勾选Key可以使用的API格式同时可以设置key访问该端点时的倍率</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">优先级</strong>在提供商优先的调度模式下不同优先级的Key按顺序故障转移同优先级的Key进行负载均衡</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">RPM限制</strong>Key的使用速率限制会预留10%给已有用户进行提高缓存命中率
<ul class="list-disc pl-5 mt-1">
<li>填写固定数值</li>
<li>不填为空采用一定策略自适应学习</li>
</ul>
</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">缓存TTL</strong>
<ul class="list-disc pl-5 mt-1">
<li>0: 不使用缓存优先</li>
<li>N: 同一个用户Key在N分钟内优先使用之前使用的提供商Key响应请求</li>
</ul>
</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">熔断探测</strong><br>
当同一个提供商Key字连续若干次请求失败后会进入熔断状态之后每间N分钟进行探测请求若请求成功解除熔断后续正常请求否则按以指数级增长探测时间以待下次探测最大探测间隔不会增长超过32分钟
</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">能力标签</strong>定义该Key可以使用的能力</li>
<li>
<strong class="text-[#262624] dark:text-[#f1ead8] font-medium">自动获取上游模型</strong><br>
在上游获取模型端点支持的情况下从接口自动获取可以用模型列表且按一定时间自动刷新不开启则默认任意模型可用或在后续模型权限中手动添加
</li>
</ul>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<img
loading="lazy"
src="/guide/concepts-add-key-1.webp"
alt="添加密钥认证"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-add-key-2.webp"
alt="密钥倍率与优先级"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
</div>
</section>
<!-- 5. 模型权限 -->
<section
id="model-permission"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>5. 模型权限</h2>
<ul class="list-decimal pl-5 space-y-2 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
<li>在编辑密钥中开启自动获取/刷新上游全部模型</li>
<li>手动限制/创建该Key的可用模型</li>
<li>不开启自动获取且勾选任意模型即模型权限为空则认为可以使用提供商的全部关联模型</li>
<li>只有在模型权限不为空的情况下才可以配合正则映射模型</li>
</ul>
<div class="grid grid-cols-1 gap-4 mt-6">
<img
loading="lazy"
src="/guide/concepts-model-perms-1.webp"
alt="模型权限 1"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<img
loading="lazy"
src="/guide/concepts-model-perms-2.webp"
alt="模型权限 2"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-model-perms-3.webp"
alt="模型权限 3"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<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]">
下一步关键策略
<!-- 6. 关联模型 -->
<section
id="link-model"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>6. 关联模型</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
从全局模型关联提供商模型即设置该提供商可以访问的模型同时可以在这里设置提供商自己的模型价格默认继承全局模型的价格
</p>
<div class="grid grid-cols-1 gap-4 mt-6">
<img
loading="lazy"
src="/guide/concepts-link-model-1.webp"
alt="关联模型 1"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<img
loading="lazy"
src="/guide/concepts-link-model-2.webp"
alt="关联模型 2"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-link-model-3.webp"
alt="关联模型 3"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
</div>
</div>
</section>
<!-- 7. 模型映射 -->
<section
id="model-mapping"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>7. 模型映射</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
如果该提供商的请求名称并非标准名称即可通过映射改变在实际请求提供时的模型 id
</p>
<div class="bg-[#f5f5f0]/80 dark:bg-[rgba(227,224,211,0.05)] p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] text-sm mb-6">
<p class="mb-2">
<span class="text-[#cc785c] font-medium mr-2">名称修正示例:</span>官方标准名称为 <code class="bg-black/5 dark:bg-white/10 px-1 py-0.5 rounded text-xs">claude-opus-4-6</code> 实际提供商叫 <code class="bg-black/5 dark:bg-white/10 px-1 py-0.5 rounded text-xs">claude-opus-4-6-last</code>
</p>
<p><span class="text-[#cc785c] font-medium mr-2">/升级请求示例:</span>官方标准名称为 <code class="bg-black/5 dark:bg-white/10 px-1 py-0.5 rounded text-xs">claude-opus-4-6</code> 实际请求提供商用 <code class="bg-black/5 dark:bg-white/10 px-1 py-0.5 rounded text-xs">claude-sonnet-4-6</code></p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<img
loading="lazy"
src="/guide/concepts-model-mapping-1.webp"
alt="模型映射 1"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-model-mapping-2.webp"
alt="模型映射 2"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
</div>
</section>
<!-- 8. 反向代理 -->
<section
id="reverse-proxy"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>8. 反向代理</h2>
<div class="space-y-6 mt-6">
<div>
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8] mb-3">
1. Codex
</h4>
<img
loading="lazy"
src="/guide/concepts-reverse-proxy-codex.webp"
alt="Codex 反向代理"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full max-w-2xl"
>
</div>
<div>
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8] mb-3">
2. Krio
</h4>
<img
loading="lazy"
src="/guide/concepts-reverse-proxy-kiro.webp"
alt="Krio 反向代理"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full max-w-2xl"
>
</div>
<div>
<h4 class="font-medium text-[#262624] dark:text-[#f1ead8] mb-3">
3. Antigravity
</h4>
<img
loading="lazy"
src="/guide/concepts-reverse-proxy-antigravity.webp"
alt="Antigravity 反向代理"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full max-w-2xl"
>
</div>
</div>
</section>
<!-- 9. 优先级管理 -->
<section
id="priority-management"
class="scroll-mt-24 lg:scroll-mt-20 pb-8"
>
<h2>9. 优先级管理</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
允许拖动或者直接点击数字输入调整优先级
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div class="space-y-4">
<div class="bg-white/50 dark:bg-white/5 p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] shadow-sm">
<h4 class="font-bold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
1. 提供商优先
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
按提供商顺序调度同优先级负载均衡
</p>
</div>
<div class="text-xs text-[#666663] dark:text-[#a3a094]">
了解调度缓存亲和性故障转移等核心策略
<div class="bg-white/50 dark:bg-white/5 p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] shadow-sm">
<h4 class="font-bold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
2. Key优先
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
全局Key统一调度同优先级负载均衡
</p>
</div>
<div class="bg-white/50 dark:bg-white/5 p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] shadow-sm">
<h4 class="font-bold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
3. 缓存亲和模式
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
在Key TTL时间的约束下优先使用上一次请求使用的Key
</p>
</div>
<div class="bg-white/50 dark:bg-white/5 p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] shadow-sm">
<h4 class="font-bold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
4. 负载均衡模式
</h4>
<ul class="list-disc pl-5 mt-1 text-sm text-[#666663] dark:text-[#a3a094] space-y-1">
<li>取消全局提供商优先级提供商内部Key依然保持优先级</li>
<li>取消全局Key优先级乱序使用</li>
</ul>
</div>
<div class="bg-white/50 dark:bg-white/5 p-4 rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] shadow-sm">
<h4 class="font-bold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
5. 固定顺序模式
</h4>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mt-1">
取消缓存亲和始终使用固定顺序Key请求
</p>
</div>
</div>
<ArrowRight class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</RouterLink>
<div class="space-y-4">
<img
loading="lazy"
src="/guide/concepts-priority-1.webp"
alt="优先级管理 1"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
<img
loading="lazy"
src="/guide/concepts-priority-2.webp"
alt="优先级管理 2"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm w-full"
>
</div>
</div>
</section>
</div>
</template>

View File

@@ -1,217 +1,73 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Search, ChevronDown, ExternalLink, HelpCircle } from 'lucide-vue-next'
import { faqItems, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
import { reactive } from 'vue'
import { HelpCircle, ChevronRight } from 'lucide-vue-next'
withDefaults(
defineProps<{
baseUrl?: string
}>(),
const faqs = reactive([
{
baseUrl: typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com'
id: 1,
question: '1. 为什么发生错误没有进行故障转移?',
answer: '故障转移机制Failover依赖于配置的重试策略和上游返回的错误码类型。通常只有在遇到 429 (Too Many Requests) 或 5xx (Server Errors) 时,并且在”最大重试次数”允许的范围内,才会触发故障转移。对于 400 (Bad Request) 或 401 (Unauthorized) 这种客户端确切错误,为避免持续无效重试,系统可能直接中断请求并返回错误。同时也要检查调度模式是否支持故障转移(例如固定顺序模式或缓存亲和性强绑定时可能会影响行为)。',
isOpen: true
},
{
id: 2,
question: '2. 如何排查请求不通的问题?',
answer: '建议首先检查【系统设置】中的请求体记录等级是否设为 `Full`,以便抓取完整的上下行数据。然后检查对应的【提供商端点】的 Base URL 和 API 格式是否正确匹配。如果开启了带来,排查【代理配置】优先级。',
isOpen: false
}
)
])
const { siteName } = useSiteInfo()
// 搜索关键词
const searchQuery = ref('')
// 展开的 FAQ
const expandedIds = ref<Set<string>>(new Set())
// 过滤后的 FAQ
const filteredFaqs = computed(() => {
if (!searchQuery.value.trim()) {
return faqItems
}
const query = searchQuery.value.toLowerCase()
return faqItems.filter(
item =>
item.question.toLowerCase().includes(query) ||
item.answer.toLowerCase().includes(query)
)
})
// 按分类分组
const faqsByCategory = computed(() => {
const grouped: Record<string, typeof faqItems> = {}
for (const item of filteredFaqs.value) {
if (!grouped[item.category]) {
grouped[item.category] = []
}
grouped[item.category].push(item)
}
return grouped
})
// 切换展开状态
function toggleExpand(id: string) {
if (expandedIds.value.has(id)) {
expandedIds.value.delete(id)
} else {
expandedIds.value.add(id)
}
}
// 全部展开/收起
function toggleAll() {
if (expandedIds.value.size === filteredFaqs.value.length) {
expandedIds.value.clear()
} else {
expandedIds.value = new Set(filteredFaqs.value.map(item => item.id))
}
const toggleFaq = (index: number) => {
faqs[index].isOpen = !faqs[index].isOpen
}
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-12 pb-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<HelpCircle class="h-3 w-3" />
答疑解惑
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
常见问题
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
关于 {{ siteName }} 使用和配置的常见问题解答
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
在使用 Aether 过程中遇到的常见问题与排错指南
</p>
</div>
<!-- 搜索栏 -->
<div
class="p-4"
:class="[panelClasses.section]"
>
<div class="flex items-center gap-3">
<Search class="h-5 w-5 text-[#999]" />
<input
v-model="searchQuery"
type="text"
placeholder="搜索问题..."
class="flex-1 bg-transparent border-none outline-none text-[#262624] dark:text-[#f1ead8] placeholder:text-[#999]"
<section class="scroll-mt-24 lg:scroll-mt-20">
<div class="space-y-4 mt-8">
<div
v-for="(faq, index) in faqs"
:key="faq.id"
class="bg-white/50 dark:bg-white/5 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.06)] rounded-xl overflow-hidden shadow-sm transition-all"
>
<button
v-if="filteredFaqs.length > 0"
class="text-sm text-[#cc785c] hover:underline"
@click="toggleAll"
>
{{ expandedIds.size === filteredFaqs.length ? '全部收起' : '全部展开' }}
</button>
</div>
</div>
<!-- FAQ 列表 -->
<div
v-if="filteredFaqs.length > 0"
class="space-y-6"
>
<div
v-for="category in Object.keys(faqsByCategory)"
:key="category"
class="space-y-3"
>
<h2 class="text-lg font-semibold text-[#262624] dark:text-[#f1ead8] flex items-center gap-2">
<HelpCircle class="h-5 w-5 text-[#cc785c]" />
{{ category }}
</h2>
<div class="space-y-2">
<div
v-for="faq in faqsByCategory[category]"
:key="faq.id"
class="overflow-hidden"
:class="[panelClasses.section]"
<button
class="w-full flex items-center justify-between p-5 text-left hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
@click="toggleFaq(index)"
>
<button
class="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-[#f5f5f0]/50 dark:hover:bg-[#1f1d1a]/50 transition-colors"
@click="toggleExpand(faq.id)"
>
<span class="font-medium text-[#262624] dark:text-[#f1ead8] pr-4">
{{ faq.question }}
</span>
<ChevronDown
class="h-5 w-5 text-[#999] flex-shrink-0 transition-transform"
:class="{ 'rotate-180': expandedIds.has(faq.id) }"
/>
</button>
<div
v-show="expandedIds.has(faq.id)"
class="px-4 pb-4 text-sm text-[#666663] dark:text-[#a3a094] whitespace-pre-line"
>
<h3 class="text-lg font-medium text-[#262624] dark:text-[#f1ead8] m-0">
{{ faq.question }}
</h3>
<ChevronRight
class="w-5 h-5 text-[#91918d] dark:text-[#a3a094] transition-transform duration-200"
:class="{ 'rotate-90 text-[#cc785c]': faq.isOpen }"
/>
</button>
<div
v-show="faq.isOpen"
class="px-5 pb-5 pt-0 text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed border-t border-[#e5e4df]/50 dark:border-[rgba(227,224,211,0.06)] mt-2"
>
<div class="pt-4">
{{ faq.answer }}
</div>
</div>
</div>
</div>
</div>
<!-- 无结果 -->
<div
v-else
class="p-8 text-center"
:class="[panelClasses.section]"
>
<HelpCircle class="h-12 w-12 text-[#999] mx-auto mb-4" />
<p class="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="grid gap-4 md:grid-cols-2">
<a
href="https://github.com/your-repo/aether"
target="_blank"
rel="noopener noreferrer"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="p-2 rounded-lg bg-gray-500/10">
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</div>
<div class="flex-1">
<div class="font-medium text-[#262624] dark:text-[#f1ead8]">GitHub</div>
<div class="text-sm text-[#666663] dark:text-[#a3a094]">查看源码提交 Issue</div>
</div>
<ExternalLink class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</a>
<a
href="https://github.com/your-repo/aether/discussions"
target="_blank"
rel="noopener noreferrer"
class="p-4 flex items-center gap-3 group"
:class="[panelClasses.section, panelClasses.cardHover]"
>
<div class="p-2 rounded-lg bg-blue-500/10">
<svg
class="h-5 w-5 text-blue-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</div>
<div class="flex-1">
<div class="font-medium text-[#262624] dark:text-[#f1ead8]">Discussions</div>
<div class="text-sm text-[#666663] dark:text-[#a3a094]">社区讨论功能建议</div>
</div>
<ExternalLink class="h-4 w-4 text-[#999] group-hover:text-[#cc785c] transition-colors" />
</a>
</div>
</section>
</div>
</template>

View File

@@ -62,13 +62,13 @@
/>
</RouterLink>
<!-- 快速开始子导航 -->
<!-- 子导航 -->
<div
v-if="item.id === 'overview' && isNavActive(item.path)"
class="ml-7 space-y-0.5 mt-0.5"
v-if="item.subItems && isNavActive(item.path)"
class="ml-7 space-y-0.5 mt-0.5 mb-2"
>
<a
v-for="sub in overviewSubItems"
v-for="sub in item.subItems"
:key="sub.hash"
:href="sub.hash"
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[12px] transition-colors"
@@ -273,21 +273,55 @@
</header>
</template>
<div class="max-w-4xl mx-auto">
<article
class="max-w-4xl mx-auto pb-24"
@click="onArticleClick"
>
<RouterView
v-slot="{ Component }"
>
<component
:is="Component"
:base-url="baseUrl"
/>
<transition
name="fade"
mode="out-in"
>
<component
:is="Component"
:base-url="baseUrl"
class="literary-content"
/>
</transition>
</RouterView>
</div>
</article>
<!-- Image Lightbox -->
<Teleport to="body">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="lightboxSrc"
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm cursor-zoom-out"
@click="lightboxSrc = ''"
>
<img
:src="lightboxSrc"
:alt="lightboxAlt"
class="max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl"
@click.stop
>
</div>
</Transition>
</Teleport>
</AppShell>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
Menu,
@@ -310,27 +344,92 @@ const { siteName, siteSubtitle } = useSiteInfo()
const mobileMenuOpen = ref(false)
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
const activeHash = ref('#production')
const activeHash = ref('')
const lightboxSrc = ref('')
const lightboxAlt = ref('')
// 快速开始子导航
const overviewSubItems = [
{ name: '部署', hash: '#production' },
{ name: '配置流程', hash: '#config-steps' },
{ name: 'API 格式', hash: '#api-formats' },
{ name: '推荐帖子', hash: '#recommended-posts' }
]
function onArticleClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG' && target.closest('.literary-content')) {
const img = target as HTMLImageElement
lightboxSrc.value = img.src
lightboxAlt.value = img.alt || ''
}
}
let observer: IntersectionObserver | null = null
function getScrollContainer(): Element | null {
return document.querySelector('.app-shell__content')
}
function setupIntersectionObserver() {
if (observer) {
observer.disconnect()
}
const scrollRoot = getScrollContainer()
observer = new IntersectionObserver(
(entries) => {
const visibleEntries = entries.filter((entry) => entry.isIntersecting)
if (visibleEntries.length > 0) {
const topEntry = visibleEntries.reduce((prev, current) => {
return (current.boundingClientRect.top < prev.boundingClientRect.top) ? current : prev
})
activeHash.value = `#${topEntry.target.id}`
}
},
{
root: scrollRoot,
rootMargin: '-80px 0px -70% 0px',
threshold: 0
}
)
const sections = document.querySelectorAll('article section[id]')
sections.forEach((section) => observer?.observe(section))
}
function scrollToHash(hash: string) {
activeHash.value = hash
const el = document.querySelector(hash)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
const container = getScrollContainer()
if (el && container) {
const elTop = el.getBoundingClientRect().top
const containerTop = container.getBoundingClientRect().top
const offset = elTop - containerTop + container.scrollTop - 80
container.scrollTo({ top: offset, behavior: 'smooth' })
}
}
// 路由变化时关闭移动端菜单
watch(() => route.path, () => {
mobileMenuOpen.value = false
// 路由变化时管理状态和观察者
watch(
() => route.path,
() => {
mobileMenuOpen.value = false
activeHash.value = ''
nextTick(() => {
setupIntersectionObserver()
const firstSection = document.querySelector('article section[id]')
if (!activeHash.value && firstSection) {
activeHash.value = `#${firstSection.id}`
}
})
},
{ immediate: true }
)
onMounted(() => {
nextTick(() => {
setupIntersectionObserver()
})
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
const currentNavItem = computed(() => {
@@ -347,6 +446,7 @@ function isNavActive(href: string) {
// 移动端菜单用的导航数据
const navigation = computed(() => [
{
title: '',
items: guideNavItems.map(item => ({
name: item.name,
href: item.path,
@@ -365,11 +465,64 @@ const contentClasses = computed(() => {
})
const mainClasses = computed(() => {
return 'pt-24 lg:pt-6'
return 'pt-24 lg:pt-8'
})
</script>
<style scoped>
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
/* Literary Tech Typography Overrides for Guide Content */
:deep(.literary-content) h2 {
@apply text-2xl mb-8 mt-12 flex items-center gap-3 transition-colors;
font-family: var(--serif);
font-weight: 500;
letter-spacing: -0.015em;
color: var(--color-text);
}
:deep(.literary-content) h3 {
@apply text-xl mb-6 mt-10 transition-colors;
font-family: var(--serif);
font-weight: 500;
letter-spacing: -0.01em;
color: var(--color-text);
}
:deep(.literary-content) p:not([class*="text-sm"]):not([class*="text-xs"]) {
font-family: var(--serif);
font-weight: 400;
@apply leading-relaxed text-[1.05rem] mb-4;
color: var(--color-text);
opacity: 0.9;
}
:deep(.literary-content) li:not([class*="text-sm"]):not([class*="text-xs"]) {
font-family: var(--serif);
font-weight: 400;
@apply leading-relaxed text-[1.05rem] mb-2;
color: var(--color-text);
opacity: 0.9;
}
/* UI Elements inside content should remain sans-serif */
:deep(.literary-content) button,
:deep(.literary-content) input,
:deep(.literary-content) select,
:deep(.literary-content) label,
:deep(.literary-content) table,
:deep(.literary-content) .font-mono,
:deep(.literary-content) [class*="font-mono"] {
font-family: var(--sans-serif);
}
:deep(.literary-content) pre,
:deep(.literary-content) code {
font-family: var(--monospace) !important;
}
:deep(.literary-content) img {
cursor: zoom-in;
}
</style>

View File

@@ -0,0 +1,151 @@
<script setup lang="ts">
import { Blocks } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
</script>
<template>
<div class="space-y-12 pb-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<Blocks class="h-3 w-3" />
系统扩展
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
模块管理
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
Aether 提供高可插入的模块化管理机制帮助连接外部服务和授权系统
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
<!-- 1. 访问令牌 -->
<section
id="management-tokens"
:class="[panelClasses.card]"
class="p-6 scroll-mt-24 lg:scroll-mt-20 flex flex-col"
>
<div class="w-12 h-12 rounded-xl bg-purple-500/10 text-purple-600 dark:text-purple-400 flex items-center justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-key-round"
><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /><circle
cx="16.5"
cy="7.5"
r=".5"
fill="currentColor"
/></svg>
</div>
<h3 class="text-xl font-bold text-[#262624] dark:text-[#f1ead8] mb-2 m-0 mt-0 pt-0 border-0">
1. 访问令牌
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
允许通过 Management Token 授权使用全部或部分后端敏感接口如自动化运维脚本接入
</p>
</section>
<!-- 2. 邮件配置 -->
<section
id="email-config"
:class="[panelClasses.card]"
class="p-6 scroll-mt-24 lg:scroll-mt-20 flex flex-col"
>
<div class="w-12 h-12 rounded-xl bg-blue-500/10 text-blue-600 dark:text-blue-400 flex items-center justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-mail"
><rect
width="20"
height="16"
x="2"
y="4"
rx="2"
/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></svg>
</div>
<h3 class="text-xl font-bold text-[#262624] dark:text-[#f1ead8] mb-2 m-0 mt-0 pt-0 border-0">
2. 邮件配置
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
用于注册验证及账户通知服务管理员可以自定义 SMTP 服务器并高度定制系统发送的各种 HTML 邮件模板
</p>
</section>
<!-- 3. OAuth 登录 -->
<section
id="oauth-login"
:class="[panelClasses.card]"
class="p-6 scroll-mt-24 lg:scroll-mt-20 flex flex-col"
>
<div class="w-12 h-12 rounded-xl bg-orange-500/10 text-orange-600 dark:text-orange-400 flex items-center justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-user-check"
><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle
cx="9"
cy="7"
r="4"
/><polyline points="16 11 18 13 22 9" /></svg>
</div>
<h3 class="text-xl font-bold text-[#262624] dark:text-[#f1ead8] mb-2 m-0 mt-0 pt-0 border-0">
3. OAuth 登录
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
支持包括 Linux Do OAuth 等多种第三方授权验证登录方便统一认证渠道并提升用户体验
</p>
</section>
<!-- 4. LDAP 认证 -->
<section
id="ldap-auth"
:class="[panelClasses.card]"
class="p-6 scroll-mt-24 lg:scroll-mt-20 flex flex-col"
>
<div class="w-12 h-12 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-building-2"
><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z" /><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2" /><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2" /><path d="M10 6h4" /><path d="M10 10h4" /><path d="M10 14h4" /><path d="M10 18h4" /></svg>
</div>
<h3 class="text-xl font-bold text-[#262624] dark:text-[#f1ead8] mb-2 m-0 mt-0 pt-0 border-0">
4. LDAP 认证
</h3>
<p class="text-sm text-[#666663] dark:text-[#a3a094] leading-relaxed">
为企业管理构建直接对接企业现有的 LDAP Active Directory 目录以映射内网员工账户和组织架构
</p>
</section>
</div>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -1,610 +1,130 @@
<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 级'
}
]
import { Target } from 'lucide-vue-next'
</script>
<template>
<div class="space-y-8">
<!-- 标题 -->
<div class="space-y-3">
<div class="space-y-12 pb-12">
<!-- Hero 区域 -->
<div class="space-y-4">
<div class="inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]">
<Target class="h-3 w-3" />
调度与策略
</div>
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
关键策略
</h1>
<p class="text-base text-[#666663] dark:text-[#a3a094]">
{{ siteName }} 的调度缓存亲和性故障转移并发控制等核心策略机制
<p class="text-base text-[#666663] dark:text-[#a3a094] max-w-2xl">
了解 Aether 内部的日志记录智能调度模式以及服务限制策略
</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 组合调度模式决定了候选的排序方式
<section
id="request-logging"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>1. 请求体记录</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
在系统设置中您可以修改请求体记录详情等级以便于调试和审计
</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"
<div class="overflow-hidden rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-white dark:bg-[#191714] shadow-sm max-w-2xl">
<table class="w-full text-sm text-left">
<thead class="bg-[#f5f5f0] dark:bg-[rgba(227,224,211,0.05)] border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]">
<tr>
<th
scope="col"
class="px-6 py-3 font-medium text-[#262624] dark:text-[#f1ead8]"
>
<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"
日志等级
</th>
<th
scope="col"
class="px-6 py-3 font-medium text-[#262624] dark:text-[#f1ead8]"
>
<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>
记录内容
</th>
</tr>
</thead>
<tbody class="divide-y divide-[#e5e4df] dark:divide-[rgba(227,224,211,0.06)]">
<tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<td class="px-6 py-4 font-mono font-medium text-[#cc785c] dark:text-[#d4a27f]">
Base
</td>
<td class="px-6 py-4 text-[#666663] dark:text-[#a3a094]">
基本请求信息IP, 模型, 耗时, Token
</td>
</tr>
<tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<td class="px-6 py-4 font-mono font-medium text-[#cc785c] dark:text-[#d4a27f]">
Headers
</td>
<td class="px-6 py-4 text-[#666663] dark:text-[#a3a094]">
Base + 请求头 (Headers)
</td>
</tr>
<tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors border-b-0">
<td class="px-6 py-4 font-mono font-medium text-[#cc785c] dark:text-[#d4a27f]">
Full
</td>
<td class="px-6 py-4 text-[#666663] dark:text-[#a3a094]">
Headers + 完整的请求体与响应体 (Payloads)
</td>
</tr>
</tbody>
</table>
</div>
<div
class="p-4"
:class="[panelClasses.section]"
<img
loading="lazy"
src="/guide/strategy-request-logging.webp"
alt="请求体记录设置"
class="rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] shadow-sm mt-6 w-full max-w-3xl"
>
<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
id="scheduling"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>2. 调度模式</h2>
<ul class="list-decimal pl-5 space-y-2 mt-4 text-[#666663] dark:text-[#a3a094] text-sm">
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">提供商优先</strong> 优先根据提供商设置的顺序进行调度</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">Key优先</strong> 无视提供商层级直接在所有可用的 Key 之间根据优先级进行调度</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">缓存亲和</strong> 尽量将相同用户的请求路由到之前处理过该用户请求的提供商/节点以最大化利用上游缓存</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">负载均衡</strong> 在相同优先级的节点之间均匀分配流量</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">固定顺序</strong> 取消随机性与动态调整严格按照固定的顺序遍历尝试</li>
<li><strong class="text-[#262624] dark:text-[#f1ead8] font-medium">故障转移</strong> 当请求失败时根据策略自动切换到下一个可用的备用节点进行重试</li>
</ul>
</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
id="rate-limit"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>3. 访问限制</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
系统支持多种维度的访问频率限制Rate Limit有效防止恶意请求或滥用保障服务稳定性
</p>
</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
id="payload-cleanup"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>4. 请求体压缩清理</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
为节省数据库空间与提高查询性能系统提供自动请求体清理与压缩策略将历史请求详情定期冷热分离并清理
</p>
</section>
<section
id="cron-tasks"
class="scroll-mt-24 lg:scroll-mt-20"
>
<h2>5. 定时任务</h2>
<p class="text-sm text-[#666663] dark:text-[#a3a094] mb-4">
平台内置多个定时任务用于模型列表同步缓存清理余额监控及统计数据聚合等周期性操作
</p>
</section>
</div>
</template>

View File

@@ -0,0 +1,706 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useDarkMode } from '@/composables/useDarkMode'
const { isDark } = useDarkMode()
// Ultra-premium floating aesthetic palette
const colors = computed(() => {
const brandVal = isDark.value ? '#d4a27f' : '#cc785c'
return {
// Pure, infinite canvas
bg: isDark.value ? '#080808' : '#fafafa',
grid: isDark.value ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
// Floating Cards - completely solid, crisp edges
cardBg: isDark.value ? '#121212' : '#ffffff',
cardBorder: isDark.value ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)',
// Core Card
coreBg: isDark.value ? '#171615' : '#ffffff',
coreBorder: isDark.value ? 'rgba(212, 162, 127, 0.3)' : 'rgba(204, 120, 92, 0.25)',
// Text
textMain: isDark.value ? '#f5f5f5' : '#111111',
textMuted: isDark.value ? '#888888' : '#777777',
// Thematic Flow Colors
brand: brandVal, // Ingress
convertAccent: '#a855f7', // Conversion
passAccent: '#3b82f6', // Passthrough
returnAccent: '#10b981', // Loop
proxyAccent: '#f59e0b', // Proxies
// The fluid river tracks
trackMain: isDark.value ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'
}
})
// Clean, precise floating shadow
const shadows = computed(() => {
return isDark.value
? {
node: '0 8px 30px rgba(0,0,0,0.6)',
coreGlow: '0 0 40px rgba(212, 162, 127, 0.03)',
}
: {
node: '0 8px 30px rgba(0,0,0,0.06)',
coreGlow: '0 0 40px rgba(204, 120, 92, 0.03)',
}
})
</script>
<template>
<div
class="relative w-full overflow-hidden rounded-[32px] border diagram-container"
:style="{
backgroundColor: colors.bg,
borderColor: colors.cardBorder,
/* Hyper-clean infinite Dot Grid Background */
backgroundImage: `radial-gradient(${colors.grid} 1px, transparent 1px)`,
backgroundSize: '24px 24px'
}"
>
<!-- Horizontal scroll wrapper for the dynamic wide canvas -->
<div class="w-full overflow-x-auto pb-4 custom-scrollbar">
<!-- Absolute, unconstrained canvas simulating an infinite whiteboard -->
<!-- Width extends far enough to fit all spaced-out floating nodes -->
<div class="relative min-w-[1300px] h-[680px] mx-auto overflow-visible py-16 px-10">
<!-- ==================== FLUID SVG "RIVERS" (The glowing tracks) ==================== -->
<!-- Placed perfectly behind the HTML nodes. Z-0 -->
<svg
class="absolute inset-0 w-full h-full pointer-events-none z-0 overflow-visible"
viewBox="0 0 1300 680"
preserveAspectRatio="none"
>
<!-- Defs for beautiful organic gradients -->
<defs>
<linearGradient
id="grad-convert"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
:stop-color="colors.brand"
/>
<stop
offset="100%"
:stop-color="colors.convertAccent"
/>
</linearGradient>
<linearGradient
id="grad-pass"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
<stop
offset="0%"
:stop-color="colors.brand"
/>
<stop
offset="100%"
:stop-color="colors.passAccent"
/>
</linearGradient>
<!-- Soft glow filter for the tracks themselves -->
<filter
id="glow-track"
x="-20%"
y="-20%"
width="140%"
height="140%"
>
<feGaussianBlur
stdDeviation="3"
result="blur"
/>
<feComposite
in="SourceGraphic"
in2="blur"
operator="over"
/>
</filter>
</defs>
<!-- The Base Tracks (Ghost Rivers) -->
<g
:stroke="colors.trackMain"
stroke-width="2"
fill="none"
stroke-linecap="round"
>
<!-- Ingress Tributaries: 3 Clients -> 1 Core -->
<!-- Starting X: 220 (clients), Ending X: 380 (core) -->
<path
id="flow-in-1"
d="M 220 180 C 300 180, 270 300, 380 300"
/>
<path
id="flow-in-2"
d="M 220 300 L 380 300"
/>
<path
id="flow-in-3"
d="M 220 420 C 300 420, 270 300, 380 300"
/>
<!-- Egress Delta: Core -> 2 Flow Engines -->
<!-- Starting X: 580 (core), Ending X: 750 (engines) -->
<!-- Beautiful fluid Bezier splits -->
<path
id="flow-out-top"
d="M 580 300 C 650 300, 650 200, 750 200"
/>
<path
id="flow-out-bot"
d="M 580 300 C 650 300, 650 400, 750 400"
/>
<!-- To the Cloud Delta: Processors -> Destinations -->
<!-- Starting X: 950 (engines), Ending X: 1100 (destinations) -->
<!-- From Purple Mapping Engine -->
<path
id="flow-cloud-1"
d="M 950 200 C 1030 200, 1030 140, 1100 140"
/>
<path
id="flow-cloud-2"
d="M 950 200 C 1020 200, 1020 200, 1100 200"
/>
<path
id="flow-cloud-3"
d="M 950 200 C 1030 200, 1030 260, 1100 260"
/>
<!-- From Blue Passthrough Engine -->
<path
id="flow-proxy-1"
d="M 950 400 C 1030 400, 1030 360, 1100 360"
/>
<path
id="flow-proxy-2"
d="M 950 400 C 1030 400, 1030 420, 1100 420"
/>
<path
id="flow-proxy-3"
d="M 950 400 C 1030 400, 1030 480, 1100 480"
/>
</g>
<!-- The Majestic Return Arch (Response Loop) -->
<g
:stroke="colors.returnAccent"
stroke-width="2"
fill="none"
opacity="0.25"
>
<path
id="flow-return"
d="M 1150 510 C 1150 630, 950 640, 600 640 C 250 640, 120 600, 120 480"
/>
</g>
<!-- ==================== GLOWING DATA PACKET ANIMATIONS ==================== -->
<!-- We use slightly larger, softer glowing circles to emphasize the "fluid" nature -->
<g style="filter: drop-shadow(0 0 8px currentColor)">
<!-- Ingress Flow -->
<circle
r="4"
:fill="colors.brand"
>
<animateMotion
dur="2.2s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-in-1" /></animateMotion>
</circle>
<circle
r="4"
:fill="colors.brand"
>
<animateMotion
dur="1.8s"
begin="0.5s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-in-2" /></animateMotion>
</circle>
<circle
r="4"
:fill="colors.brand"
>
<animateMotion
dur="2.4s"
begin="0.2s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-in-3" /></animateMotion>
</circle>
<!-- Delta Splits (Using gradients) -->
<circle
r="4.5"
:fill="colors.convertAccent"
>
<animateMotion
dur="2s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-out-top" /></animateMotion>
</circle>
<circle
r="4.5"
:fill="colors.passAccent"
>
<!-- Make this packet slightly pulsing -->
<animate
attributeName="r"
values="4;5;4"
dur="1s"
repeatCount="indefinite"
/>
<animateMotion
dur="2.1s"
begin="0.8s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-out-bot" /></animateMotion>
</circle>
<!-- Cloud Destiny -->
<circle
r="3.5"
:fill="colors.convertAccent"
>
<animateMotion
dur="1.5s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-cloud-1" /></animateMotion>
</circle>
<circle
r="3.5"
:fill="colors.convertAccent"
>
<animateMotion
dur="1.4s"
begin="0.4s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-cloud-3" /></animateMotion>
</circle>
<circle
r="3.5"
:fill="colors.passAccent"
>
<animateMotion
dur="1.6s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-proxy-1" /></animateMotion>
</circle>
<circle
r="3.5"
:fill="colors.passAccent"
>
<animateMotion
dur="1.5s"
begin="0.7s"
repeatCount="indefinite"
keyPoints="0;1"
keyTimes="0;1"
calcMode="spline"
keySplines="0.4 0 0.2 1"
><mpath href="#flow-proxy-3" /></animateMotion>
</circle>
</g>
<!-- Return Path Slow Comets -->
<circle
r="5"
:fill="colors.returnAccent"
style="filter: drop-shadow(0 0 10px currentColor)"
>
<animateMotion
dur="6s"
repeatCount="indefinite"
><mpath href="#flow-return" /></animateMotion>
</circle>
<circle
r="5"
:fill="colors.returnAccent"
style="filter: drop-shadow(0 0 10px currentColor)"
>
<animateMotion
dur="6s"
begin="3s"
repeatCount="indefinite"
><mpath href="#flow-return" /></animateMotion>
</circle>
</svg>
<!-- ========================================================================= -->
<!-- FREE FLOATING HTML NODES -->
<!-- Beautifully positioned to align exactly with the ends of the SVG rivers -->
<!-- ========================================================================= -->
<!-- 1. INGRESS CLIENTS -->
<div class="absolute left-10 top-[160px] flex flex-col gap-[80px] z-10 w-[140px]">
<div class="absolute -top-[45px] font-sans text-[11px] font-bold tracking-[0.2em] uppercase opacity-40 ml-4 flex gap-2 items-center">
<div
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: colors.textMuted }"
/>Sources
</div>
<!-- Nodes -->
<div
class="h-[40px] rounded-xl flex items-center px-4 gap-3 bg-white dark:bg-[#121212] transition-transform hover:-translate-y-1 cursor-pointer"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<div
class="w-2 h-2 rounded-full"
:style="{ backgroundColor: colors.brand }"
/>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Claude</span>
</div>
<div
class="h-[40px] rounded-xl flex items-center px-4 gap-3 bg-white dark:bg-[#121212] transition-transform hover:-translate-y-1 cursor-pointer"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<div
class="w-2 h-2 rounded-full"
:style="{ backgroundColor: colors.brand }"
/>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>OpenAI</span>
</div>
<div
class="h-[40px] rounded-xl flex items-center px-4 gap-3 bg-white dark:bg-[#121212] transition-transform hover:-translate-y-1 cursor-pointer"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<div
class="w-2 h-2 rounded-full"
:style="{ backgroundColor: colors.brand }"
/>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Gemini</span>
</div>
</div>
<!-- 2. THE AETHER NEXUS (Core Gateway) -->
<!-- Perfectly centered vertically. Y anchors to 300px -->
<div
class="absolute left-[380px] top-[140px] w-[200px] rounded-[24px] py-8 px-5 z-20 flex flex-col items-center transition-transform hover:scale-[1.02]"
:style="{ backgroundColor: colors.coreBg, border: `1px solid ${colors.coreBorder}`, boxShadow: `${shadows.node}, ${shadows.coreGlow}` }"
>
<div
class="absolute -top-3.5 px-3 py-0.5 rounded-full shadow-sm bg-white dark:bg-[#111] text-[10px] font-bold tracking-[0.2em] font-sans"
:style="{ color: colors.brand, border: `1px solid ${colors.cardBorder}` }"
>
AETHER
</div>
<h3 class="text-xl font-bold font-sans tracking-[0.1em] mb-8 text-transparent bg-clip-text bg-gradient-to-r from-neutral-800 to-neutral-400 dark:from-white dark:to-neutral-500">
GATEWAY
</h3>
<!-- Internal Micro-Pills -->
<div class="flex flex-col gap-2.5 w-full">
<div
class="h-[32px] rounded-lg border-b flex items-center justify-center font-sans text-[10px] font-medium opacity-70"
:style="{ borderColor: colors.cardBorder, color: colors.textMain }"
>
统一模型名 / 格式聚合
</div>
<div class="flex gap-2">
<div
class="flex-1 rounded-lg flex items-center justify-center py-[7px] font-sans font-bold text-[10px] bg-black/[0.04] dark:bg-white/[0.04]"
:style="{ color: colors.textMain }"
>
鉴定
</div>
<div
class="flex-1 rounded-lg flex items-center justify-center py-[7px] font-sans font-bold text-[10px] bg-black/[0.04] dark:bg-white/[0.04]"
:style="{ color: colors.textMain }"
>
并发
</div>
</div>
<div
class="h-[36px] rounded-lg border flex items-center justify-center font-sans text-[10px] font-bold relative mt-2"
:style="{ borderColor: colors.coreBorder, color: colors.textMain, backgroundColor: isDark ? 'rgba(212, 162, 127, 0.05)' : 'rgba(204, 120, 92, 0.03)' }"
>
智能分发引擎
<div
class="absolute -right-[5px] w-2 h-2 rounded-full bg-current"
:style="{ color: colors.brand }"
/>
</div>
</div>
</div>
<!-- 3. EGRESS DELTA ENGINES -->
<!-- Y perfectly aligned to the flow-out-top (y=200) and flow-out-bot (y=400) -->
<!-- Format Engine (Purple) -->
<div
class="absolute left-[750px] top-[165px] w-[200px] h-[70px] rounded-2xl flex flex-col justify-center px-6 z-10 transition-transform hover:-translate-y-1 cursor-pointer"
:style="{ backgroundColor: colors.cardBg, border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<div
class="absolute -left-1 w-2 h-6 rounded-full"
:style="{ backgroundColor: colors.convertAccent }"
/>
<span
class="font-sans text-[14px] font-bold tracking-wide flex items-center gap-2 mb-1"
:style="{ color: colors.textMain }"
>格式转换流 <div
class="w-1.5 h-1.5 rounded-full animate-pulse"
:style="{ backgroundColor: colors.convertAccent }"
/></span>
<span
class="font-sans text-[9px] opacity-50"
:style="{ color: colors.textMain }"
>双向翻译协议与模型还原</span>
</div>
<!-- Passthrough Engine (Blue) -->
<div
class="absolute left-[750px] top-[365px] w-[200px] h-[70px] rounded-2xl flex flex-col justify-center px-6 z-10 transition-transform hover:-translate-y-1 cursor-pointer border-dashed"
:style="{ backgroundColor: colors.cardBg, borderColor: colors.cardBorder, borderWidth: '2px', boxShadow: shadows.node }"
>
<div
class="absolute -left-1.5 w-2.5 h-6 rounded-full border-2 bg-transparent"
:style="{ borderColor: colors.passAccent }"
/>
<span
class="font-sans text-[14px] font-bold tracking-wide flex items-center gap-2 mb-1"
:style="{ color: colors.passAccent }"
>原生直通管道</span>
<span
class="font-sans text-[9px] opacity-60"
:style="{ color: colors.textMain }"
>同源生态双向超低延迟透传</span>
</div>
<!-- 4. UPSTREAM CLOUDS & PROXIES -->
<!-- Standard Cloud Targets (Top Flow: Y=140, 200, 260) -->
<div class="absolute left-[1100px] top-[120px] flex flex-col gap-[20px] w-[140px] z-10">
<div
class="absolute -top-[30px] font-sans text-[11px] font-bold tracking-[0.2em] uppercase"
:style="{ color: colors.convertAccent }"
>
Providers
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-purple-500/30"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Claude</span>
<div
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: colors.textMuted }"
/>
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-purple-500/30"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>OpenAI</span>
<div
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: colors.textMuted }"
/>
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-purple-500/30"
:style="{ border: `1px solid ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Gemini</span>
<div
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: colors.textMuted }"
/>
</div>
</div>
<!-- Proxy Targets (Bottom Flow: Y=360, 420, 480) -->
<div class="absolute left-[1100px] top-[340px] flex flex-col gap-[20px] w-[140px] z-10">
<div
class="absolute -top-[30px] font-sans text-[11px] font-bold tracking-[0.2em] uppercase"
:style="{ color: colors.passAccent }"
>
Proxies
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-blue-500/30"
:style="{ border: `1px dashed ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Codex</span>
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-blue-500/30"
:style="{ border: `1px dashed ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Kiro</span>
</div>
<div
class="h-[40px] rounded-xl flex items-center justify-between px-4 bg-white dark:bg-[#121212] transition-all hover:ring-2 hover:ring-blue-500/30"
:style="{ border: `1px dashed ${colors.cardBorder}`, boxShadow: shadows.node }"
>
<span
class="font-sans text-[12px] font-semibold tracking-wide"
:style="{ color: colors.textMain }"
>Antigrav</span>
</div>
</div>
<!-- 5. THE ORGANIC RETURN LOOP OVERLAYS -->
<!-- Floating labels sitting precisely on the huge bottom elliptic sweep -->
<!-- Left anchor label -->
<div
class="absolute left-[120px] bottom-[20px] bg-white/80 dark:bg-black/60 backdrop-blur-md px-4 py-2 rounded-2xl border"
:style="{ borderColor: colors.cardBorder, color: colors.returnAccent, boxShadow: shadows.node }"
>
<div class="font-sans text-[10px] font-bold tracking-wide flex items-center gap-2">
<svg
class="w-3 h-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
><polyline points="15 18 9 12 15 6" /></svg>返回兼容格式响应
</div>
</div>
<!-- Center massive anchor -->
<div
class="absolute left-[540px] bottom-[15px] bg-white dark:bg-[#121212] px-6 py-2.5 rounded-full border-2 flex items-center gap-4 z-20 cursor-default"
:style="{ borderColor: colors.returnAccent, boxShadow: shadows.node }"
>
<div
class="w-2.5 h-2.5 rounded-full animate-ping absolute"
:style="{ backgroundColor: colors.returnAccent, opacity: 0.4 }"
/>
<div
class="w-2.5 h-2.5 rounded-full relative"
:style="{ backgroundColor: colors.returnAccent }"
/>
<div class="flex flex-col">
<span
class="font-sans text-[13px] font-bold tracking-widest uppercase"
:style="{ color: colors.returnAccent }"
>Response Loop</span>
<span
class="font-sans text-[10px] font-semibold opacity-60 mt-0.5"
:style="{ color: colors.textMain }"
>响应逆向格式转换 / 上游模型实体还原</span>
</div>
</div>
<!-- Right anchor label -->
<div
class="absolute right-[120px] bottom-[90px] bg-white/80 dark:bg-black/60 backdrop-blur-md px-4 py-2 rounded-2xl border"
:style="{ borderColor: colors.cardBorder, color: colors.returnAccent, boxShadow: shadows.node }"
>
<div class="font-sans text-[10px] font-bold tracking-wide flex items-center gap-2">
拉取上游原始流端<svg
class="w-3 h-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
><polyline points="9 18 15 12 9 6" /></svg>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.font-sans {
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Custom horizontal scrollbar for tight spaces */
.custom-scrollbar::-webkit-scrollbar {
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(150, 150, 150, 0.2);
border-radius: 20px;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(150, 150, 150, 0.4);
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<div class="markdown-viewer-container">
<div
class="markdown-body"
v-html="renderedHtml"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { marked, type Renderer } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'
const props = defineProps<{
content: string
}>()
const renderedHtml = ref('')
// marked v16+ 需要通过自定义 renderer 实现代码高亮
const renderer: Partial<Renderer> = {
code({ text, lang }) {
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'
const highlighted = hljs.highlight(text, { language }).value
return `<pre><code class="hljs language-${language}">${highlighted}</code></pre>`
}
}
marked.use({ renderer, gfm: true, breaks: true })
const renderMarkdown = () => {
if (!props.content) {
renderedHtml.value = ''
return
}
try {
const rawHtml = marked.parse(props.content) as string
renderedHtml.value = DOMPurify.sanitize(rawHtml)
} catch {
renderedHtml.value = '<p class="text-red-500">Failed to render content</p>'
}
}
watch(() => props.content, () => {
renderMarkdown()
})
onMounted(() => {
renderMarkdown()
})
</script>
<style>
/* Literary Tech Markdown Styles */
.markdown-viewer-container {
@apply w-full max-w-none;
}
.markdown-body {
@apply text-[var(--color-text)] font-serif leading-relaxed;
}
.markdown-body h1 {
@apply text-4xl mb-8 mt-12 font-medium tracking-tight text-[var(--color-text)];
font-family: var(--serif);
}
.markdown-body h2 {
@apply text-2xl mb-6 mt-12 font-medium tracking-tight text-[var(--color-text)] border-b pb-2;
border-color: var(--color-border-soft);
font-family: var(--serif);
}
.markdown-body h3 {
@apply text-xl mb-4 mt-8 font-medium tracking-tight text-[var(--color-text)];
font-family: var(--serif);
}
.markdown-body p {
@apply mb-4 text-[1.05rem] opacity-90;
font-family: var(--serif);
}
.markdown-body ul,
.markdown-body ol {
@apply pl-6 mb-6 opacity-90 space-y-2 text-[1.05rem];
font-family: var(--serif);
}
.markdown-body ul {
list-style-type: disc;
}
.markdown-body ol {
list-style-type: decimal;
}
.markdown-body blockquote {
@apply border-l-4 pl-4 italic opacity-80 mb-6;
border-color: var(--book-cloth);
background: var(--color-background-soft);
@apply py-2 rounded-r-lg;
}
/* Code Blocks & Inline Code */
.markdown-body pre {
@apply p-4 rounded-xl overflow-x-auto mb-6 text-sm backdrop-blur-md;
background: var(--color-background-soft);
border: 1px solid var(--color-border-soft);
font-family: var(--monospace) !important;
box-shadow: var(--shadow-sm);
}
.markdown-body code {
font-family: var(--monospace) !important;
}
.markdown-body :not(pre) > code {
@apply px-1.5 py-0.5 rounded text-sm;
background: var(--color-background-soft);
color: var(--book-cloth);
font-family: var(--monospace) !important;
border: 1px solid var(--color-border-soft);
}
/* Tables */
.markdown-body table {
@apply w-full mb-6 border-collapse text-left;
font-family: var(--sans-serif);
}
.markdown-body th {
@apply px-4 py-3 font-medium bg-[var(--color-background-soft)] border;
border-color: var(--color-border-soft);
}
.markdown-body td {
@apply px-4 py-3 border opacity-90;
border-color: var(--color-border-soft);
}
.markdown-body tr:nth-child(even) {
@apply bg-[var(--color-background)]/50;
}
/* Links */
.markdown-body a {
@apply text-[var(--book-cloth)] underline decoration-dashed underline-offset-4 transition-all;
}
.markdown-body a:hover {
@apply decoration-solid opacity-80;
}
/* Images */
.markdown-body img {
@apply max-w-full rounded-xl border object-contain mx-auto mb-6 shadow-sm;
border-color: var(--color-border-soft);
max-height: 600px;
}
.markdown-body p:has(img) {
@apply text-center;
}
</style>

View File

@@ -0,0 +1,33 @@
# 高级功能
## 1. 格式转换
1. **全局格式转换**
2. **提供商级转换**
3. **端点级转换**
## 2. 请求上游固定非流/流式
您可以根据提供商的特性,强制将请求转换为流式或非流式输出。
## 3. 请求头/体编辑
支持在请求发往上游前,动态对 Headers 或 Body 结构进行剔除、修改、正则替换等操作。
## 4. 模型映射
将 Aether 统一模型名称映射至上游真实的非标模型名称。
## 5. 正则映射
1. **模型权限**:通过正则快速匹配一类模型权限
2. **自动获取上游模型**:针对不支持标准模型获取接口的上游,通过抓取/正则自定义获取模型列表
## 6. 能力标签
为模型或 Key 赋予 `vision``function_call``1m_context` 等细分能力标签。
## 7. 余额监控
自动抓取配置了余额查询接口的提供商账户余额。
## 8. 配置导入/出
支持将完整的配置环境(除去核心凭证)导出并在不同实例中导入。
## 9. 锁定用户密钥
发现滥用时,可一键锁定用户请求密钥。

View File

@@ -0,0 +1,35 @@
# 架构说明
Aether的系统架构、请求处理流程和数据流向。
## 1. 系统概览
```mermaid
graph LR
Client[客户端<br>SDK / CLI / Web]
Aether[Aether<br>认证 / 路由 / 编排]
PostgreSQL[(PostgreSQL)]
Redis[(Redis)]
Upstream[上游供应商<br>Claude / OpenAI / Gemini]
Client -->|API Request| Aether
Aether -.->|Auth / Config| PostgreSQL
Aether <.-.>|Cache / Quota / Lock| Redis
Aether -->|Proxy Request| Upstream
```
## 2. 核心原则
1. **API格式、端点、认证方式说明**
提供商支持不同的格式(如 OpenAI Chat, Claude ChatAether在接收请求后将统一管理路由。
2. **统一的入口模型名称**
在内部完成多提供商、多模型名称风格的聚合映射管理。客户端只需知道统一的模型名(例如 `claude-3-opus`Aether将根据映射规则自动寻找真正对应的上游模型名称。
3. **请求流转:格式转换**
`多API格式兼容入口``格式转换``上游提供商``格式转换``多API格式兼容出口`
Aether 会尝试分析客户端的请求体格式,再将其转换为对应上游供应商要求的真正格式。
4. **请求流转:透传模式**
`多API格式入口``同格式请求透传``上游提供商``同格式响应透传``多API格式出口`
如果客户端请求格式与上游目标格式一致Aether 会直接透传Pass-through不进行数据解包和重整最大化性能。

View File

@@ -0,0 +1,120 @@
# 相关概念
## 1. 创建统一模型
1. **左侧模型选择、搜索区域**
- 模型来源: opencode维护的 [models.dev](https://models.dev)
- 仅显示几个官方模型提供商, 可以通过搜索获取更多模型
2. **右侧参数配置区域**
- 手动添加模型 或 选择模型后自动填写
- 模型偏好: 标定这个模型是否支持这些能力, 一般情况下可不选
- 价格配置
- 计费方式: Token价格 + 按次价格 + 视频计费 = 最终计费
- 价格阶梯: 以总Token数落在哪个区间为准, 并非按阶梯溢出补全式计费
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%209.png)
## 2. 添加提供商
1. **提供商类型**: 自定义或反代; 一般自定义即可, 反代请进入反代章节
2. **计费类型**:
- 按量付费: 持续使用
- 月卡额度: 按周期(天)限额
- 免费套餐: 不计入成本即倍率为0
3. **最大重试次数**
- 在缓存亲和调度模式下, 首次请求失败后的重试次数。
4. **超时时间**
- 流式首字超时时间: 流式请求收到首字前的超时时间
- 非流请求超时时间: 非流请求的总超时时间
5. **保持优先级**
- 通过格式转换的请求, 是否保持当前优先级。
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2010.png)
## 3. 添加端点
添加端点是添加上游支持的端点, 并非你需要使用的端点. 比如Anyrouter只支持Claude Code接入, 那么就应该仅添加Claude CLI端点。
1. **选择格式**: 选择上游支持的端点
2. **自定义Base URL、Path**: 根据上游提供的信息填写。为空则使用提示的默认值。
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2011.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2012.png)
## 4. 添加密钥
1. **认证类型**
- API Key: 传统密钥
- Vertex AI: 用于Google Cloud
2. **支持的API格式**: 勾选Key可以使用的API格式, 同时可以设置key访问该端点时的倍率
3. **优先级**: 在提供商优先的调度模式下, 不同优先级的Key按顺序故障转移, 同优先级的Key进行负载均衡
4. **RPM限制**:
Key的使用速率限制, 会预留10%给已有用户进行提高缓存命中率
- 填写固定数值
- 不填为空, 采用一定策略自适应学习。
5. **缓存TTL**
- 0: 不使用缓存优先
- N: 同一个用户Key在N分钟内, 优先使用之前使用的提供商、Key响应请求。
6. **熔断探测**
当同一个提供商Key自动连续若干次请求失败后, 会进入熔断状态, 之后每间N分钟进行探测请求, 若请求成功解除熔断后续正常请求, 否则按以指数级增长探测时间以待下次探测, 最大探测间隔不会增长超过32分钟
7. **能力标签**
定义该Key可以使用的能力
8. **自动获取上游模型**
在上游获取模型端点支持的情况下, 从接口自动获取可以用模型列表。且按一定时间自动刷新, 不开启则默认任意模型可用, 或在后续模型权限中手动添加。
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2013.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2014.png)
## 5. 模型权限
1. 在编辑密钥中开启自动获取/刷新上游全部模型
2. 手动限制/创建该Key的可用模型
3. 不开启自动获取且勾选任意模型, 即模型权限为空, 则认为可以使用提供商的全部关联模型
4. 只有在模型权限不为空的情况下, 才可以配合正则映射模型
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2015.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2016.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2017.png)
## 6. 关联模型
从全局模型关联提供商模型, 即设置该提供商可以访问的模型, 同时可以在这里设置提供商自己的模型价格。默认继承全局模型的价格
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2018.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2019.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2020.png)
## 7. 模型映射
如果该提供的请求名称并非标准名称, 即可通过映射改变在实际请求提供时的模型id, 比如:
官方标准名称为: `claude-opus-4-6` 实际提供商叫 `claude-opus-4-6-last`
又或者可以降/升级请求模型:
官方标准名称为: `claude-opus-4-6` 实际请求提供商用 `claude-sonnet-4-6`
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2021.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2022.png)
## 8. 反向代理
1. **Codex**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2023.png)
2. **Krio**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2024.png)
3. **Antigravity**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2025.png)
## 9. 优先级管理
允许拖动、或者直接点击数字输入调整优先级
1. **提供商优先**
按提供商顺序调度, 同优先级负载均衡
2. **Key优先**
全局Key统一调度, 同优先级负载均衡
3. **缓存亲和模式**
在KeyTTL时间的约束下, 优先使用上一次请求使用的Key
4. **负载均衡模式**
- 取消全局提供商优先级, 提供商内部Key依然保持优先级
- 取消全局Key优先级, 乱序使用
5. **固定顺序模式**
取消缓存亲和, 始终使用固定顺序Key请求
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2026.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2027.png)

View File

@@ -0,0 +1,10 @@
# 常见问题
## 1. 为什么发生错误没有进行故障转移?
故障转移只在满足安全的重试条件下发生。主要检查:
1. 错误是否属于可重试类型(如 429, 5xx 或网络超时400/401/404 等请求本身的错误一般不重试。
2. 是否已经达到提供商或全局配置中的「最大重试次数」。
3. 在某些特定的流式响应中,如果内容已经开始返回中途断开,可能为了防止数据混乱而不进行转移。
## 2. 其它问题
请提交 Issue 寻求帮助或参与社区讨论。

View File

@@ -0,0 +1,6 @@
# 模块管理
1. **访问令牌**: 允许通过令牌授权使用全部后端接口
2. **邮件配置**: 注册、通知服务, 可自定义邮件模版
3. **OAuth登录**: 支持 Linux Do OAuth 授权登录
4. **LDAP认证**: 提供企业级统合身份与权限管理

View File

@@ -0,0 +1,131 @@
# 快速开始
## 部署
选择适合你的部署方式开始
### 1. 预构建镜像 (Docker Compose)
```markdown
# 1. 克隆代码
git clone https://github.com/fawney19/Aether.git
cd Aether
# 2. 配置环境变量
cp .env.example .env
python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env
# 3. 部署 / 更新(自动执行数据库迁移)
docker compose pull && docker compose up -d
# 4. 升级前备份
docker compose exec postgres pg_dump -U postgres aether | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
```
### 2. 本地代码构建镜像 (Docker Compose)
```markdown
# 1. 克隆代码
git clone https://github.com/fawney19/Aether.git
cd Aether
# 2. 配置环境变量
cp .env.example .env
python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env
# 3. 构建(自动构建、启动、迁移)
./deploy.sh
# 4. 更新需要拉取最新代码
git pull origin master
```
### 3. 本地开发
依赖 Docker、uv、nodejs
```markdown
# 启动数据库
docker compose -f docker-compose.build.yml up -d postgres redis
# 后端
uv sync
./dev.sh
# 前端
cd frontend && npm install && npm run dev
```
## 配置流程
1. **创建统一模型**
以Opus4.6为例, 其他模型同样添加即可, 非必要建议只添加官方支持的模型ID
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image.png)
2. **添加提供商**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%201.png)
3. **添加端点**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%202.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%203.png)
4. **添加密钥**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%204.png)
5. **关联全局模型**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%205.png)
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%206.png)
6. **模型映射**
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%207.png)
## 反向代理
添加提供商时, 提供商类型选择对应类型即可, 反向代理默认开启提供商级格式转换。
1. **Codex**
- OAuth授权登录
- 导入RefreshToken, 支持批量导入
2. **Kiro**
- Build ID
- Identity Center
- Start URL
- Region
- 导入 RefreshToken, 支持批量导入
- Social 格式要求
```json
{
"refresh_token": ""
}
```
- IDC 格式要求
```json
{
"refresh_token": "",
"client_id": "",
"client_secret": "",
"machine_id": ""
}
```
3. **Antigravity**
- OAuth授权登录
- 导入RefreshToken, 支持批量导入
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%208.png)
## 异步任务
需要有提供商端点支持
1. Veo
2. Sora
## 代理配置
1. **Aether-Proxy**
Rust实现, 超小资源占有, 适合性能低的vps直接使用。
[https://github.com/fawney19/Aether/tree/master/aether-proxy](https://github.com/fawney19/Aether/tree/master/aether-proxy)
2. **代理节点**
在模块管理中, 开启代理模块后可以添加和使用代理功能, 包括手动添加和Aether-Proxy自动连接。
3. **多级代理**
优先级: Key代理 > 提供商代理 > 全局代理
- 全局代理 - 系统配置
- 提供商代理 - 提供商配置
- Key代理 - Key配置

View File

@@ -0,0 +1,31 @@
# 关键策略
## 1. 请求体记录
在系统设置中, 可以修改请求体记录详情等级
| 日志等级 | 记录内容 |
| --- | --- |
| **Base** | 基本请求信息 |
| **Headers** | Base + 请求头 |
| **Full** | Headers + 请求体 |
![image.png](/Aether%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/image%2028.png)
## 2. 调度模式
1. 提供商优先
2. Key优先
3. 缓存亲和
4. 负载均衡
5. 固定顺序
6. 故障转移
## 3. 访问限制
*全局或 Key 级别的访问频率及配额限制*
## 4. 请求体压缩清理
*去除多余空格与不可见字符以节省 Token 操作*
## 5. 定时任务
*自动刷新模型列表及定时检查配额状态*

View File

@@ -0,0 +1,292 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ArrowLeft } from 'lucide-vue-next'
const activeDemo = ref<number>(1)
const demos = [
{ id: 1, name: '垂直管道流 (Vertical Pipeline)' },
{ id: 2, name: '中心辐射 (Central Star Hub)' },
{ id: 3, name: '2.5D 层级结构 (Isometric Layered)' },
]
</script>
<template>
<div class="fixed inset-0 z-50 bg-[#faf9f5] dark:bg-[#141311] overflow-y-auto">
<div class="sticky top-0 z-10 flex items-center justify-between px-6 py-4 bg-white/80 dark:bg-[#1a1815]/80 backdrop-blur border-b border-[#e5e4df] dark:border-white/10">
<div class="flex items-center gap-4">
<router-link
to="/guide/architecture"
class="flex items-center gap-2 text-sm font-medium text-[#666663] hover:text-[#cc785c] dark:text-[#a3a094] transition-colors"
>
<ArrowLeft class="w-4 h-4" /> 返回文档
</router-link>
<h1 class="text-xl font-bold text-[#262624] dark:text-[#f1ead8] m-0 border-l border-[#e5e4df] dark:border-white/10 pl-4">
架构布局草案演示
</h1>
</div>
<div class="flex gap-2 bg-[#f5f5f0] dark:bg-[#1e1c19] p-1 rounded-xl border border-[#e5e4df] dark:border-white/5">
<button
v-for="demo in demos"
:key="demo.id"
class="px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200"
:class="[
activeDemo === demo.id
? 'bg-white dark:bg-[#2a2825] text-[#cc785c] shadow-sm'
: 'text-[#666663] dark:text-[#a3a094] hover:bg-black/5 dark:hover:bg-white/5'
]"
@click="activeDemo = demo.id"
>
{{ demo.name }}
</button>
</div>
</div>
<!-- Canvas Area -->
<div class="p-8 w-full max-w-7xl mx-auto flex justify-center min-h-[800px]">
<!-- Variant 1: Vertical -->
<div
v-if="activeDemo === 1"
class="w-full h-[800px] border border-dashed border-[#cc785c]/30 rounded-3xl relative flex flex-col items-center justify-between p-12"
>
<div class="absolute top-4 left-6 text-sm font-mono text-[#cc785c]/50">
Layout Variant: Vertical Pipeline
</div>
<!-- Ingress row -->
<div class="flex gap-12 w-full justify-center">
<div class="w-48 h-16 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded-xl flex items-center justify-center font-mono font-bold">
Claude API Request
</div>
<div class="w-48 h-16 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded-xl flex items-center justify-center font-mono font-bold">
OpenAI API Request
</div>
<div class="w-48 h-16 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded-xl flex items-center justify-center font-mono font-bold">
Gemini API Request
</div>
</div>
<!-- Down Arrows -->
<div class="flex gap-12 w-full justify-center my-4 opacity-50">
<div class="w-1 h-12 bg-[#cc785c] mx-auto relative">
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 border-l-4 border-r-4 border-t-8 border-transparent border-t-[#cc785c]" />
</div>
<div class="w-1 h-12 bg-[#cc785c] mx-auto relative">
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 border-l-4 border-r-4 border-t-8 border-transparent border-t-[#cc785c]" />
</div>
<div class="w-1 h-12 bg-[#cc785c] mx-auto relative">
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 border-l-4 border-r-4 border-t-8 border-transparent border-t-[#cc785c]" />
</div>
</div>
<!-- Aether Core -->
<div class="w-full max-w-3xl h-48 bg-[#cc785c]/10 border-2 border-[#cc785c] rounded-2xl flex flex-col items-center justify-center">
<h2 class="text-3xl font-black tracking-widest text-[#cc785c] mb-2">
AETHER GATEWAY
</h2>
<div class="text-sm font-medium w-full px-12 text-center text-[#262624] dark:text-[#f1ead8]">
<span class="px-3">多源聚合</span>|<span class="px-3">鉴权 / 配额管控</span>|<span class="px-3">负载均衡 / 故障转移</span>|<span class="px-3">基于亲和性路由</span>
</div>
</div>
<!-- Split flow down -->
<div class="w-full max-w-2xl h-16 relative flex justify-between px-24 my-4 opacity-50">
<!-- Left line to convert -->
<div class="absolute top-0 left-[25%] w-[1px] h-full bg-[#a855f7]" />
<!-- Right line to passthrough -->
<div class="absolute top-0 right-[25%] w-[1px] h-full bg-[#3b82f6]" />
</div>
<!-- Egress Processing -->
<div class="flex gap-24 w-full justify-center">
<div class="w-72 h-32 bg-[#a855f7]/10 border-2 border-[#a855f7] rounded-xl flex flex-col items-center justify-center text-[#a855f7]">
<h3 class="font-bold text-lg mb-2">
3. 格式转换引擎
</h3>
<span class="text-xs font-mono">Protocols Translate</span>
</div>
<div class="w-72 h-32 bg-[#3b82f6]/10 border-2 border-[#3b82f6] rounded-xl flex flex-col items-center justify-center text-[#3b82f6]">
<h3 class="font-bold text-lg mb-2">
4. 原生双向透传
</h3>
<span class="text-xs font-mono">Direct Passthrough</span>
</div>
</div>
<!-- Final hop down -->
<div class="w-1 h-12 bg-[#e5e4df] dark:bg-white/20 mx-auto my-4" />
<!-- Upstream -->
<div class="w-full max-w-4xl h-24 border-2 border-dashed border-[#e5e4df] dark:border-white/20 rounded-xl flex items-center justify-around">
<div class="font-bold text-[#666663] dark:text-[#a3a094]">
Upstream:
</div>
<div class="px-6 py-2 bg-emerald-500/10 text-emerald-600 rounded-lg font-bold">
Claude
</div>
<div class="px-6 py-2 bg-emerald-500/10 text-emerald-600 rounded-lg font-bold">
OpenAI
</div>
<div class="px-6 py-2 bg-emerald-500/10 text-emerald-600 rounded-lg font-bold">
Gemini
</div>
<div class="px-6 py-2 bg-amber-500/10 text-amber-600 rounded-lg font-bold">
Custom Reverse Proxies (Codex, Kiro...)
</div>
</div>
</div>
<!-- Variant 2: Central Hub -->
<div
v-if="activeDemo === 2"
class="w-full h-[800px] border border-dashed border-[#cc785c]/30 rounded-3xl relative flex items-center justify-center p-12"
>
<div class="absolute top-4 left-6 text-sm font-mono text-[#cc785c]/50">
Layout Variant: Central Star/Hub
</div>
<!-- Aether Core / Center -->
<div class="w-72 h-72 rounded-full border-4 border-[#cc785c] bg-[#cc785c]/5 flex flex-col items-center justify-center z-10 relative">
<h2 class="text-xl font-black text-[#cc785c] text-center px-4 leading-tight">
AETHER<br>GATEWAY
</h2>
<div class="mt-4 text-xs font-mono text-center leading-relaxed">
Auth<br>Quota<br>Load Balance
</div>
</div>
<!-- Ingress Left Arc -->
<div class="absolute left-24 top-1/2 -translate-y-1/2 flex flex-col gap-6">
<div class="w-40 h-10 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded pl-4 flex items-center relative">
<span class="font-mono text-sm font-bold">Claude API</span>
<div class="absolute right-[-40px] top-1/2 h-[1px] w-10 bg-[#cc785c]" />
</div>
<div class="w-40 h-10 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded pl-4 flex items-center relative">
<span class="font-mono text-sm font-bold">OpenAI API</span>
<div class="absolute right-[-40px] top-1/2 h-[1px] w-10 bg-[#cc785c]" />
</div>
<div class="w-40 h-10 bg-white dark:bg-[#1e1c19] border border-[#e5e4df] dark:border-white/10 rounded pl-4 flex items-center relative">
<span class="font-mono text-sm font-bold">Gemini API</span>
<div class="absolute right-[-40px] top-1/2 h-[1px] w-10 bg-[#cc785c]" />
</div>
</div>
<!-- Engines Top/Bottom -->
<div class="absolute top-24 left-1/2 -translate-x-1/2 flex flex-col items-center">
<div class="w-48 h-16 bg-[#a855f7]/10 border-2 border-[#a855f7] rounded-xl flex items-center justify-center text-sm font-bold text-[#a855f7]">
格式转换引擎
</div>
<div class="w-[1px] h-12 bg-[#a855f7]" />
</div>
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex flex-col items-center">
<div class="w-[1px] h-12 bg-[#3b82f6]" />
<div class="w-48 h-16 bg-[#3b82f6]/10 border-2 border-[#3b82f6] rounded-xl flex items-center justify-center text-sm font-bold text-[#3b82f6]">
原生直通管道
</div>
</div>
<!-- Egress Right Arc -->
<div class="absolute right-24 top-1/2 -translate-y-1/2 flex flex-col gap-4">
<div class="w-48 h-12 border border-dashed border-emerald-500 rounded flex items-center justify-center relative bg-emerald-500/5 text-emerald-600 font-bold">
<div class="absolute left-[-60px] top-1/2 h-[1px] w-14 bg-emerald-500/50" />
Standard Providers
</div>
<div class="w-48 h-12 border border-dashed border-amber-500 rounded flex items-center justify-center relative bg-amber-500/5 text-amber-600 font-bold">
<div class="absolute left-[-60px] top-1/2 h-[1px] w-14 bg-amber-500/50" />
Reverse Proxies
</div>
</div>
</div>
<!-- Variant 3: 2.5D Stacked -->
<div
v-if="activeDemo === 3"
class="w-full h-[800px] border border-dashed border-[#cc785c]/30 rounded-3xl relative pt-32 pb-12 flex justify-center perspective-[1000px]"
>
<div class="absolute top-4 left-6 text-sm font-mono text-[#cc785c]/50">
Layout Variant: 2.5D Stacked Layers (AWS Style)
</div>
<div
class="w-[600px] h-[600px] relative transition-transform duration-500 preserve-3d"
style="transform: rotateX(55deg) rotateZ(-45deg);"
>
<!-- Layer 1: Ingress (Top) -->
<div class="absolute inset-x-0 top-0 h-40 bg-white/40 dark:bg-black/40 backdrop-blur-md border border-white/50 dark:border-white/10 rounded-tr-3xl rounded-bl-3xl shadow-2xl flex flex-col justify-center px-12 transform translate-z-[120px]">
<h3 class="text-sm font-bold text-[#cc785c] absolute top-4 left-4 tracking-widest">
LAYER 1: INGRESS CLIENTS
</h3>
<div class="flex gap-4 w-full">
<div class="flex-1 h-12 bg-white dark:bg-[#2a2a2a] rounded flex items-center justify-center font-mono font-bold shadow-sm">
Claude
</div>
<div class="flex-1 h-12 bg-white dark:bg-[#2a2a2a] rounded flex items-center justify-center font-mono font-bold shadow-sm">
OpenAI
</div>
<div class="flex-1 h-12 bg-white dark:bg-[#2a2a2a] rounded flex items-center justify-center font-mono font-bold shadow-sm">
Gemini
</div>
</div>
<!-- Data Flow drops to L2 -->
<div class="absolute bottom-[-60px] left-1/2 w-2 h-16 bg-[#cc785c] opacity-50 blur-sm rounded-full transform -rotate-x-90 translate-y-8" />
</div>
<!-- Layer 2: Aether Gateway Core (Middle) -->
<div class="absolute inset-x-0 top-[200px] h-48 bg-[#cc785c]/20 backdrop-blur-xl border-2 border-[#cc785c] rounded-lg shadow-2xl flex items-center justify-center relative transform translate-z-[60px]">
<h3 class="text-sm font-bold text-[#cc785c] absolute top-4 left-4 tracking-widest">
LAYER 2: AETHER GATEWAY (CORE)
</h3>
<div class="text-center">
<h2 class="text-3xl font-black text-[#cc785c] tracking-[0.3em] mb-4">
AETHER
</h2>
<div class="flex gap-2">
<span class="px-2 py-1 text-[10px] bg-white/50 dark:bg-black/50 rounded ring-1 ring-[#cc785c]">Auth</span>
<span class="px-2 py-1 text-[10px] bg-white/50 dark:bg-black/50 rounded ring-1 ring-[#cc785c]">Rate Limit</span>
<span class="px-2 py-1 text-[10px] bg-white/50 dark:bg-black/50 rounded ring-1 ring-[#cc785c]">Load Balance</span>
</div>
</div>
</div>
<!-- Layer 3: Egress & Providers (Bottom) -->
<div class="absolute inset-x-0 bottom-[-50px] h-60 bg-[#f5f5f0]/50 dark:bg-[#1a1815]/50 backdrop-blur border border-[#e5e4df] dark:border-white/10 rounded-br-3xl rounded-tl-3xl shadow-xl flex flex-col p-8 transform translate-z-[0px]">
<h3 class="text-sm font-bold text-[#666663] dark:text-[#a3a094] mb-4">
LAYER 3: EGRESS & UPSTREAM
</h3>
<div class="flex gap-8 mb-6">
<div class="flex-1 h-16 bg-[#a855f7]/20 border border-[#a855f7] rounded flex items-center justify-center text-[#a855f7] font-bold">
Format Convert
</div>
<div class="flex-1 h-16 bg-[#3b82f6]/20 border border-[#3b82f6] rounded flex items-center justify-center text-[#3b82f6] font-bold">
Direct Passthrough
</div>
</div>
<div class="w-full flex gap-4 h-16">
<div class="w-full border border-dashed border-emerald-500 rounded bg-emerald-500/10 flex items-center justify-center text-emerald-600 font-bold">
Providers (Claude, OpenAI)
</div>
<div class="w-full border border-dashed border-amber-500 rounded bg-amber-500/10 flex items-center justify-center text-amber-600 font-bold">
Reverse Proxies
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.perspective-\[1000px\] {
perspective: 1000px;
}
.preserve-3d {
transform-style: preserve-3d;
}
.translate-z-\[120px\] { transform: translateZ(120px); }
.translate-z-\[60px\] { transform: translateZ(60px); }
.translate-z-\[0px\] { transform: translateZ(0px); }
.-rotate-x-90 { transform: rotateX(-90deg); }
</style>

View File

@@ -5,6 +5,7 @@ import {
BookOpen,
Target,
Settings,
Blocks,
HelpCircle
} from 'lucide-vue-next'
@@ -15,6 +16,7 @@ export interface GuideNavItem {
path: string
icon: Component
description?: string
subItems?: { name: string; hash: string }[]
}
export const guideNavItems: GuideNavItem[] = [
@@ -23,309 +25,113 @@ export const guideNavItems: GuideNavItem[] = [
name: '快速开始',
path: '/guide',
icon: Rocket,
description: '部署后的配置指南'
description: '部署后的配置指南',
subItems: [
{ name: '部署', hash: '#production' },
{ name: '配置流程', hash: '#config-steps' },
{ name: '反向代理', hash: '#reverse-proxy' },
{ name: '异步任务', hash: '#async-tasks' },
{ name: '代理配置', hash: '#proxy-config' }
]
},
{
id: 'architecture',
name: '架构说明',
path: '/guide/architecture',
icon: Network,
description: '系统架构与请求流程'
description: '系统架构'
},
{
id: 'concepts',
name: '相关概念',
path: '/guide/concepts',
icon: BookOpen,
description: '核心概念深入解释'
description: '核心概念',
subItems: [
{ name: '创建统一模型', hash: '#create-model' },
{ name: '添加提供商', hash: '#add-provider' },
{ name: '添加端点', hash: '#add-endpoint' },
{ name: '添加密钥', hash: '#add-key' },
{ name: '模型权限', hash: '#model-permission' },
{ name: '关联模型', hash: '#link-model' },
{ name: '模型映射', hash: '#model-mapping' },
{ name: '反向代理', hash: '#reverse-proxy' },
{ name: '优先级管理', hash: '#priority-management' }
]
},
{
id: 'strategy',
name: '关键策略',
path: '/guide/strategy',
icon: Target,
description: '调度、缓存与故障转移'
description: '关键策略',
subItems: [
{ name: '请求体记录', hash: '#request-logging' },
{ name: '调度模式', hash: '#scheduling' },
{ name: '访问限制', hash: '#rate-limit' },
{ name: '请求体清理', hash: '#payload-cleanup' },
{ name: '定时任务', hash: '#cron-tasks' }
]
},
{
id: 'advanced',
name: '高级功能',
path: '/guide/advanced',
icon: Settings,
description: '格式转换、请求规则等'
description: '高级功能',
subItems: [
{ name: '格式转换', hash: '#format-conversion' },
{ name: '流式/非流式', hash: '#stream-policy' },
{ name: '请求头/体编辑', hash: '#header-body-edit' },
{ name: '模型映射', hash: '#model-mapping' },
{ name: '正则映射', hash: '#regex-mapping' },
{ name: '能力标签', hash: '#capabilities' },
{ name: '余额监控', hash: '#balance-monitor' },
{ name: '配置导入/出', hash: '#config-export' },
{ name: '锁定用户密钥', hash: '#lock-key' }
]
},
{
id: 'modules',
name: '模块管理',
path: '/guide/modules',
icon: Blocks,
description: '模块管理',
subItems: [
{ name: '访问令牌', hash: '#management-tokens' },
{ name: '邮件配置', hash: '#email-config' },
{ name: 'OAuth登录', hash: '#oauth-login' },
{ name: 'LDAP认证', hash: '#ldap-auth' }
]
},
{
id: 'faq',
name: '常见问题',
path: '/guide/faq',
icon: HelpCircle,
description: '使用中的常见问题'
description: '常见问题'
}
]
// 样式类常量
// 样式类常量 - 使用 Literary Tech 主题
export const panelClasses = {
card: 'bg-white/70 dark:bg-[#262624]/80 backdrop-blur-sm rounded-2xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)]',
cardHover: 'hover:border-[#cc785c]/30 dark:hover:border-[#d4a27f]/30 transition-colors',
section: 'bg-white/50 dark:bg-[#262624]/60 backdrop-blur-sm rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]',
commandPanel: 'rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] bg-white/50 dark:bg-[#1f1d1a]/50',
configPanel: 'rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] overflow-hidden',
panelHeader: 'px-4 py-2 border-b border-[#e5e4df] dark:border-[rgba(227,224,211,0.08)] bg-[#fafaf7]/50 dark:bg-[#1f1d1a]/50',
card: 'literary-card rounded-2xl backdrop-blur-sm transition-all duration-300',
cardHover: 'hover:-translate-y-1 hover:shadow-lg dark:hover:shadow-[var(--book-cloth)]/10 shadow-[var(--book-cloth)]/10',
section: 'literary-surface-inset bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-xl md:rounded-2xl p-5 md:p-8 transition-colors',
commandPanel: 'literary-surface-elevated rounded-xl overflow-hidden shadow-sm backdrop-blur-md',
configPanel: 'literary-surface-elevated rounded-xl overflow-hidden',
panelHeader: 'px-4 py-3 border-b literary-border bg-[var(--color-background-soft)]/50',
codeBody: 'p-0',
badge: 'inline-flex items-center gap-1.5 rounded-full bg-[#cc785c]/10 dark:bg-[#cc785c]/20 border border-[#cc785c]/20 dark:border-[#cc785c]/40 px-3 py-1.5 text-xs font-medium text-[#cc785c] dark:text-[#d4a27f]',
badge: 'literary-badge bg-[var(--color-background)] rounded-full px-3 py-1.5',
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)]',
'flex items-center justify-center rounded-lg border h-8 w-8',
'literary-border',
'bg-transparent',
'text-[#666663] dark:text-[#f1ead8]',
'transition hover:bg-[#f0f0eb] dark:hover:bg-[#3a3731]'
'text-[var(--color-text)]',
'transition hover:bg-[var(--color-background-soft)]'
].join(' ')
} as const
// API 格式说明
export const apiFormats = [
{
name: 'OpenAI Chat',
endpoint: '/v1/chat/completions',
auth: 'Authorization: Bearer xxx',
clients: ['OpenAI SDK', 'Cursor', 'LangChain', '大部分开源工具']
},
{
name: 'OpenAI CLI',
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',
auth: 'x-api-key: xxx',
clients: ['Anthropic SDK']
},
{
name: 'Claude CLI',
endpoint: '/v1/messages',
auth: 'Authorization: Bearer xxx',
clients: ['Claude Code']
},
{
name: 'Gemini Chat',
endpoint: '/v1beta/models/{model}:generateContent',
auth: 'x-goog-api-key: xxx',
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 configSteps = [
{
step: 1,
title: '添加供应商',
description: '创建供应商并配置端点URL、API Key、API 格式)',
path: '/admin/providers'
},
{
step: 2,
title: '创建模型',
description: '定义用户可用的模型名,关联到端点',
path: '/admin/models'
},
{
step: 3,
title: '发放密钥',
description: '为用户创建 API Key设置权限和配额',
path: '/admin/keys'
},
{
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)'
},
{
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 格式'
}
]
// FAQ 数据
export const faqItems = [
{
id: 'concept-provider-endpoint',
category: '概念理解',
question: '供应商和端点有什么区别?',
answer: '供应商是逻辑分组,用于组织管理多个端点。一个供应商可以有多个端点(比如不同区域、不同账号的 API。端点才是实际调用 API 的配置单元,包含 URL、密钥等信息。'
},
{
id: 'concept-model-mapping',
category: '概念理解',
question: '模型映射是什么意思?',
answer: '模型映射让你可以用自定义的模型名(如 gpt-4来访问实际的模型如某端点的 gpt-4-turbo。一个模型可以映射到多个端点实现负载均衡和故障转移。'
},
{
id: 'config-format',
category: '配置问题',
question: '端点的 API 格式怎么选择?',
answer: '根据实际服务商的 API 格式选择。比如 OpenAI 官方选 OpenAIAnthropic 官方选 Claude。如果用的是 OpenAI 兼容的第三方服务,通常选 OpenAI 格式。'
},
{
id: 'config-priority',
category: '配置问题',
question: '端点优先级有什么用?',
answer: '当模型关联多个端点且使用优先级负载均衡模式时,系统会先调用高优先级端点。如果失败(超时、错误等),会自动降级到低优先级端点。适合主备切换场景。'
},
{
id: 'config-quota',
category: '配置问题',
question: '如何限制用户的使用量?',
answer: '在 API Key 配置中设置配额:可以限制每日/每月的请求次数或 Token 用量。也可以在用户层面设置默认配额,新建的 Key 会继承用户配额。'
},
{
id: 'advanced-conversion',
category: '高级功能',
question: '什么是格式转换?',
answer: '格式转换允许用 OpenAI SDK 调用 Claude 模型,或用 Anthropic SDK 调用 OpenAI 模型。系统会自动转换请求和响应格式。需要在系统设置开启,并在端点配置中启用。'
},
{
id: 'advanced-header-rules',
category: '高级功能',
question: '请求头规则有什么用?',
answer: '可以在转发请求时添加、修改或删除 HTTP 头。常用于:添加认证信息、设置特定的 API 版本、添加跟踪标记等。'
},
{
id: 'error-401',
category: '常见错误',
question: '返回 401 Unauthorized 错误?',
answer: '检查1) API Key 是否正确2) Key 是否已过期或被禁用3) 请求头格式是否正确OpenAI 用 BearerClaude 用 x-api-key。'
},
{
id: 'error-404',
category: '常见错误',
question: '返回 404 Not Found 错误?',
answer: '检查1) 模型名称是否正确2) 该模型是否已在系统中配置3) API Key 是否有权限访问该模型4) 端点 URL 是否正确。'
},
{
id: 'error-502',
category: '常见错误',
question: '返回 502/503 错误?',
answer: '表示上游服务不可用。检查1) 端点健康状态2) 供应商 API 是否正常3) 网络连接是否正常。可以在健康监控页面查看端点状态。'
}
]