<div align="center">

<img src="docs/assets/logo.svg" width="104" alt="OCI Portal logo">

# OCI Portal

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

[![Release](https://img.shields.io/github/v/release/wangdefaa/oci-portal?display_name=tag)](https://github.com/wangdefaa/oci-portal/releases/latest)
[![Go](https://img.shields.io/badge/Go-1.26.5-00ADD8?logo=go&logoColor=white)](go.mod)
[![Docker](https://img.shields.io/badge/Docker-amd64%20%7C%20arm64-2496ED?logo=docker&logoColor=white)](docker-compose.yml)
[![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)

[界面预览](#界面预览) · [核心能力](#核心能力) · [快速开始](#快速开始) · [生产部署](#生产部署) · [AI 网关](#ai-网关) · [开发](#开发)

[AI 网关文档](docs/AI网关.md) · [OpenAPI](docs/swagger.yaml) · [更新日志](CHANGELOG.md) · [前端仓库](https://github.com/wangdefaa/oci-portal-dash)

</div>

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

本仓库为后端与发行仓库；前端源码位于 [oci-portal-dash](https://github.com/wangdefaa/oci-portal-dash)。

## 界面预览

<p align="center">
  <img src="docs/assets/screenshot-overview.png" width="960" alt="OCI Portal 总览">
</p>

<details>
<summary><strong>展开更多界面截图</strong></summary>

| 登录 | 租户 |
| --- | --- |
| ![登录](docs/assets/screenshot-signin.png) | ![租户](docs/assets/screenshot-tenants.png) |

| 任务 | AI 网关 |
| --- | --- |
| ![任务](docs/assets/screenshot-tasks.png) | ![AI 网关](docs/assets/screenshot-ai-gateway.png) |

| 通知设置 |
| --- |
| ![通知设置](docs/assets/screenshot-settings-notify.png) |

</details>

## 核心能力

| 能力域 | 覆盖范围 |
| --- | --- |
| **租户与云资源** | 多 OCI API Key、分组、批量测活、账户画像、订阅区域缓存与切换；实例创建与电源操作、VNIC、公网 IP、IPv6、VCN、安全列表、引导卷、块存储挂载、限额与成本查询 |
| **自动化与控制台** | 抢机、租户测活、成本同步、AI 渠道探测；执行日志、重叠防护、熔断与结果通知；xterm 串行终端、noVNC 和 OCI 控制台连接两跳 SSH 隧道 |
| **身份与审计** | IAM 用户、MFA、API Key、密码策略、SAML、通知收件人与多 Identity Domain；通过 Service Connector Hub 与 Notifications 接收并分类推送 OCI Audit 事件 |
| **通知与安全** | Telegram、Webhook、ntfy、Bark、SMTP；AES-256-GCM、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） │
└─────────────────────────────────────────────────────────────────┘
```

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

## 快速开始

### Docker Compose（推荐）

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

> [!CAUTION]
> `.env` 中的 `DATA_KEY` 用于解密 OCI 私钥、口令和渠道凭据。首次生成后必须长期
> 保存；升级或重装时不要覆盖，否则已有密文将无法恢复。

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`）可写；
   Docker Desktop 用户通常不需要执行：

   ```bash
   sudo chown 65532:65532 data
   ```

3. 启动并检查状态：

   ```bash
   docker compose up -d
   docker compose ps
   ```

4. 查看初始管理员密码并登录：

   ```bash
   grep '^ADMIN_PASSWORD=' .env
   ```

   访问 `http://127.0.0.1:18888`，默认用户名为 `admin`。

   启动异常时查看最近日志：

   ```bash
   docker compose logs --tail=100 oci-portal
   ```

> [!TIP]
> 请将 `.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
curl -fLO https://github.com/wangdefaa/oci-portal/releases/latest/download/SHA256SUMS
grep 'oci-portal-server-linux-amd64$' SHA256SUMS | sha256sum -c -
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` 中的真实前端产物不应提交到仓库。

## 生产部署

> [!WARNING]
> 服务本身只提供 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 网关

面板内置 OpenAI / Anthropic 兼容的 GenAI 网关：独立密钥鉴权（`Authorization: Bearer sk-...` 或 `x-api-key`），支持渠道分组、加权路由、熔断探测、模型黑白名单、内容日志与调用日志。

| 端点 | 定位 | 流式 |
| --- | --- | --- |
| `POST /ai/v1/responses` | OpenAI Responses，无状态主接口（xAI 服务端工具 / MCP） | SSE |
| `POST /ai/v1/chat/completions` | OpenAI Chat Completions 兼容层 | SSE |
| `POST /ai/v1/messages` | Anthropic Messages 转换层 | SSE |
| `POST /ai/v1/embeddings` | OpenAI Embeddings | 否 |
| `POST /ai/v1/audio/speech` | 文本转语音（xAI Voice） | 否 |
| `POST /ai/v1/tts` | 文本转语音（xAI 官方格式） | 否 |
| `POST /ai/v1/rerank` | 文档重排（Cohere Rerank） | 否 |
| `POST /ai/v1/moderations` | 内容审核（OCI Guardrails） | 否 |
| `GET /ai/v1/models` | 当前密钥可见的模型列表 | 否 |

协议兼容边界、已知上游限制与逐字段兼容矩阵见 **[AI 网关文档](./docs/AI网关.md)**。

## 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 / PostgreSQL | — | 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` |

## 升级与备份

1. 阅读 [CHANGELOG](CHANGELOG.md)，确认目标版本的行为变化。
2. 备份 `.env` 和数据库。SQLite Compose 部署建议先停止服务，再复制
   `data/oci-portal.db`，避免在线复制产生不一致快照。
3. 保持原 `DATA_KEY` 不变；只有数据库而没有对应密钥时，敏感字段无法解密。
4. Compose 部署执行 `docker compose pull`，再执行 `docker compose up -d`；服务启动时
   会自动完成数据库迁移。
5. 生产环境保持 Swagger 关闭、限制管理面访问来源，并为 OCI API Key 配置最小权限。

## 开发

```bash
gofmt -l .
go vet ./...
go test ./...

# Handler 注释变更后重新生成唯一的对外 API 文档。
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency --overridesFile docs/.swaggo
```

- 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)
