OCI Portal 将多份 OCI API Key、云资源、自动化任务、审计事件和 OCI GenAI 渠道集中到一个管理界面。Vue 前端通过 go:embed 嵌入 Go 服务,Release 以单个二进制和多架构容器镜像交付。
本仓库为后端与发行仓库;前端源码位于 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 兼容接口,支持渠道分组、加权路由、熔断探测、模型黑白名单、密钥管理和调用日志
运行形态
浏览器 / 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。
-
克隆仓库并生成一份需要长期保存的
.env: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 -
准备数据目录并启动:
mkdir -p data # Linux bind mount 需要让镜像内的 nonroot 用户(uid 65532)可写。 sudo chown 65532:65532 data docker compose up -d docker compose ps -
查看初始管理员密码并登录:
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:
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 指定的前端产物并校验:
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
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 日志回传链路使用
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;
}
}
若前端静态文件与后端分离部署,/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、非nullconversation和background:true - Responses 支持 xAI Grok
web_search/x_search服务端工具,但仅限非流式请求 - Responses 的
reasoning.effort、Messages 的output_config.effort和 Chat Completions 的reasoning_effort会传给上游,实际档位和效果由模型决定 - Chat Completions 只承担协议转换与兼容修复;新能力优先在 Responses 和 Messages 提供
- 单次请求最多尝试三个渠道;可重试错误会切换渠道,流式响应建立后不会换渠道重试
已知上游限制:大 system 区流式断流
实测(2026-07-13)OCI 兼容面对 instructions 与 tools 合计超约 64.5KB 的流式请求会在发出少量事件后静默断开连接(无任何错误事件;同请求非流式正常),与模型、字符集、消息正文大小均无关——消息正文(input)不计入该限制。Chat Completions 与 Messages 的 system/developer 提示会转换为 instructions,因此 Claude Code 等自带大体量系统提示与工具定义的客户端极易触发。
网关侧应对:
- Messages 与 Chat Completions 的流式请求在客户端尚未收到任何输出时遭遇上游断流,会自动降级为非流式重做,并按标准事件/chunk 序列一次推送;调用日志记
retries=1与降级标记 - Responses 直通因初始事件已转发、协议上无法透明降级,调用日志记「上游流提前终止」,客户端需自行回退非流式
- 应急规避:将超长 system 内容移入首条 user 消息正文可绕过该限制(正文不计入),但语义有别,根治有待上游修复
这里提供的是兼容接口而非 OpenAI / Anthropic 协议的完整实现。OCI OpenAI 兼容面的部分行为来自实测,未见 Oracle 文档承诺,可能随上游调整。路由与鉴权定义以 Swagger YAML 或运行时 Swagger UI 为准;无法由 OpenAPI 完整表达的兼容边界列于上方。
字段兼容矩阵
以下矩阵以 2026-07-13 的 OpenAI Responses、Chat Completions、Embeddings 和 Anthropic Messages 为标准基线,并与当前实现逐项核对。
这是一份兼容性快照,不替代 Swagger。标准接口和 OCI 上游都可能变化,最终行为以当前版本代码与实测为准。
| 标记 | 含义 |
|---|---|
| ✅ | 网关直接支持 |
| ➡️ | 网关原样透传;是否生效由 OCI 上游决定 |
| 🔄 | 网关进行字段或协议转换后支持 |
| ◐ | 部分支持、存在前置条件或语义降级 |
| ⚠️ | 请求可被接受,但字段会被忽略 |
| ❌ | 网关在请求到达上游前拒绝 |
POST /ai/v1/responses 对比 OpenAI Responses
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 · responses.go · aigateway.go
POST /ai/v1/chat/completions 对比 OpenAI Chat Completions
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];上游断流且尚无输出时自动降级非流式重做,结果按 chunk 序列一次推送 |
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 · openai.go · aigateway.go
POST /ai/v1/embeddings 对比 OpenAI Embeddings
| 标准字段 | 状态 | 网关行为 |
|---|---|---|
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 · aigateway_chat.go · aigateway.go
POST /ai/v1/messages 对比 Anthropic Messages
网关接受 Authorization: Bearer 或 x-api-key,但不会校验或使用标准 Anthropic anthropic-version、anthropic-beta 请求头。
| 标准字段 | 状态 | 网关行为 |
|---|---|---|
model |
◐ | 用于模型与渠道选择,但空字符串不会在 handler 中按参数错误拒绝,通常最终返回模型不存在 |
max_tokens |
🔄 | 可缺省(缺省或 ≤0 时按默认值 8192),转换为 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;上游错误事件与无终态断流会转成 Anthropicerror事件
实现依据:anthresponses.go · anthropic.go · aigateway.go
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.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
- Compose 部署使用
docker compose pull && docker compose up -d更新镜像 - OCI API Key 应遵循最小权限原则;生产环境保持 Swagger 关闭并限制管理面访问来源
开发
gofmt -l .
go vet ./...
go test ./...
# Handler 注释变更后重新生成唯一的对外 API 文档。
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency
- Go 后端规范与目录约定:
AGENTS.md·.trellis/spec/backend/ - 前端开发与构建:oci-portal-dash
- 版本变更:
CHANGELOG.md





