wangdefa c7cc5616ed
CI / test (push) Successful in 31s
Release / release (push) Successful in 48s
发布 0.3.0:恢复 Chat 接口并增强云端事件通知
2026-07-13 10:13:44 +08:00

OCI Portal logo

OCI Portal

自托管的 OCI 多租户管理面板与 GenAI 兼容网关

Release Go Docker License

快速开始 · 核心能力 · 生产部署 · AI 网关 · 开发

OCI Portal 将多份 OCI API Key、云资源、自动化任务、审计事件和 OCI GenAI 渠道集中到一个管理界面。Vue 前端通过 go:embed 嵌入 Go 服务,Release 以单个二进制和多架构容器镜像交付。

本仓库为后端与发行仓库;前端源码位于 oci-portal-dash

界面预览

总览 登录
总览 登录
租户 任务
租户 任务
AI 网关 通知设置
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 / PostgreSQLexperimental │
└─────────────────────────────────────────────────────────────────┘

默认推荐 SQLite 单实例部署。MySQL 和 PostgreSQL 适配仍属 experimental,不应据此推断服务支持多副本并发运行。

快速开始

Docker Compose(推荐)

前置条件:Docker、Docker Compose v2、OpenSSL。

  1. 克隆仓库并生成一份需要长期保存的 .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
    
  2. 准备数据目录并启动:

    mkdir -p data
    
    # Linux bind mount 需要让镜像内的 nonroot 用户(uid 65532)可写。
    sudo chown 65532:65532 data
    
    docker compose up -d
    docker compose ps
    
  3. 查看初始管理员密码并登录:

    grep '^ADMIN_PASSWORD=' .env
    

    访问 http://127.0.0.1:18888,默认用户名为 admin

DATA_KEY 用于解密数据库中的 OCI 私钥、口令和渠道凭据。它只能生成一次并持续复用;丢失或更换后,已有密文无法恢复。请将 .envdata/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:8080ADMIN_PASSWORD 只在数据库中没有用户时创建初始管理员,后续启动不会用它重置密码。

源码构建

源码构建要求 Go 1.26.5、curlunzipsha256sum。仓库只保留前端占位页,编译完整单文件前应下载 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、非 null conversationbackground: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 或运行时 Swagger UI 为准;无法由 OpenAPI 完整表达的兼容边界列于上方。

字段兼容矩阵

以下矩阵以 2026-07-13 的 OpenAI ResponsesChat CompletionsEmbeddingsAnthropic Messages 为标准基线,并与当前实现逐项核对。

这是一份兼容性快照,不替代 Swagger。标准接口和 OCI 上游都可能变化,最终行为以当前版本代码与实测为准。

标记 含义
网关直接支持
➡️ 网关原样透传;是否生效由 OCI 上游决定
🔄 网关进行字段或协议转换后支持
部分支持、存在前置条件或语义降级
⚠️ 请求可被接受,但字段会被忽略
网关在请求到达上游前拒绝
POST /ai/v1/responses 对比 OpenAI Responses

Responses 是“原始 JSON 直通 + 本地门禁”。除 store 外,网关不会重建请求体;未知顶层字段也会保留并送往 OCI。

标准字段 状态 网关行为
model 必须非空,还要通过密钥模型白名单并匹配可用渠道;随后原样透传
input ➡️ 支持标准的字符串或 item 数组,原始内容透传;网关不逐项保证 OCI 能处理所有 item 类型
instructionsmax_output_tokens ➡️ 类型可解析后原样透传,不做范围或模型能力校验
temperaturetop_pparallel_tool_calls ➡️ 原样透传,不做取值范围校验
text / text.format / text.verbosity ➡️ 整个原始对象透传;结构化输出是否可用由 OCI 模型决定
reasoning ➡️ 整个原始对象透传;effort 不校验档位,summary 等字段不会被网关删除
tool_choice ➡️ 任意 JSON 原样透传,本地不校验枚举或结构
context_managementincludemax_tool_callsmetadatamoderationprompt ➡️ 网关未建模但会保留在原始请求中,由 OCI 决定是否接受
prompt_cache_keyprompt_cache_retentionsafety_identifierservice_tier ➡️ 原样透传,不代表 OCI 一定实现 OpenAI 的对应语义
stream_optionstop_logprobstruncationuser ➡️ 原样透传,不做本地语义校验
store 🔄 无论客户端传什么,上游请求都强制改写为 false
previous_response_id 非空即返回 400;网关不保存历史响应
conversation 省略或 null 可通过;任何非 null 值返回 400
background true 返回 400falsenull 或省略时继续透传
stream 普通请求和 function 工具支持 SSE;含 web_search / x_search 时拒绝流式
tools[].type=function ➡️ 非流式和流式均可,工具对象原样透传
tools[].type=web_search / x_search 使用 xAI 服务端工具语义且仅支持非流式;x_search 不是 OpenAI 标准工具
其他标准工具类型 web_search_previewfile_searchcomputercode_interpreterimage_generationmcpshellcustom 等返回 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_imageimage_url.detail 被忽略
input_audiofilerefusal 等内容块 当前消息转换器不支持,返回 400
assistant tool_calls / role=tool 🔄 转为 function_call / function_call_output,保留调用 ID、函数名和参数
消息 namerefusalaudio、旧式 function_call ⚠️ 当前消息结构未建模,静默忽略
max_completion_tokens 🔄 转为 max_output_tokens,优先于 max_tokens
max_tokens 🔄 未提供 max_completion_tokens 时转为 max_output_tokens
temperaturetop_pparallel_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 namedescriptionparameters 支持;function.strict 被忽略
tools[].type=custom 及其他工具 只接受 function,其他类型返回 400
tool_choice 支持 auto / none / required 和具名 function;非法或未知值被静默忽略
response_format json_objectjson_schema 转为 Responses text.format;未知类型交给 OCI 处理
reasoning_effort 转小写后映射为 reasoning.effort,不校验模型或档位
store ⚠️ / 🔄 客户端字段被忽略,转换后的上游请求始终使用 store:false
stopseednfrequency_penaltypresence_penalty ⚠️ 静默忽略;n 因此恒为单个 choice
logprobstop_logprobslogit_biasuser ⚠️ 静默忽略
audiomodalitiespredictionmetadatamoderation ⚠️ 静默忽略
prompt_cache_keysafety_identifierservice_tierverbosityweb_search_options ⚠️ 静默忽略
其他未知字段 ⚠️ JSON 解码器不会拒绝未知字段,但转换后的上游请求不包含它们

响应边界:

  • 非流式固定生成一个 choices[0];文本会合并,函数调用转为 tool_calls
  • finish_reason 只生成 stoptool_callslength;其他上游终止原因不保留
  • reasoning 输出、logprobs、refusal、annotations、audio、service tier 和 system fingerprint 不返回
  • usage 保留 prompt_tokenscompletion_tokenstotal_tokensprompt_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"indexmodel 和可选 usage 外壳;向量为 float32 数组,不支持流式。

实现依据:embeddings.go · aigateway_chat.go · aigateway.go

POST /ai/v1/messages 对比 Anthropic Messages

网关接受 Authorization: Bearerx-api-key,但不会校验或使用标准 Anthropic anthropic-versionanthropic-beta 请求头。

标准字段 状态 网关行为
model 用于模型与渠道选择,但空字符串不会在 handler 中按参数错误拒绝,通常最终返回模型不存在
max_tokens 🔄 必填且必须大于 0,转换为 max_output_tokens
messages 必须非空;角色、交替顺序和空内容不做完整校验
system 支持字符串或 text 块数组;多个文本块直接拼接,cache_control 等附加字段被忽略
temperaturetop_p 写入 Responses 请求,不做取值范围或模型能力校验
top_k ⚠️ 能解析但不会传给上游
stop_sequences ⚠️ 能解析但不会传给上游;响应 stop_sequence 恒为 null
stream 🔄 Responses SSE 桥接为 Anthropic 事件序列
tools 每个工具都转换成 Responses function;自定义客户端工具可用,Anthropic 服务端工具类型不保留原语义
tool_choice 支持 autoanynone、具名 tooldisable_parallel_tool_use 等附加字段被忽略
metadata ⚠️ 能解析但不会传给上游
thinking ⚠️ 顶层 thinking 配置不会控制上游思考预算
output_config.effort 🔄 转小写后映射为 Responses reasoning.effort
output_config 其他子字段 ⚠️ 未建模,静默忽略
cache_controlcontainerinference_geoservice_tier ⚠️ 标准 SDK 中存在,但当前请求结构未建模,静默忽略
其他未知顶层字段 ⚠️ JSON 解码器接受,但转换后的上游请求不包含它们

messages[].content

标准内容块 状态 网关行为
字符串 / text 🔄 user 转 input_textassistant 历史转 output_text
image 仅支持 base64url source;缺字段或其他 source 类型返回 400
tool_use 🔄 转为 function_call,保留 ID、名称和输入
tool_result 转为 function_call_output;块数组只拼接 textis_error 和非文本结果丢失
thinking / redacted_thinking ⚠️ 历史思考块被删除,不进入上游上下文
document、服务端工具结果及其他未知块 返回 400
null / 空块数组 该消息可能从上游 input 中消失,不返回参数错误

响应边界:

  • 非流式只把 Responses output_text 转成 textfunction_call 转成 tool_use
  • stop_reason 只生成 end_turntool_usemax_tokensstop_sequence 恒为 null
  • reasoning 不会生成 Anthropic thinking / redacted_thinking 块,也没有 signature
  • usage 只保留 input_tokensoutput_tokenscache_read_input_tokens,不提供 cache_creation_input_tokens
  • 流式输出标准事件骨架,但不生成 thinking_deltasignature_delta 或上游失败对应的 Anthropic error 事件

实现依据:anthresponses.go · anthropic.go · aigateway.go

GET /ai/v1/models 使用 OpenAI Models 列表外壳(objectdata[].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

License

MIT

S
Description
No description provided
Readme MIT
21 MiB
v0.8.3
Latest
2026-07-30 12:44:14 +08:00
Languages
Go 99.9%
Dockerfile 0.1%