449 lines
25 KiB
Markdown
449 lines
25 KiB
Markdown
<div align="center">
|
||
|
||
<img src="docs/assets/logo.svg" width="104" alt="OCI Portal logo">
|
||
|
||
# OCI Portal
|
||
|
||
**自托管的 OCI 多租户管理面板与 GenAI 兼容网关**
|
||
|
||

|
||

|
||

|
||

|
||
|
||
[快速开始](#快速开始) · [核心能力](#核心能力) · [生产部署](#生产部署) · [AI 网关](#ai-网关) · [开发](#开发)
|
||
|
||
</div>
|
||
|
||
OCI Portal 将多份 OCI API Key、云资源、自动化任务、审计事件和 OCI GenAI 渠道集中到一个管理界面。Vue 前端通过 `go:embed` 嵌入 Go 服务,Release 以单个二进制和多架构容器镜像交付。
|
||
|
||
本仓库为后端与发行仓库;前端源码位于 [oci-portal-dash](https://github.com/wangdefaa/oci-portal-dash)。
|
||
|
||
## 界面预览
|
||
|
||
| 总览 | 登录 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
| 租户 | 任务 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
| AI 网关 | 通知设置 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
## 核心能力
|
||
|
||
- **租户与区域**:集中管理多份 OCI API Key,支持分组、批量测活、账户画像、订阅区域缓存与区域切换;私钥和口令使用 AES-256-GCM 加密落库
|
||
- **计算、网络与存储**:实例创建与电源操作、公网 IP、IPv6、VNIC、串行控制台连接,VCN / 子网 / 安全列表,引导卷与块存储挂载,限额和成本查询
|
||
- **自动化任务**:抢机、租户测活、成本同步、AI 渠道探测四类 cron 任务,提供执行日志、重叠执行防护、熔断与结果通知
|
||
- **网页控制台**:浏览器内使用 xterm 串行终端和 noVNC,通过 OCI 控制台连接建立两跳 SSH 隧道
|
||
- **身份与审计**:IAM 用户、MFA、API Key、密码策略、SAML 身份提供商、通知收件人和多 Identity Domain 管理;OCI Audit 事件可经 Service Connector Hub 与 Notifications 回传,关键事件按类别通过「云端事件」通知推送
|
||
- **通知与安全**:Telegram、Webhook、ntfy、Bark、SMTP 五类渠道;JWT、bcrypt、TOTP、OIDC / GitHub 登录、登录锁定、IP 限速、会话撤销和系统操作审计
|
||
- **AI 网关**:提供 OpenAI Responses、Chat Completions、Embeddings 与 Anthropic Messages 兼容接口,支持渠道分组、加权路由、熔断探测、模型黑白名单、密钥管理和调用日志
|
||
|
||
## 运行形态
|
||
|
||
```text
|
||
浏览器 / API 客户端
|
||
│
|
||
▼
|
||
┌──────────────────── OCI Portal 单个 Go 进程 ────────────────────┐
|
||
│ Vue 3 静态资源(go:embed) │
|
||
│ /api/v1 → Gin → Service → OCI SDK │
|
||
│ /ai/v1 → 兼容转换与路由 → OCI Generative AI │
|
||
│ GORM → SQLite(默认)/ MySQL / PostgreSQL(experimental) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
默认推荐 SQLite 单实例部署。MySQL 和 PostgreSQL 适配仍属 experimental,不应据此推断服务支持多副本并发运行。
|
||
|
||
## 快速开始
|
||
|
||
### Docker Compose(推荐)
|
||
|
||
前置条件:Docker、Docker Compose v2、OpenSSL。
|
||
|
||
1. 克隆仓库并生成一份需要长期保存的 `.env`:
|
||
|
||
```bash
|
||
git clone https://github.com/wangdefaa/oci-portal.git
|
||
cd oci-portal
|
||
|
||
umask 077
|
||
printf 'DATA_KEY=%s\nJWT_SECRET=%s\nADMIN_PASSWORD=%s\n' \
|
||
"$(openssl rand -hex 32)" \
|
||
"$(openssl rand -hex 32)" \
|
||
"$(openssl rand -base64 24)" > .env
|
||
chmod 600 .env
|
||
```
|
||
|
||
2. 准备数据目录并启动:
|
||
|
||
```bash
|
||
mkdir -p data
|
||
|
||
# Linux bind mount 需要让镜像内的 nonroot 用户(uid 65532)可写。
|
||
sudo chown 65532:65532 data
|
||
|
||
docker compose up -d
|
||
docker compose ps
|
||
```
|
||
|
||
3. 查看初始管理员密码并登录:
|
||
|
||
```bash
|
||
grep '^ADMIN_PASSWORD=' .env
|
||
```
|
||
|
||
访问 `http://127.0.0.1:18888`,默认用户名为 `admin`。
|
||
|
||
> `DATA_KEY` 用于解密数据库中的 OCI 私钥、口令和渠道凭据。它只能生成一次并持续复用;丢失或更换后,已有密文无法恢复。请将 `.env` 与 `data/oci-portal.db` 一起备份。
|
||
|
||
### 二进制运行
|
||
|
||
Release 提供 Linux amd64 / arm64 二进制。以下以 amd64 为例;arm64 主机将文件名中的 `amd64` 替换为 `arm64`:
|
||
|
||
```bash
|
||
curl -fLO https://github.com/wangdefaa/oci-portal/releases/latest/download/oci-portal-server-linux-amd64
|
||
chmod +x oci-portal-server-linux-amd64
|
||
|
||
# 复用上文生成并妥善保存的 .env。
|
||
set -a
|
||
. ./.env
|
||
set +a
|
||
|
||
./oci-portal-server-linux-amd64
|
||
```
|
||
|
||
默认访问地址为 `http://localhost:8080`。`ADMIN_PASSWORD` 只在数据库中没有用户时创建初始管理员,后续启动不会用它重置密码。
|
||
|
||
### 源码构建
|
||
|
||
源码构建要求 Go 1.26.5、`curl`、`unzip` 和 `sha256sum`。仓库只保留前端占位页,编译完整单文件前应下载 `DASH_VERSION` 指定的前端产物并校验:
|
||
|
||
```bash
|
||
DASH_TAG="$(tr -d '\r\n' < DASH_VERSION)"
|
||
DASH_BASE="https://github.com/wangdefaa/oci-portal-dash/releases/download/${DASH_TAG}"
|
||
|
||
curl -fL -o dist.zip "${DASH_BASE}/dist.zip"
|
||
curl -fL -o dist.zip.sha256 "${DASH_BASE}/dist.zip.sha256"
|
||
sha256sum -c dist.zip.sha256
|
||
|
||
rm -rf internal/webui/dist
|
||
mkdir -p internal/webui/dist
|
||
unzip -q dist.zip -d internal/webui/dist
|
||
|
||
CGO_ENABLED=0 go build -trimpath -o bin/oci-portal-server ./cmd/server
|
||
```
|
||
|
||
本地构建显示 `dev` 版本;Release 工作流会注入正式版本和构建时间。`internal/webui/dist` 中的真实前端产物不应提交到仓库。
|
||
|
||
## 生产部署
|
||
|
||
服务本身只提供 HTTP。除本机试用外,应保持服务仅监听回环地址或容器内部网络,并由 TLS 反向代理提供 HTTPS;否则管理员密码、JWT、AI 密钥和租户凭据会经明文连接传输。
|
||
|
||
### Caddy
|
||
|
||
```caddyfile
|
||
portal.example.com {
|
||
reverse_proxy 127.0.0.1:18888
|
||
}
|
||
```
|
||
|
||
Caddy 会自动处理 WebSocket。使用 nginx、Traefik 或其他反向代理时,还需要满足:
|
||
|
||
- 为串行终端和 VNC 转发 WebSocket Upgrade 头
|
||
- 为 AI 流式响应和控制台连接设置较长的读写超时,建议 `1h`
|
||
- 整站请求体上限至少为 `10MB`;后端会继续限制面板 API 为 `1MB`、AI 网关为 `10MB`
|
||
- 将面板内「设置 → 安全 → 真实 IP 请求头」配置为反向代理实际写入的头;系统审计、登录锁定和 IP 限速依赖它
|
||
- 配置 `PUBLIC_URL` 或面板地址,供 OAuth 回调和 OCI 日志回传链路使用
|
||
|
||
<details>
|
||
<summary>nginx 最小示例</summary>
|
||
|
||
```nginx
|
||
map $http_upgrade $connection_upgrade {
|
||
default upgrade;
|
||
"" close;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl;
|
||
server_name portal.example.com;
|
||
|
||
ssl_certificate /etc/nginx/certs/portal.example.com.crt;
|
||
ssl_certificate_key /etc/nginx/certs/portal.example.com.key;
|
||
|
||
client_max_body_size 10m;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:18888;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection $connection_upgrade;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_read_timeout 1h;
|
||
proxy_send_timeout 1h;
|
||
}
|
||
}
|
||
```
|
||
|
||
</details>
|
||
|
||
若前端静态文件与后端分离部署,`/api/*` 和 `/ai/*` 必须代理到后端,其余路径由 SPA 静态服务处理并回退到 `index.html`。Traefik 通过容器网络连接时应直达容器端口 `8080`,无需暴露宿主机端口。
|
||
|
||
## AI 网关
|
||
|
||
AI 网关使用面板创建的独立密钥鉴权,支持 `Authorization: Bearer sk-...` 和 `x-api-key: sk-...`。密钥可绑定渠道分组和模型白名单;全局模型黑名单会从模型列表、路由和探测候选中同时排除目标模型。
|
||
|
||
| 端点 | 定位 | 流式 |
|
||
| --- | --- | --- |
|
||
| `POST /ai/v1/responses` | OpenAI Responses,无状态主接口 | SSE;服务端工具除外 |
|
||
| `POST /ai/v1/chat/completions` | OpenAI Chat Completions,存量客户端兼容层 | SSE |
|
||
| `POST /ai/v1/messages` | Anthropic Messages 转换层 | SSE |
|
||
| `POST /ai/v1/embeddings` | OpenAI Embeddings | 否 |
|
||
| `GET /ai/v1/models` | 当前密钥可见的模型列表 | 否 |
|
||
|
||
兼容边界:
|
||
|
||
- 对话请求统一转发 OCI OpenAI 兼容面,当前供给以 `xai.`、`meta.`、`openai.` 前缀模型为主;Cohere Embeddings 不受该对话模型范围影响
|
||
- 网关不保存会话历史,客户端需要携带完整上下文;Responses 拒绝非空 `previous_response_id`、非 `null` `conversation` 和 `background:true`
|
||
- Responses 支持 xAI Grok `web_search` / `x_search` 服务端工具,但仅限非流式请求
|
||
- Responses 的 `reasoning.effort`、Messages 的 `output_config.effort` 和 Chat Completions 的 `reasoning_effort` 会传给上游,实际档位和效果由模型决定
|
||
- Chat Completions 只承担协议转换与兼容修复;新能力优先在 Responses 和 Messages 提供
|
||
- 单次请求最多尝试三个渠道;可重试错误会切换渠道,流式响应建立后不会换渠道重试
|
||
|
||
这里提供的是兼容接口而非 OpenAI / Anthropic 协议的完整实现。OCI OpenAI 兼容面的部分行为来自实测,未见 Oracle 文档承诺,可能随上游调整。路由与鉴权定义以 [Swagger YAML](docs/swagger.yaml) 或运行时 Swagger UI 为准;无法由 OpenAPI 完整表达的兼容边界列于上方。
|
||
|
||
### 字段兼容矩阵
|
||
|
||
以下矩阵以 2026-07-13 的 [OpenAI Responses](https://developers.openai.com/api/reference/resources/responses/methods/create)、[Chat Completions](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create)、[Embeddings](https://developers.openai.com/api/reference/resources/embeddings/methods/create) 和 [Anthropic Messages](https://platform.claude.com/docs/en/api/messages/create) 为标准基线,并与当前实现逐项核对。
|
||
|
||
这是一份兼容性快照,不替代 Swagger。标准接口和 OCI 上游都可能变化,最终行为以当前版本代码与实测为准。
|
||
|
||
| 标记 | 含义 |
|
||
| :---: | --- |
|
||
| ✅ | 网关直接支持 |
|
||
| ➡️ | 网关原样透传;是否生效由 OCI 上游决定 |
|
||
| 🔄 | 网关进行字段或协议转换后支持 |
|
||
| ◐ | 部分支持、存在前置条件或语义降级 |
|
||
| ⚠️ | 请求可被接受,但字段会被忽略 |
|
||
| ❌ | 网关在请求到达上游前拒绝 |
|
||
|
||
<details>
|
||
<summary><code>POST /ai/v1/responses</code> 对比 OpenAI Responses</summary>
|
||
|
||
Responses 是“原始 JSON 直通 + 本地门禁”。除 `store` 外,网关不会重建请求体;未知顶层字段也会保留并送往 OCI。
|
||
|
||
| 标准字段 | 状态 | 网关行为 |
|
||
| --- | :---: | --- |
|
||
| `model` | ◐ | 必须非空,还要通过密钥模型白名单并匹配可用渠道;随后原样透传 |
|
||
| `input` | ➡️ | 支持标准的字符串或 item 数组,原始内容透传;网关不逐项保证 OCI 能处理所有 item 类型 |
|
||
| `instructions`、`max_output_tokens` | ➡️ | 类型可解析后原样透传,不做范围或模型能力校验 |
|
||
| `temperature`、`top_p`、`parallel_tool_calls` | ➡️ | 原样透传,不做取值范围校验 |
|
||
| `text` / `text.format` / `text.verbosity` | ➡️ | 整个原始对象透传;结构化输出是否可用由 OCI 模型决定 |
|
||
| `reasoning` | ➡️ | 整个原始对象透传;`effort` 不校验档位,`summary` 等字段不会被网关删除 |
|
||
| `tool_choice` | ➡️ | 任意 JSON 原样透传,本地不校验枚举或结构 |
|
||
| `context_management`、`include`、`max_tool_calls`、`metadata`、`moderation`、`prompt` | ➡️ | 网关未建模但会保留在原始请求中,由 OCI 决定是否接受 |
|
||
| `prompt_cache_key`、`prompt_cache_retention`、`safety_identifier`、`service_tier` | ➡️ | 原样透传,不代表 OCI 一定实现 OpenAI 的对应语义 |
|
||
| `stream_options`、`top_logprobs`、`truncation`、`user` | ➡️ | 原样透传,不做本地语义校验 |
|
||
| `store` | 🔄 | 无论客户端传什么,上游请求都强制改写为 `false` |
|
||
| `previous_response_id` | ❌ | 非空即返回 400;网关不保存历史响应 |
|
||
| `conversation` | ◐ | 省略或 `null` 可通过;任何非 `null` 值返回 400 |
|
||
| `background` | ◐ | `true` 返回 400;`false`、`null` 或省略时继续透传 |
|
||
| `stream` | ◐ | 普通请求和 `function` 工具支持 SSE;含 `web_search` / `x_search` 时拒绝流式 |
|
||
| `tools[].type=function` | ➡️ | 非流式和流式均可,工具对象原样透传 |
|
||
| `tools[].type=web_search` / `x_search` | ◐ | 使用 xAI 服务端工具语义且仅支持非流式;`x_search` 不是 OpenAI 标准工具 |
|
||
| 其他标准工具类型 | ❌ | `web_search_preview`、`file_search`、`computer`、`code_interpreter`、`image_generation`、`mcp`、`shell`、`custom` 等返回 400 |
|
||
| 其他未知顶层字段 | ➡️ | 只要整个请求是合法 JSON,字段名、结构和数值都会保留 |
|
||
|
||
响应边界:
|
||
|
||
- 非流式成功响应不做转换,OCI JSON 原样返回;usage 解析只用于面板调用日志
|
||
- SSE 事件逐行原样转发,不补 `data: [DONE]`,推理增量也不会被网关过滤
|
||
- 流建立前最多切换三个渠道;流建立后中断不重试,客户端可能只收到部分事件
|
||
- 未知模型返回 404、无渠道返回 503;上游错误会套入 OpenAI 风格错误体,不保证与标准 OpenAI 错误字段完全相同
|
||
|
||
实现依据:[`airesponses.go`](internal/service/airesponses.go) · [`responses.go`](internal/aiwire/responses.go) · [`aigateway.go`](internal/api/aigateway.go)
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary><code>POST /ai/v1/chat/completions</code> 对比 OpenAI Chat Completions</summary>
|
||
|
||
Chat Completions 会先转换为 Responses 请求,再把 OCI Responses 响应桥接回 Chat Completions 形态。
|
||
|
||
| 标准字段 | 状态 | 网关行为 |
|
||
| --- | :---: | --- |
|
||
| `model` | ◐ | 必填,受密钥白名单和可用渠道限制;模型名保留到上游请求 |
|
||
| `messages` | 🔄 | 必填并转换为 Responses `input` / `instructions` |
|
||
| `system` / `developer` 消息 | ◐ | 文本按出现顺序合并为 `instructions`;块数组中的非文本内容被忽略 |
|
||
| `user` / `assistant` 文本内容 | 🔄 | 字符串及 `text` 块分别转为 `input_text` / `output_text` |
|
||
| `image_url` 内容块 | ◐ | URL 或 data URI 转为 `input_image`;`image_url.detail` 被忽略 |
|
||
| `input_audio`、`file`、`refusal` 等内容块 | ❌ | 当前消息转换器不支持,返回 400 |
|
||
| assistant `tool_calls` / `role=tool` | 🔄 | 转为 `function_call` / `function_call_output`,保留调用 ID、函数名和参数 |
|
||
| 消息 `name`、`refusal`、`audio`、旧式 `function_call` | ⚠️ | 当前消息结构未建模,静默忽略 |
|
||
| `max_completion_tokens` | 🔄 | 转为 `max_output_tokens`,优先于 `max_tokens` |
|
||
| `max_tokens` | 🔄 | 未提供 `max_completion_tokens` 时转为 `max_output_tokens` |
|
||
| `temperature`、`top_p`、`parallel_tool_calls` | ◐ | 原值写入 Responses 请求,但不校验范围或模型能力 |
|
||
| `stream` | 🔄 | OCI Responses SSE 桥接为 `chat.completion.chunk`,末尾补 `data: [DONE]` |
|
||
| `stream_options.include_usage` | 🔄 | 控制网关在终块后追加 `choices: []` 的 usage 块 |
|
||
| `stream_options.include_obfuscation` | ⚠️ | 未建模,静默忽略 |
|
||
| `tools[].type=function` | ◐ | `name`、`description`、`parameters` 支持;`function.strict` 被忽略 |
|
||
| `tools[].type=custom` 及其他工具 | ❌ | 只接受 `function`,其他类型返回 400 |
|
||
| `tool_choice` | ◐ | 支持 `auto` / `none` / `required` 和具名 function;非法或未知值被静默忽略 |
|
||
| `response_format` | ◐ | `json_object`、`json_schema` 转为 Responses `text.format`;未知类型交给 OCI 处理 |
|
||
| `reasoning_effort` | ◐ | 转小写后映射为 `reasoning.effort`,不校验模型或档位 |
|
||
| `store` | ⚠️ / 🔄 | 客户端字段被忽略,转换后的上游请求始终使用 `store:false` |
|
||
| `stop`、`seed`、`n`、`frequency_penalty`、`presence_penalty` | ⚠️ | 静默忽略;`n` 因此恒为单个 choice |
|
||
| `logprobs`、`top_logprobs`、`logit_bias`、`user` | ⚠️ | 静默忽略 |
|
||
| `audio`、`modalities`、`prediction`、`metadata`、`moderation` | ⚠️ | 静默忽略 |
|
||
| `prompt_cache_key`、`safety_identifier`、`service_tier`、`verbosity`、`web_search_options` | ⚠️ | 静默忽略 |
|
||
| 其他未知字段 | ⚠️ | JSON 解码器不会拒绝未知字段,但转换后的上游请求不包含它们 |
|
||
|
||
响应边界:
|
||
|
||
- 非流式固定生成一个 `choices[0]`;文本会合并,函数调用转为 `tool_calls`
|
||
- `finish_reason` 只生成 `stop`、`tool_calls`、`length`;其他上游终止原因不保留
|
||
- reasoning 输出、logprobs、refusal、annotations、audio、service tier 和 system fingerprint 不返回
|
||
- usage 保留 `prompt_tokens`、`completion_tokens`、`total_tokens` 和 `prompt_tokens_details.cached_tokens`
|
||
- 已建立的流中断或上游 `response.failed` 不会转换成标准 SSE 错误事件
|
||
|
||
实现依据:[`chatresponses.go`](internal/service/chatresponses.go) · [`openai.go`](internal/aiwire/openai.go) · [`aigateway.go`](internal/api/aigateway.go)
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary><code>POST /ai/v1/embeddings</code> 对比 OpenAI Embeddings</summary>
|
||
|
||
| 标准字段 | 状态 | 网关行为 |
|
||
| --- | :---: | --- |
|
||
| `model` | ◐ | 必填,受密钥白名单限制,并且必须存在具有 `EMBEDDING` 能力的渠道 |
|
||
| `input` 为字符串 | ✅ | 包装为单个输入后调用 OCI |
|
||
| `input` 为字符串数组 | ✅ | 按原顺序调用 OCI |
|
||
| `input` 为 token ID 数组或二维 token ID 数组 | ❌ | 只能解码字符串或字符串数组,绑定阶段返回 400 |
|
||
| 空数组 / `null` | ❌ | 本地返回 400;空字符串不在本地拒绝,由 OCI 决定 |
|
||
| `dimensions` | ◐ | 映射为 OCI 输出维度,不做范围或模型能力校验 |
|
||
| `encoding_format=float` | ✅ | 返回 float 数组;省略时行为相同 |
|
||
| `encoding_format=base64` | ❌ | 本地返回 400,不提供 base64 响应 |
|
||
| `user` | ⚠️ | 能解析但不会传给 OCI |
|
||
| 其他未知字段 | ⚠️ | 静默忽略 |
|
||
|
||
响应使用标准的 `object:"list"`、`data[].object:"embedding"`、`index`、`model` 和可选 `usage` 外壳;向量为 `float32` 数组,不支持流式。
|
||
|
||
实现依据:[`embeddings.go`](internal/aiwire/embeddings.go) · [`aigateway_chat.go`](internal/service/aigateway_chat.go) · [`aigateway.go`](internal/api/aigateway.go)
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary><code>POST /ai/v1/messages</code> 对比 Anthropic Messages</summary>
|
||
|
||
网关接受 `Authorization: Bearer` 或 `x-api-key`,但不会校验或使用标准 Anthropic `anthropic-version`、`anthropic-beta` 请求头。
|
||
|
||
| 标准字段 | 状态 | 网关行为 |
|
||
| --- | :---: | --- |
|
||
| `model` | ◐ | 用于模型与渠道选择,但空字符串不会在 handler 中按参数错误拒绝,通常最终返回模型不存在 |
|
||
| `max_tokens` | 🔄 | 必填且必须大于 0,转换为 `max_output_tokens` |
|
||
| `messages` | ◐ | 必须非空;角色、交替顺序和空内容不做完整校验 |
|
||
| `system` | ◐ | 支持字符串或 text 块数组;多个文本块直接拼接,`cache_control` 等附加字段被忽略 |
|
||
| `temperature`、`top_p` | ◐ | 写入 Responses 请求,不做取值范围或模型能力校验 |
|
||
| `top_k` | ⚠️ | 能解析但不会传给上游 |
|
||
| `stop_sequences` | ⚠️ | 能解析但不会传给上游;响应 `stop_sequence` 恒为 `null` |
|
||
| `stream` | 🔄 | Responses SSE 桥接为 Anthropic 事件序列 |
|
||
| `tools` | ◐ | 每个工具都转换成 Responses `function`;自定义客户端工具可用,Anthropic 服务端工具类型不保留原语义 |
|
||
| `tool_choice` | ◐ | 支持 `auto`、`any`、`none`、具名 `tool`;`disable_parallel_tool_use` 等附加字段被忽略 |
|
||
| `metadata` | ⚠️ | 能解析但不会传给上游 |
|
||
| `thinking` | ⚠️ | 顶层 thinking 配置不会控制上游思考预算 |
|
||
| `output_config.effort` | 🔄 | 转小写后映射为 Responses `reasoning.effort` |
|
||
| `output_config` 其他子字段 | ⚠️ | 未建模,静默忽略 |
|
||
| `cache_control`、`container`、`inference_geo`、`service_tier` | ⚠️ | 标准 SDK 中存在,但当前请求结构未建模,静默忽略 |
|
||
| 其他未知顶层字段 | ⚠️ | JSON 解码器接受,但转换后的上游请求不包含它们 |
|
||
|
||
`messages[].content`:
|
||
|
||
| 标准内容块 | 状态 | 网关行为 |
|
||
| --- | :---: | --- |
|
||
| 字符串 / `text` | 🔄 | user 转 `input_text`,assistant 历史转 `output_text` |
|
||
| `image` | ◐ | 仅支持 `base64` 和 `url` source;缺字段或其他 source 类型返回 400 |
|
||
| `tool_use` | 🔄 | 转为 `function_call`,保留 ID、名称和输入 |
|
||
| `tool_result` | ◐ | 转为 `function_call_output`;块数组只拼接 text,`is_error` 和非文本结果丢失 |
|
||
| `thinking` / `redacted_thinking` | ⚠️ | 历史思考块被删除,不进入上游上下文 |
|
||
| `document`、服务端工具结果及其他未知块 | ❌ | 返回 400 |
|
||
| `null` / 空块数组 | ◐ | 该消息可能从上游 input 中消失,不返回参数错误 |
|
||
|
||
响应边界:
|
||
|
||
- 非流式只把 Responses `output_text` 转成 `text`、`function_call` 转成 `tool_use`
|
||
- `stop_reason` 只生成 `end_turn`、`tool_use`、`max_tokens`;`stop_sequence` 恒为 `null`
|
||
- reasoning 不会生成 Anthropic `thinking` / `redacted_thinking` 块,也没有 signature
|
||
- usage 只保留 `input_tokens`、`output_tokens` 和 `cache_read_input_tokens`,不提供 `cache_creation_input_tokens`
|
||
- 流式输出标准事件骨架,但不生成 `thinking_delta`、`signature_delta` 或上游失败对应的 Anthropic `error` 事件
|
||
|
||
实现依据:[`anthresponses.go`](internal/service/anthresponses.go) · [`anthropic.go`](internal/aiwire/anthropic.go) · [`aigateway.go`](internal/api/aigateway.go)
|
||
|
||
</details>
|
||
|
||
`GET /ai/v1/models` 使用 OpenAI Models 列表外壳(`object`、`data[].id/object/created/owned_by`),但只返回当前渠道目录中通过分组、全局黑名单和密钥白名单筛选后的模型;网关不提供标准的单模型检索端点。
|
||
|
||
## API 与配置
|
||
|
||
### API 文档
|
||
|
||
| 路径 | 鉴权 | 用途 |
|
||
| --- | --- | --- |
|
||
| `/api/v1/*` | 登录返回的 JWT Bearer Token | 面板管理 API |
|
||
| `/ai/v1/*` | AI 网关密钥 | OpenAI / Anthropic 兼容接口 |
|
||
| `/api/v1/webhooks/oci-logs/:secret` | URL 中的独立回传密钥 | OCI Notifications 日志回传 |
|
||
|
||
OpenAPI 文件随仓库维护:[`docs/swagger.yaml`](docs/swagger.yaml) · [`docs/swagger.json`](docs/swagger.json)。
|
||
|
||
运行进程时设置 `SWAGGER=1` 可开放 `/swagger/index.html`。Swagger 默认关闭,生产环境建议仅在受控网络内按需开启。接口注释变更后必须重新生成 OpenAPI。
|
||
|
||
### 环境变量
|
||
|
||
| 变量 | 必填 | 默认值 | 说明 |
|
||
| --- | :---: | --- | --- |
|
||
| `DATA_KEY` | 是 | — | 敏感字段加密主密钥;必须持久保存,不能随意轮换 |
|
||
| `JWT_SECRET` | 是 | — | JWT 签名密钥;更换会使已有登录令牌失效 |
|
||
| `ADMIN_USERNAME` | 否 | `admin` | 初始管理员用户名 |
|
||
| `ADMIN_PASSWORD` | 首次启动 | — | 仅在数据库无用户时创建管理员,不会重置已有密码 |
|
||
| `ADDR` | 否 | `:8080` | HTTP 监听地址 |
|
||
| `DB_DRIVER` | 否 | `sqlite` | `sqlite` / `mysql` / `postgres`;后两者为 experimental |
|
||
| `DB_PATH` | SQLite | `oci-portal.db` | SQLite 文件路径 |
|
||
| `DB_DSN` | 外部数据库 | — | MySQL 需 `parseTime=True`;不要在日志或文档中暴露凭据 |
|
||
| `PUBLIC_URL` | 否 | — | 面板公网基址,作为 OAuth 回调和日志回传引导的回退值 |
|
||
| `TZ` | 否 | 系统时区 | cron 表达式的解释时区;容器示例使用 `Asia/Shanghai` |
|
||
| `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | 否 | — | Go 标准出站代理变量;面板内显式代理配置优先用于对应业务 |
|
||
| `SWAGGER` | 否 | 关闭 | 设为 `1` 时开放 Swagger UI |
|
||
| `GIN_MODE` | 否 | `release` | `debug` / `release` |
|
||
|
||
## 升级与备份
|
||
|
||
- 升级前同时备份 `.env` 和数据库;SQLite Compose 部署的数据文件为 `data/oci-portal.db`
|
||
- 保持 `DATA_KEY` 不变;只恢复数据库而没有原密钥,敏感字段无法解密
|
||
- 服务启动时会自动执行数据库迁移;跨版本升级前先阅读 [CHANGELOG](CHANGELOG.md)
|
||
- Compose 部署使用 `docker compose pull && docker compose up -d` 更新镜像
|
||
- OCI API Key 应遵循最小权限原则;生产环境保持 Swagger 关闭并限制管理面访问来源
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
gofmt -l .
|
||
go vet ./...
|
||
go test ./...
|
||
|
||
# Handler 注释变更后重新生成唯一的对外 API 文档。
|
||
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency
|
||
```
|
||
|
||
- Go 后端规范与目录约定:[`AGENTS.md`](AGENTS.md) · [`.trellis/spec/backend/`](.trellis/spec/backend/)
|
||
- 前端开发与构建:[oci-portal-dash](https://github.com/wangdefaa/oci-portal-dash)
|
||
- 版本变更:[`CHANGELOG.md`](CHANGELOG.md)
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE)
|