refactor(docs/home): 重构首页导航和文档结构

- README 新增架构图(明暗主题 SVG),更新 Proxy 描述
- 首页导航栏将"文档"链接从底部按钮区移至顶部导航
- 删除独立的 body-rules-spec 文档
- GuideLayout 路由切换时自动滚动到顶部
- 架构图组件移除网格背景
This commit is contained in:
fawney19
2026-02-24 12:15:14 +08:00
parent 837bed3d47
commit 39b036abd5
7 changed files with 270 additions and 277 deletions

View File

@@ -22,6 +22,14 @@
Aether 是一个自托管的 AI API 网关,为团队和个人提供多租户管理、智能负载均衡、成本配额控制和健康监控能力。通过统一的 API 入口,可以无缝对接 Claude、OpenAI、Gemini 等主流 AI 服务及其 CLI 工具。 Aether 是一个自托管的 AI API 网关,为团队和个人提供多租户管理、智能负载均衡、成本配额控制和健康监控能力。通过统一的 API 入口,可以无缝对接 Claude、OpenAI、Gemini 等主流 AI 服务及其 CLI 工具。
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/architecture/architecture-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="docs/architecture/architecture-light.svg">
<img src="docs/architecture/architecture-light.svg" width="680" alt="Aether Architecture">
</picture>
</p>
页面预览: https://fawney19.github.io/Aether/ 页面预览: https://fawney19.github.io/Aether/
## 部署 ## 部署
@@ -75,7 +83,7 @@ cd frontend && npm install && npm run dev
## Aether Proxy ## Aether Proxy
Aether Proxy 是配套的正向代理节点,部署在海外 VPS 上,为墙内的 Aether 实例中转 API 流量。支持 TUI 向导一键配置、systemd 服务管理、TLS 加密、DNS 缓存及连接池调优。 Aether Proxy 是配套的正向代理节点,部署在海外 VPS 上,为墙内的 Aether 实例中转 API 流量。或者部署在其他服务器为指定的提供商、账号、Key使用不同的节点访问。支持 TUI 向导一键配置、systemd 服务管理、TLS 加密、DNS 缓存及连接池调优。
- Docker Compose 部署或下载预编译二进制直接运行 - Docker Compose 部署或下载预编译二进制直接运行
- 通过 `aether-proxy setup` 完成交互式配置,自动注册为系统服务 - 通过 `aether-proxy setup` 完成交互式配置,自动注册为系统服务

View File

@@ -0,0 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" width="760" height="420" font-family="'Inter', system-ui, -apple-system, sans-serif">
<defs>
<filter id="shadow-sm" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-opacity="0.5"/>
</filter>
<filter id="shadow-lg" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-opacity="0.7"/>
</filter>
<clipPath id="gateway-clip">
<path d="M 0 0 h 760 v 420 H 0 Z M 74 169 v 54 h 12 v -54 Z" clip-rule="evenodd"/>
</clipPath>
</defs>
<!-- Background -->
<rect width="760" height="420" rx="24" fill="#121212"/>
<!-- === Labels placeholder, drawn at top layer === -->
<!-- Source pills -->
<rect x="138" y="24" width="100" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="163" cy="42" r="3" fill="#cc7154"/>
<text x="195" y="46" font-size="11" font-weight="700" fill="#f3f4f6" text-anchor="middle">Claude</text>
<rect x="330" y="24" width="100" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="355" cy="42" r="3" fill="#cc7154"/>
<text x="387" y="46" font-size="11" font-weight="700" fill="#f3f4f6" text-anchor="middle">OpenAI</text>
<rect x="522" y="24" width="100" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="547" cy="42" r="3" fill="#cc7154"/>
<text x="579" y="46" font-size="11" font-weight="700" fill="#f3f4f6" text-anchor="middle">Gemini</text>
<!-- Output pills -->
<rect x="128" y="360" width="120" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="153" cy="378" r="3" fill="#cc7154"/>
<text x="195" y="382" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">Claude 响应</text>
<rect x="320" y="360" width="120" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="345" cy="378" r="3" fill="#cc7154"/>
<text x="387" y="382" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">OpenAI 响应</text>
<rect x="512" y="360" width="120" height="36" rx="18" fill="#1a1a1c" stroke="#2a2a2c" filter="url(#shadow-sm)"/>
<circle cx="537" cy="378" r="3" fill="#cc7154"/>
<text x="579" y="382" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">Gemini 响应</text>
<!-- === Layer 2: AETHER GATEWAY box === -->
<rect x="80" y="78" width="600" height="240" rx="24" fill="#1a1a1c" filter="url(#shadow-lg)"/>
<rect x="80" y="78" width="600" height="240" rx="24" fill="none" stroke="rgba(204,113,84,0.4)" clip-path="url(#gateway-clip)"/>
<g transform="translate(80, 176)" font-size="7" font-weight="900" fill="#cc7154" text-anchor="middle">
<text x="0" y="0">A</text><text x="0" y="9">E</text><text x="0" y="18">T</text>
<text x="0" y="27">H</text><text x="0" y="36">E</text><text x="0" y="45">R</text>
</g>
<!-- Dashed box -->
<rect x="180" y="98" width="400" height="112" rx="16" fill="none" stroke="rgba(204,113,84,0.4)" stroke-width="1.5" stroke-dasharray="6 4"/>
<rect x="200" y="108" width="360" height="26" rx="12" fill="none" stroke="#2a2a2c"/>
<text x="380" y="125" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">统一模型规范 / 协议聚合</text>
<rect x="200" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.12)"/>
<text x="242" y="157" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">多端鉴权</text>
<rect x="290" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.12)"/>
<text x="332" y="157" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">配额管控</text>
<rect x="380" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.12)"/>
<text x="422" y="157" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">全局并发</text>
<rect x="470" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.12)"/>
<text x="512" y="157" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">缓存亲和</text>
<rect x="200" y="172" width="360" height="28" rx="14" fill="none" stroke="#2a2a2c"/>
<text x="380" y="190" font-size="10" font-weight="600" fill="#f3f4f6" text-anchor="middle">智能调度 / 故障转移</text>
<!-- Engine cards -->
<rect x="103" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.12)" stroke="#2a2a2c"/>
<circle cx="163" cy="264" r="3" fill="#cc7154"/>
<text x="192" y="268" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">格式转换</text>
<rect x="295" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.12)" stroke="#2a2a2c"/>
<circle cx="355" cy="264" r="3" fill="#cc7154"/>
<text x="384" y="268" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">反向代理</text>
<rect x="487" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.12)" stroke="#2a2a2c"/>
<circle cx="547" cy="264" r="3" fill="#cc7154"/>
<text x="576" y="268" font-size="10" font-weight="700" fill="#f3f4f6" text-anchor="middle">原生直通</text>
<!-- === Layer 3: Track lines === -->
<g stroke="rgba(255,255,255,0.08)" stroke-width="1.5" fill="none" stroke-linecap="round">
<path id="f-in-1" d="M 188 60 L 188 66 C 188 72, 380 72, 380 78 L 380 98"/>
<path id="f-in-2" d="M 380 60 L 380 98"/>
<path id="f-in-3" d="M 572 60 L 572 66 C 572 72, 380 72, 380 78 L 380 98"/>
<path id="f-sL" d="M 380 210 C 380 230, 188 230, 188 250"/>
<path id="f-sM" d="M 380 210 L 380 250"/>
<path id="f-sR" d="M 380 210 C 380 230, 572 230, 572 250"/>
<path id="f-oL" d="M 188 278 L 188 284 C 188 302, 380 302, 380 318"/>
<path id="f-oM" d="M 380 278 L 380 318"/>
<path id="f-oR" d="M 572 278 L 572 284 C 572 302, 380 302, 380 318"/>
<path id="f-oL2" d="M 380 318 C 380 338, 188 338, 188 360"/>
<path id="f-oM2" d="M 380 318 L 380 360"/>
<path id="f-oR2" d="M 380 318 C 380 338, 572 338, 572 360"/>
</g>
<!-- Animated dots -->
<g>
<circle r="2.5" fill="#cc7154"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#f-in-1"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#f-in-3"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-sL"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-sR"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-oL"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-oR"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.2s" repeatCount="indefinite"><mpath href="#f-oL2"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.2s" repeatCount="indefinite"><mpath href="#f-oR2"/></animateMotion></circle>
</g>
<!-- === Layer 4: Junction dots === -->
<circle cx="380" cy="78" r="4" fill="#cc7154" stroke="#1e1e1e"/>
<circle cx="380" cy="210" r="4" fill="#cc7154" stroke="#1e1e1e"/>
<circle cx="380" cy="318" r="4" fill="#cc7154" stroke="#1e1e1e"/>
<!-- === Layer 5: SOURCES / OUTPUTS labels === -->
<g>
<circle cx="355" cy="12" r="2" fill="#6b7280"/>
<text x="365" y="15" font-size="9" font-weight="800" letter-spacing="0.2em" fill="#6b7280">SOURCES</text>
</g>
<g>
<circle cx="353" cy="346" r="2" fill="#6b7280"/>
<text x="363" y="349" font-size="9" font-weight="800" letter-spacing="0.2em" fill="#6b7280">OUTPUTS</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,118 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" width="760" height="420" font-family="'Inter', system-ui, -apple-system, sans-serif">
<defs>
<filter id="shadow-sm" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.04"/>
</filter>
<filter id="shadow-lg" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="6" stdDeviation="10" flood-opacity="0.06"/>
</filter>
<!-- Clip path: full area minus AETHER label zone -->
<clipPath id="gateway-clip">
<path d="M 0 0 h 760 v 420 H 0 Z M 74 169 v 54 h 12 v -54 Z" clip-rule="evenodd"/>
</clipPath>
</defs>
<!-- Background -->
<rect width="760" height="420" rx="24" fill="#fcfcfc"/>
<!-- === Labels placeholder, drawn at top layer === -->
<!-- Source pills -->
<rect x="138" y="24" width="100" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="163" cy="42" r="3" fill="#cc7154"/>
<text x="195" y="46" font-size="11" font-weight="700" fill="#1f2937" text-anchor="middle">Claude</text>
<rect x="330" y="24" width="100" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="355" cy="42" r="3" fill="#cc7154"/>
<text x="387" y="46" font-size="11" font-weight="700" fill="#1f2937" text-anchor="middle">OpenAI</text>
<rect x="522" y="24" width="100" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="547" cy="42" r="3" fill="#cc7154"/>
<text x="579" y="46" font-size="11" font-weight="700" fill="#1f2937" text-anchor="middle">Gemini</text>
<!-- Output pills -->
<rect x="128" y="360" width="120" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="153" cy="378" r="3" fill="#cc7154"/>
<text x="195" y="382" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">Claude 响应</text>
<rect x="320" y="360" width="120" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="345" cy="378" r="3" fill="#cc7154"/>
<text x="387" y="382" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">OpenAI 响应</text>
<rect x="512" y="360" width="120" height="36" rx="18" fill="#ffffff" stroke="#e8e8ec" filter="url(#shadow-sm)"/>
<circle cx="537" cy="378" r="3" fill="#cc7154"/>
<text x="579" y="382" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">Gemini 响应</text>
<!-- === Layer 2: AETHER GATEWAY box (z-20) === -->
<rect x="80" y="78" width="600" height="240" rx="24" fill="#ffffff" filter="url(#shadow-lg)"/>
<rect x="80" y="78" width="600" height="240" rx="24" fill="none" stroke="rgba(204,113,84,0.25)" clip-path="url(#gateway-clip)"/>
<g transform="translate(80, 176)" font-size="7" font-weight="900" fill="#cc7154" text-anchor="middle">
<text x="0" y="0">A</text><text x="0" y="9">E</text><text x="0" y="18">T</text>
<text x="0" y="27">H</text><text x="0" y="36">E</text><text x="0" y="45">R</text>
</g>
<!-- Dashed box -->
<rect x="180" y="98" width="400" height="112" rx="16" fill="none" stroke="rgba(204,113,84,0.25)" stroke-width="1.5" stroke-dasharray="6 4"/>
<rect x="200" y="108" width="360" height="26" rx="12" fill="none" stroke="#e8e8ec"/>
<text x="380" y="125" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">统一模型规范 / 协议聚合</text>
<rect x="200" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.06)"/>
<text x="242" y="157" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">多端鉴权</text>
<rect x="290" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.06)"/>
<text x="332" y="157" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">配额管控</text>
<rect x="380" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.06)"/>
<text x="422" y="157" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">全局并发</text>
<rect x="470" y="140" width="85" height="26" rx="13" fill="rgba(204,113,84,0.06)"/>
<text x="512" y="157" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">缓存亲和</text>
<rect x="200" y="172" width="360" height="28" rx="14" fill="none" stroke="#e8e8ec"/>
<text x="380" y="190" font-size="10" font-weight="600" fill="#1f2937" text-anchor="middle">智能调度 / 故障转移</text>
<!-- Engine cards -->
<rect x="103" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.06)" stroke="#e8e8ec"/>
<circle cx="163" cy="264" r="3" fill="#cc7154"/>
<text x="192" y="268" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">格式转换</text>
<rect x="295" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.06)" stroke="#e8e8ec"/>
<circle cx="355" cy="264" r="3" fill="#cc7154"/>
<text x="384" y="268" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">反向代理</text>
<rect x="487" y="250" width="170" height="28" rx="14" fill="rgba(204,113,84,0.06)" stroke="#e8e8ec"/>
<circle cx="547" cy="264" r="3" fill="#cc7154"/>
<text x="576" y="268" font-size="10" font-weight="700" fill="#1f2937" text-anchor="middle">原生直通</text>
<!-- === Layer 3: Track lines (z-30, above Gateway fill) === -->
<g stroke="#e5e7eb" stroke-width="1.5" fill="none" stroke-linecap="round">
<path id="f-in-1" d="M 188 60 L 188 66 C 188 72, 380 72, 380 78 L 380 98"/>
<path id="f-in-2" d="M 380 60 L 380 98"/>
<path id="f-in-3" d="M 572 60 L 572 66 C 572 72, 380 72, 380 78 L 380 98"/>
<path id="f-sL" d="M 380 210 C 380 230, 188 230, 188 250"/>
<path id="f-sM" d="M 380 210 L 380 250"/>
<path id="f-sR" d="M 380 210 C 380 230, 572 230, 572 250"/>
<path id="f-oL" d="M 188 278 L 188 284 C 188 302, 380 302, 380 318"/>
<path id="f-oM" d="M 380 278 L 380 318"/>
<path id="f-oR" d="M 572 278 L 572 284 C 572 302, 380 302, 380 318"/>
<path id="f-oL2" d="M 380 318 C 380 338, 188 338, 188 360"/>
<path id="f-oM2" d="M 380 318 L 380 360"/>
<path id="f-oR2" d="M 380 318 C 380 338, 572 338, 572 360"/>
</g>
<!-- Animated dots -->
<g>
<circle r="2.5" fill="#cc7154"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#f-in-1"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="2.2s" repeatCount="indefinite"><mpath href="#f-in-3"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-sL"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-sR"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-oL"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.4s" repeatCount="indefinite"><mpath href="#f-oR"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.2s" repeatCount="indefinite"><mpath href="#f-oL2"/></animateMotion></circle>
<circle r="2.5" fill="#cc7154"><animateMotion dur="1.2s" repeatCount="indefinite"><mpath href="#f-oR2"/></animateMotion></circle>
</g>
<!-- === Layer 4: Junction dots (z-40, topmost) === -->
<circle cx="380" cy="78" r="4" fill="#cc7154" stroke="#ffffff"/>
<circle cx="380" cy="210" r="4" fill="#cc7154" stroke="#ffffff"/>
<circle cx="380" cy="318" r="4" fill="#cc7154" stroke="#ffffff"/>
<!-- === Layer 5: SOURCES / OUTPUTS labels (topmost) === -->
<g>
<circle cx="355" cy="12" r="2" fill="#8c94a1"/>
<text x="365" y="15" font-size="9" font-weight="800" letter-spacing="0.2em" fill="#8c94a1">SOURCES</text>
</g>
<g>
<circle cx="353" cy="346" r="2" fill="#8c94a1"/>
<text x="363" y="349" font-size="9" font-weight="800" letter-spacing="0.2em" fill="#8c94a1">OUTPUTS</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,262 +0,0 @@
# Aether 请求体规则 (Body Rules) — AI 辅助指南
> 本文档面向 AI 助手。用户在「端点管理 → 请求规则 → + 请求体」中添加规则AI 需要指导用户在 UI 表单的各个输入框中填写什么内容。
## 系统背景
Aether 是一个 AI API 网关,将用户请求转发给上游 ProviderOpenAI、Claude、Gemini 等)。请求体规则在转发前修改请求体 JSON每个 Endpoint 可配置多条规则。
## 用户请求体长什么样
规则操作的对象是用户发给 Aether 的 JSON 请求体。根据 Endpoint 的 API 格式不同,结构也不同:
### OpenAI Chat 格式(`openai:chat`
```json
{
"model": "gpt-4",
"messages": [
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "你好"}
],
"temperature": 0.7,
"max_tokens": 1000,
"stream": true,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"stop": ["\n"],
"tools": [...],
"tool_choice": "auto",
"response_format": {"type": "json_object"},
"metadata": {...}
}
```
### Claude Chat 格式(`claude:chat`
```json
{
"model": "claude-sonnet-4-20250514",
"messages": [
{"role": "user", "content": "你好"}
],
"system": "你是一个助手",
"max_tokens": 1024,
"temperature": 0.7,
"stream": true,
"top_p": 1,
"top_k": 40,
"stop_sequences": ["###"],
"metadata": {"user_id": "xxx"}
}
```
### Claude CLI 格式(`claude:cli`
```json
{
"model": "claude-sonnet-4-20250514",
"messages": [
{"role": "user", "content": "帮我重构这段代码"}
],
"system": "你是一个编程助手",
"max_tokens": 16384,
"stream": true
}
```
### Gemini Chat 格式(`gemini:chat`
```json
{
"contents": [
{"role": "user", "parts": [{"text": "你好"}]}
],
"generationConfig": {
"temperature": 0.7,
"maxOutputTokens": 1024,
"topP": 0.9,
"topK": 40
},
"systemInstruction": {"parts": [{"text": "你是一个助手"}]},
"safetySettings": [...]
}
```
> **注意:** `model` 和 `stream` 是受保护字段,任何规则都无法修改它们。
---
## UI 表单说明
用户在端点管理对话框中点击「+ 请求体」添加规则。每条规则有一个下拉框选择操作类型,然后根据类型显示不同的输入框。
---
### 覆写set
**UI 布局:** `[覆写 ▼]` `[字段路径]` `=` `[值]` `[✓]`
| 输入框 | 填什么 | 示例 |
|--------|--------|------|
| 字段路径 | 要设置的字段,用 `.` 分隔层级,`[N]` 访问数组 | `temperature` |
| 值 | **JSON 格式**的值。字符串要加引号,数字直接写 | `0.7` |
值输入框右边有验证图标:绿色勾=JSON合法红色叉=格式错误。
**用户常见需求 → 怎么填:**
| 用户说 | 字段路径 | 值 |
|--------|---------|-----|
| "固定 temperature 为 0.3" | `temperature` | `0.3` |
| "限制最大输出 500 token" | `max_tokens` | `500` |
| "加一个 metadata 字段标记来源" | `metadata.source` | `"my-app"` |
| "设置 top_p 为 0.9" | `top_p` | `0.9` |
| "添加停止序列" | `stop` | `["\n", "###"]` |
| "设置响应格式为 JSON" | `response_format` | `{"type": "json_object"}` |
| "把第一条消息内容改掉" | `messages[0].content` | `"新的内容"` |
| "设置一个嵌套对象" | `metadata.tracking` | `{"id": "abc", "env": "prod"}` |
| "设置值为空" | `some_field` | `null` |
> **注意:** 字符串值必须加引号!`"hello"` 是字符串,`hello` 是无效 JSON。数字、布尔、null、数组、对象不需要额外引号。
---
### 删除drop
**UI 布局:** `[删除 ▼]` `[要删除的字段路径]`
| 输入框 | 填什么 | 示例 |
|--------|--------|------|
| 字段路径 | 要删除的字段路径 | `user_info.ip_address` |
**用户常见需求 → 怎么填:**
| 用户说 | 字段路径 |
|--------|---------|
| "去掉 user 字段" | `user` |
| "删除 metadata 里的 internal_flag" | `metadata.internal_flag` |
| "移除第一条消息" | `messages[0]` |
| "去掉 frequency_penalty" | `frequency_penalty` |
> **注意:** 删除数组元素会导致后续索引前移。如需删除多个数组元素,从后往前删。
---
### 重命名rename
**UI 布局:** `[重命名 ▼]` `[原路径]` `→` `[新路径]`
| 输入框 | 填什么 | 示例 |
|--------|--------|------|
| 原路径 | 源字段的路径 | `extra.trace_id` |
| 新路径 | 目标字段的路径(中间层级会自动创建) | `metadata.request_id` |
**用户常见需求 → 怎么填:**
| 用户说 | 原路径 | 新路径 |
|--------|--------|--------|
| "把 max_tokens 改名为 max_completion_tokens" | `max_tokens` | `max_completion_tokens` |
| "把 extra 里的 id 移到 metadata 下" | `extra.id` | `metadata.id` |
---
### 插入insert
**UI 布局:** `[插入 ▼]` `[数组路径]` `[位置]` `[值(JSON)]` `[✓]`
| 输入框 | 填什么 | 示例 |
|--------|--------|------|
| 数组路径 | 目标数组的路径(必须是已有的数组) | `messages` |
| 位置 | 插入位置的数字索引,**留空=追加到末尾** | `0`(开头)或留空(末尾) |
| 值 | JSON 格式的元素 | `{"role": "system", "content": "..."}` |
**用户常见需求 → 怎么填:**
| 用户说 | 数组路径 | 位置 | 值 |
|--------|---------|------|-----|
| "在开头加一条 system 消息" | `messages` | `0` | `{"role": "system", "content": "你是一个专业助手"}` |
| "在末尾追加一条消息" | `messages` | (留空) | `{"role": "user", "content": "请用中文回答"}` |
| "在第二条消息前插入" | `messages` | `1` | `{"role": "assistant", "content": "好的"}` |
> **位置说明:** `0`=最前面,`1`=第二个位置,`-1`=倒数第一个前面。留空=追加到最后。
---
### 正则替换regex_replace
**UI 布局:** `[正则替换 ▼]` `[字段路径]` `[正则]` `→` `[替换为]` `[ims]` `[✓]`
| 输入框 | 填什么 | 示例 |
|--------|--------|------|
| 字段路径 | 目标字符串字段的路径 | `messages[-1].content` |
| 正则 | 正则表达式(**不需要**加 `/` 包裹) | `1[3-9]\d{9}` |
| 替换为 | 替换成什么,留空=删除匹配内容 | `[手机号已隐藏]` |
| ims | 可选标志,留空=默认 | `i` |
> **重要:** 正则输入框里直接写正则语法即可,**不需要** JSON 转义。`\d` 就写 `\d`,不用写 `\\d`。JSON 转义是保存时系统自动处理的。
**flags 含义:**
| 字母 | 作用 | 什么时候用 |
|------|------|-----------|
| `i` | 忽略大小写 | 匹配 `hello`/`Hello`/`HELLO` |
| `m` | 多行模式 | `^`/`$` 匹配每行而非整个字符串 |
| `s` | dotall | `.` 能匹配换行符 |
大多数情况留空即可。
**用户常见需求 → 怎么填:**
| 用户说 | 字段路径 | 正则 | 替换为 | flags |
|--------|---------|------|--------|-------|
| "隐藏最后一条消息里的手机号" | `messages[-1].content` | `1[3-9]\d{9}` | `[手机号已隐藏]` | |
| "隐藏邮箱地址" | `messages[-1].content` | `[\w.+-]+@[\w.-]+\.\w{2,}` | `[邮箱已隐藏]` | `i` |
| "去掉 HTML 标签" | `messages[-1].content` | `<[^>]+>` | (留空) | |
| "把所有的 foo 替换成 bar" | `messages[-1].content` | `\bfoo\b` | `bar` | |
| "删掉 Markdown 加粗标记" | `messages[-1].content` | `\*\*([^*]+)\*\*` | `\1` | |
| "隐藏身份证号" | `messages[-1].content` | `\d{17}[\dXx]` | `[身份证已隐藏]` | |
| "隐藏 IP 地址" | `messages[-1].content` | `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}` | `[IP已隐藏]` | |
---
## 路径语法速查
| 写法 | 含义 |
|------|------|
| `temperature` | 顶层字段 |
| `metadata.source` | 嵌套字段 |
| `messages[0]` | 数组第一个元素 |
| `messages[-1]` | 数组最后一个元素 |
| `messages[0].content` | 第一条消息的 content |
| `messages[-1].content` | 最后一条消息的 content |
| `data[0].items[2]` | 嵌套数组访问 |
| `config\.v1.enabled` | key 名字里有点号时用 `\.` 转义 |
---
## 多条规则示例
用户说:"帮我配置规则:在开头插入一条 system 消息说'用中文回答',固定 temperature 为 0.3,把用户消息里的手机号脱敏"
应指导用户添加 3 条规则:
| # | 操作 | 字段 1 | 字段 2 | 字段 3 | 字段 4 |
|---|------|--------|--------|--------|--------|
| 1 | 插入 | 路径: `messages` | 位置: `0` | 值: `{"role": "system", "content": "请用中文回答所有问题"}` | |
| 2 | 覆写 | 路径: `temperature` | 值: `0.3` | | |
| 3 | 正则替换 | 路径: `messages[-1].content` | 正则: `1[3-9]\d{9}` | 替换为: `[手机号]` | flags: (留空) |
规则按从上到下的顺序执行。
---
## 注意事项
1. **`model``stream` 不可修改** — 这两个字段由系统管理,写了规则也会被跳过
2. **值必须是合法 JSON** — 覆写/插入的值输入框要求 JSON 格式:字符串加引号 `"text"`,数字直接写 `123`,布尔 `true`/`false`
3. **路径必须指向正确类型** — 插入的路径必须是数组,正则替换的路径必须是字符串
4. **路径不存在时的行为** — 覆写会自动创建中间层级dict其他操作遇到不存在的路径会静默跳过
5. **正则在 UI 里直接写** — 不需要 JSON 转义,`\d` 就是 `\d`
6. **正则保存时校验** — 无效的正则表达式会在保存时报错,不会静默通过

View File

@@ -111,11 +111,11 @@
</div> </div>
<!-- Navigation --> <!-- Navigation -->
<nav class="flex items-center gap-2 mx-16 lg:mx-28"> <nav class="flex items-center gap-2 mx-8 lg:mx-16">
<button <button
v-for="(section, index) in sections" v-for="(section, index) in sections.slice(0, -1)"
:key="index" :key="index"
class="group relative px-3 py-2 text-sm font-medium transition" class="group relative px-3 py-2 text-sm font-medium transition whitespace-nowrap"
:class="currentSection === index :class="currentSection === index
? 'text-[#cc785c] dark:text-[#d4a27f]' ? 'text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'" : 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'"
@@ -127,6 +127,25 @@
:class="currentSection === index ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'" :class="currentSection === index ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'"
/> />
</button> </button>
<RouterLink
to="/guide"
class="group relative px-3 py-2 text-sm font-medium transition whitespace-nowrap text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white"
>
文档
</RouterLink>
<button
class="group relative px-3 py-2 text-sm font-medium transition whitespace-nowrap"
:class="currentSection === SECTIONS.FEATURES
? 'text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'"
@click="scrollToSection(SECTIONS.FEATURES)"
>
{{ sections[SECTIONS.FEATURES].name }}
<div
class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full transition-all duration-300"
:class="currentSection === SECTIONS.FEATURES ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'"
/>
</button>
</nav> </nav>
<!-- Login/Dashboard Button --> <!-- Login/Dashboard Button -->
@@ -408,13 +427,6 @@
class="mt-6 md:mt-12 transition-all duration-700 flex items-center justify-center gap-4 relative z-30" class="mt-6 md:mt-12 transition-all duration-700 flex items-center justify-center gap-4 relative z-30"
:style="getButtonsStyle(SECTIONS.FEATURES)" :style="getButtonsStyle(SECTIONS.FEATURES)"
> >
<RouterLink
to="/guide"
class="inline-flex items-center justify-center gap-2 rounded-xl bg-transparent border-2 border-[#cc785c] px-6 py-3 text-base font-semibold text-[#cc785c] dark:text-[#d4a27f] dark:border-[#d4a27f] transition hover:bg-[#cc785c]/10 dark:hover:bg-[#d4a27f]/10 hover:scale-105 w-[160px]"
>
<BookOpen class="h-5 w-5" />
配置教程
</RouterLink>
<RouterLink <RouterLink
v-if="authStore.isAuthenticated" v-if="authStore.isAuthenticated"
:to="dashboardPath" :to="dashboardPath"
@@ -444,7 +456,6 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { import {
BookOpen,
ChevronDown, ChevronDown,
Code2, Code2,
Moon, Moon,

View File

@@ -409,6 +409,10 @@ watch(
() => { () => {
mobileMenuOpen.value = false mobileMenuOpen.value = false
activeHash.value = '' activeHash.value = ''
const container = getScrollContainer()
if (container) {
container.scrollTo({ top: 0 })
}
nextTick(() => { nextTick(() => {
setupIntersectionObserver() setupIntersectionObserver()
const firstSection = document.querySelector('article section[id]') const firstSection = document.querySelector('article section[id]')

View File

@@ -34,7 +34,6 @@ const colors = computed(() => {
const brand = '#cc7154' const brand = '#cc7154'
return { return {
bg: isDark.value ? '#121212' : '#fcfcfc', bg: isDark.value ? '#121212' : '#fcfcfc',
grid: isDark.value ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
cardBg: isDark.value ? '#1a1a1c' : '#ffffff', cardBg: isDark.value ? '#1a1a1c' : '#ffffff',
cardBorder: isDark.value ? '#2a2a2c' : '#e8e8ec', cardBorder: isDark.value ? '#2a2a2c' : '#e8e8ec',
track: isDark.value ? 'rgba(255,255,255,0.08)' : '#e5e7eb', track: isDark.value ? 'rgba(255,255,255,0.08)' : '#e5e7eb',
@@ -98,8 +97,6 @@ const shadows = computed(() => {
:style="{ :style="{
backgroundColor: colors.bg, backgroundColor: colors.bg,
borderColor: colors.cardBorder, borderColor: colors.cardBorder,
backgroundImage: `radial-gradient(${colors.grid} 1px, transparent 1px)`,
backgroundSize: '20px 20px',
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: 'top left', transformOrigin: 'top left',
marginLeft: `${offsetLeft}px`, marginLeft: `${offsetLeft}px`,