265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
<div align="center">
|
||
|
||
<img src="docs/assets/logo.svg" width="104" alt="OCI Portal logo">
|
||
|
||
# OCI Portal
|
||
|
||
**Oracle Cloud Infrastructure 多租户管理面板**
|
||
|
||

|
||

|
||

|
||
|
||
本仓库为后端;前端工程见 [oci-portal-dash](https://github.com/wangdefaa/oci-portal-dash)(构建产物嵌入本服务成单文件)
|
||
|
||
</div>
|
||
|
||
## 界面预览
|
||
|
||
| 总览 | 登录 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
| 租户 | 任务 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
| AI 网关 | 通知设置 |
|
||
| --- | --- |
|
||
|  |  |
|
||
|
||
## 特性
|
||
|
||
- **多租户管理**:多份 OCI API Key 配置集中管理,私钥/口令 AES-256-GCM 加密落库;分组、批量测活、账户类型与订阅信息识别
|
||
- **资源管理**:实例(含创建/电源操作/更换公网 IP/IPv6/VNIC/引导卷)、VCN/子网/安全列表、块存储、限额与成本查询,多区域/多区间支持
|
||
- **抢机任务**:cron 周期尝试创建实例直到成功,支持熔断与通知
|
||
- **网页控制台**:实例串行控制台(xterm)与 VNC(noVNC),两跳 SSH 隧道
|
||
- **AI 网关**:OpenAI Responses / Anthropic Messages / Embeddings 兼容端点转发 OCI GenAI,号池渠道加权负载均衡、熔断探测、密钥管理与用量日志;对话统一走 OCI OpenAI 兼容面(xai / meta / openai 厂商模型,流式事件与推理增量原样保真),支持 xAI Grok 服务端工具 `web_search` / `x_search`(格式同 xAI 官方 Agent Tools,仅非流式)——兼容面属实测可用但未见 Oracle 文档承诺的能力,行为可能随上游调整
|
||
- **日志回传**:OCI Audit 事件经 Connector Hub → Notifications HTTPS 订阅回传入库,一键创建链路;自定义告警规则(事件类型/来源 IP 白名单/资源/频率阈值)命中即推送
|
||
- **租户治理**:IAM 用户/MFA/API Key 管理、密码策略、身份提供商(SAML)、通知收件人;多 Identity Domain 租户可按域切换管理
|
||
- **安全**:JWT + bcrypt(凭据变更旧令牌立即失效,可一键撤销全部会话)、TOTP 两步验证、OIDC/GitHub 外部登录、登录锁定、IP 限速、真实 IP 头可配、请求体/超时防护、系统操作审计
|
||
- **通知**:模板化推送,五渠道并存(Telegram / Webhook / ntfy / Bark / SMTP),Webhook 可对接飞书、钉钉、Slack、企业微信机器人
|
||
|
||
## 快速开始
|
||
|
||
### 二进制运行
|
||
|
||
从 Release 下载对应架构的二进制后:
|
||
|
||
```bash
|
||
DATA_KEY=$(openssl rand -hex 32) JWT_SECRET=$(openssl rand -hex 32) ADMIN_PASSWORD=<初始密码> ./oci-portal-server
|
||
```
|
||
|
||
访问 `http://localhost:8080`,用 `admin` 与初始密码登录。
|
||
|
||
### Docker Compose
|
||
|
||
```bash
|
||
# 镜像以 distroless nonroot(uid 65532)运行,数据卷需可写,否则 SQLite 报 unable to open database file
|
||
mkdir -p ./data && sudo chown 65532:65532 ./data
|
||
DATA_KEY=$(openssl rand -hex 32) JWT_SECRET=$(openssl rand -hex 32) ADMIN_PASSWORD=<初始密码> docker compose up -d
|
||
```
|
||
|
||
默认只监听 `127.0.0.1:18888`;公网访问须置于 TLS 反向代理之后,见下文「反向代理」。
|
||
|
||
### 源码构建(单文件,含前端)
|
||
|
||
```bash
|
||
# 1. 获取前端产物:从前端仓库 Release 下载 dist.zip(或本地 npm run build 后拷入)
|
||
curl -fL -o dist.zip https://github.com/wangdefaa/oci-portal-dash/releases/latest/download/dist.zip
|
||
rm -rf internal/webui/dist && mkdir -p internal/webui/dist && unzip -q dist.zip -d internal/webui/dist
|
||
# 2. 编译(免 CGO,可交叉编译;-X 两项注入「设置·关于」页的版本与构建时间,可省略,省略则显示 dev)
|
||
CGO_ENABLED=0 go build -trimpath \
|
||
-ldflags "-s -w -X oci-portal/internal/api.buildVersion=v0.0.1 -X oci-portal/internal/api.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||
-o bin/oci-portal-server ./cmd/server
|
||
```
|
||
|
||
> 注意:上述解压会覆盖仓库占位文件 `internal/webui/dist/index.html`,提交代码前勿把真实产物带入版本库。
|
||
|
||
### 反向代理(公网部署必读)
|
||
|
||
面板自身只提供 HTTP,管理员口令、JWT 与租户 API 私钥都会经明文承载。除本机试用外,应让面板仅监听回环地址(compose 示例已默认 `127.0.0.1:18888`),由支持 TLS 的反向代理对外提供 HTTPS。
|
||
|
||
Caddy 最小示例(整站反代,自动申请并续期 Let's Encrypt 证书,WebSocket 自动透传):
|
||
|
||
```caddyfile
|
||
portal.example.com {
|
||
reverse_proxy 127.0.0.1:18888
|
||
}
|
||
```
|
||
|
||
nginx 示例(证书自备;Web Console 的 WebSocket 升级与长超时必须显式配置,否则串行终端连不上或空闲即断):
|
||
|
||
```nginx
|
||
# http 块内:按请求是否升级为 WebSocket 决定 Connection 头
|
||
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;
|
||
|
||
# 放行到最大入口(AI 网关 10MB);各入口更细的上限由后端自身执行
|
||
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;
|
||
# WebSocket 空闲与 AI 流式响应都需要长读超时(默认 60s 会断)
|
||
proxy_read_timeout 1h;
|
||
proxy_send_timeout 1h;
|
||
}
|
||
}
|
||
```
|
||
|
||
Traefik 示例(已运行 Traefik 的 Docker 环境,面板容器加 label 接入即可,WebSocket 透明支持):
|
||
|
||
```yaml
|
||
services:
|
||
oci-portal:
|
||
image: ghcr.io/wangdefaa/oci-portal:latest
|
||
# ……环境变量与数据卷同上文 compose 示例;与 Traefik 同一 docker 网络,
|
||
# Traefik 经容器网络直达 8080,无需(也不应)对外映射 ports
|
||
labels:
|
||
- traefik.enable=true
|
||
- traefik.http.routers.oci-portal.rule=Host(`portal.example.com`)
|
||
- traefik.http.routers.oci-portal.entrypoints=websecure
|
||
- traefik.http.routers.oci-portal.tls.certresolver=le # 换成你的 certResolver 名
|
||
- traefik.http.services.oci-portal.loadbalancer.server.port=8080
|
||
```
|
||
|
||
若前端静态文件由反代直接伺服(不走内嵌页面),则按前缀代理,缺一不可:
|
||
|
||
| 前缀 | 内容 | 何时需要 |
|
||
| --- | --- | --- |
|
||
| `/api/*` | 面板 REST、Web Console WebSocket、日志回传 webhook | 始终 |
|
||
| `/ai/*` | AI 网关(OpenAI / Claude 兼容端点,独立密钥鉴权) | 启用 AI 网关时 |
|
||
| 其余路径 | 前端 SPA 静态文件(404 回退 `index.html`) | 静态分离形态 |
|
||
|
||
- 反代默认追加的 `X-Forwarded-For` 用于还原真实客户端 IP,系统日志留痕、登录锁定与 IP 限速都依赖它
|
||
- 请求体上限建议与后端一致:`/api/*` 1MB、`/ai/*` 10MB(Caddy 用 `request_body` 按前缀分层)
|
||
|
||
## AI 网关接口
|
||
|
||
对外提供 OpenAI / Anthropic 兼容端点,转发 OCI GenAI On-Demand 推理。鉴权用面板创建的 AI 密钥,`Authorization: Bearer sk-...` 与 `x-api-key: sk-...` 双头均可;密钥可绑定渠道分组实现路由隔离,也可配置模型白名单(白名单外调用 404,模型列表只返回交集)。请求体上限 10MB。
|
||
|
||
| 端点 | 协议 | 流式 |
|
||
| --- | --- | --- |
|
||
| `POST /ai/v1/responses` | OpenAI Responses(无状态子集) | SSE 支持(服务端工具除外) |
|
||
| `POST /ai/v1/messages` | Anthropic Messages | SSE 支持 |
|
||
| `POST /ai/v1/embeddings` | OpenAI Embeddings | — |
|
||
| `GET /ai/v1/models` | OpenAI 模型列表(来自渠道模型目录) | — |
|
||
|
||
对话端点统一转发 OCI OpenAI 兼容面(`/actions/v1/responses`),因此仅提供该面支持的 `xai.` / `meta.` / `openai.` 前缀模型;google / cohere 对话模型不在兼容面供给(上游 400),不再提供(cohere embed 模型不受影响)。兼容面属实测可用但无 Oracle 文档承诺的能力,行为可能随上游调整。**Chat Completions(`/ai/v1/chat/completions`)端点已移除**,请迁移到 Responses 或 Messages。
|
||
|
||
以下各表对照网关行为:✅ 转发上游;⚠️ 接受但忽略(静默丢弃,不影响请求);❌ 拒绝(400,不发上游)。
|
||
|
||
### Responses 字段
|
||
|
||
请求体除下列例外**原样直通上游**(未列字段一并转发,效果由上游决定):
|
||
|
||
| 字段 | 支持 | 说明 |
|
||
| --- | --- | --- |
|
||
| `model`、`input` | ✅ | `input` 接受 string 或 item 数组 |
|
||
| `stream` | ✅ | SSE,上游原生事件流(含 gpt-oss 推理增量);与服务端工具互斥 |
|
||
| `reasoning.effort` | ✅ | 直达上游不限档位,见下「推理力度」 |
|
||
| `store` | ⚠️ | 网关无状态,强制改写为 `false` |
|
||
| `previous_response_id`、`conversation`、`background` | ❌ | 网关不保存历史,请求需自带完整上下文 |
|
||
| `web_search` / `x_search` / `function` 之外的工具类型 | ❌ | 如 `file_search`、`code_interpreter`、`mcp` |
|
||
|
||
**服务端工具**:`tools` 支持 `{"type":"web_search"}` 与 `{"type":"x_search"}`(xAI Grok 官方 Agent Tools 格式,可与 `function` 混用),响应保留 `web_search_call` 输出项与引用;**仅非流式**。响应 `usage.input_tokens_details.cached_tokens` 透传缓存命中量。
|
||
|
||
### Anthropic Messages 字段
|
||
|
||
网关把 Messages 请求转换为 Responses 请求送上游,响应(含流式事件序列)转回 Anthropic 形态:
|
||
|
||
| 字段 | 支持 | 说明 |
|
||
| --- | --- | --- |
|
||
| `model`、`max_tokens`、`messages` | ✅ | `max_tokens` 必填;content 块支持 `text` / `image` / `tool_use` / `tool_result`,其余块类型 400 |
|
||
| `system` | ✅ | string 或块数组,映射为 `instructions` |
|
||
| `temperature`、`top_p` | ✅ | |
|
||
| `stream` | ✅ | SSE,标准 Anthropic 事件序列(`message_start` → `content_block_*` → `message_delta` → `message_stop`) |
|
||
| `tools`、`tool_choice` | ✅ | 映射为 Responses `function` 工具;`tool_choice` 支持 `auto` / `any` / `none` / `tool` |
|
||
| `output_config.effort` | ✅ | 直达上游不限档位,见下「推理力度」 |
|
||
| `stop_sequences`、`top_k`、`metadata`、`thinking`、`service_tier` | ⚠️ | Responses 面无对应物,静默忽略 |
|
||
|
||
模型的思考输出(reasoning)不转为 `thinking` 块,静默丢弃;`usage.cache_read_input_tokens` 透传缓存命中量。
|
||
|
||
### Embeddings 字段
|
||
|
||
| 字段 | 支持 | 说明 |
|
||
| --- | --- | --- |
|
||
| `model`、`input` | ✅ | `input` 接受 string 或数组 |
|
||
| `dimensions` | ✅ | |
|
||
| `encoding_format`、`user` | ⚠️ | 输出恒为 float 数组 |
|
||
|
||
### 推理力度(effort)
|
||
|
||
两个对话协议各以原生字段控制推理深度,取值直达上游不做档位校验:
|
||
|
||
| 协议 | 字段 |
|
||
| --- | --- |
|
||
| Responses | `reasoning.effort` |
|
||
| Anthropic Messages | `output_config.effort`(转小写后透传) |
|
||
|
||
是否支持、可用档位与实际效果由模型决定(2026-07 兼容面实测):
|
||
|
||
| 模型 | 支持档位 | 实测行为 |
|
||
| --- | --- | --- |
|
||
| `xai.grok-4.3` | `none` / `minimal` / `low` / `medium` / `high` | 全部生效:`none` 完全关闭推理,默认 `low`,`high` 推理量最大 |
|
||
| `openai.gpt-oss-*` | `minimal` / `low` / `medium` / `high` | `none` 被上游 400(Harmony 格式不支持);思考计入 completion tokens |
|
||
| `xai.grok-3-mini(-fast)` | 名义 `low` / `high` | 各档均被接受,但 `none` 并不关闭推理(仍产生思考) |
|
||
| `meta.llama-*` | 接受任意档 | 非推理模型,参数无实际效果 |
|
||
| `xai.grok-4.20-multi-agent*` | `low` / `medium` / `high` / `xhigh` | effort 控制并行 agent 数(4 / 16)而非推理深度 |
|
||
| 其余 grok(`grok-3`、`grok-4`、`grok-4-fast-*`、`grok-4-1-fast-*`、`grok-code-fast-1`、`grok-4.20-*` 单/双型号全系) | 不支持 | 携带即被上游 400(`does not support parameter reasoningEffort`) |
|
||
|
||
不携带该字段时网关不下发,行为由模型默认档位决定。
|
||
|
||
### 通用行为
|
||
|
||
- 未知模型 404;无可用渠道 503;单次请求最多尝试 3 个渠道(429/5xx/网络错误自动换渠道并计入熔断,模型级 400/404 换渠道不计熔断,其余 4xx 原样透传);流式建立后绑定渠道,中断不重试
|
||
- 调用计量(渠道、token 用量、缓存命中、时延、重试数)写入面板「调用日志」;密钥可按需开启限时内容抓取
|
||
|
||
## 环境变量
|
||
|
||
| 变量 | 必填 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `DATA_KEY` | 是 | 无 | 敏感字段加密主密钥(更换后已入库密文无法解密) |
|
||
| `JWT_SECRET` | 是 | 无 | 登录令牌签名密钥 |
|
||
| `ADMIN_USERNAME` | 否 | `admin` | 初始管理员用户名 |
|
||
| `ADMIN_PASSWORD` | 首次启动是 | 无 | 仅在用户不存在时创建;已存在不重置 |
|
||
| `ADDR` | 否 | `:8080` | HTTP 监听地址 |
|
||
| `DB_DRIVER` | 否 | `sqlite` | `sqlite` / `mysql` / `postgres`(后两者 experimental) |
|
||
| `DB_DSN` | 外部库时是 | 无 | MySQL 需 `parseTime=True`;PostgreSQL 标准 DSN |
|
||
| `DB_PATH` | 否 | `oci-portal.db` | SQLite 文件路径 |
|
||
| `PUBLIC_URL` | 否 | 无 | 面板公网基址,日志回传一键创建链路用 |
|
||
| `HTTPS_PROXY` | 否 | 无 | 出站代理(如 Telegram 通知走代理) |
|
||
| `TZ` | 否 | 系统 | cron 表达式解释时区(容器内建议显式设置,二进制已嵌 tzdata) |
|
||
| `SWAGGER` | 否 | 关 | `1` 时开放 `/swagger/index.html` API 文档(生产建议按需临时开启) |
|
||
| `GIN_MODE` | 否 | `release` | `debug` / `release`,影响日志与调试输出 |
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
go test ./... # 全量测试
|
||
go vet ./... && gofmt -l .
|
||
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency # 接口注释变更后重新生成 OpenAPI
|
||
```
|
||
|
||
API 文档:全部接口带 swaggo 注释,`SWAGGER=1` 启动后访问 `/swagger/index.html`;spec 文件在 `docs/swagger.json|yaml`。
|
||
|
||
编码规范与项目约定见 [AGENTS.md](AGENTS.md) 与 [.trellis/spec/](.trellis/spec/)。
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE)
|