@@ -0,0 +1,10 @@
|
|||||||
|
# 本地产物与数据不进构建上下文(internal/webui/dist 必须保留:嵌入产物来源)
|
||||||
|
.git/
|
||||||
|
bin/
|
||||||
|
*.db
|
||||||
|
*.env
|
||||||
|
.env*
|
||||||
|
onsspike
|
||||||
|
__debug_bin*
|
||||||
|
.trellis/
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# 仅分支推送触发:tag 推送交给 release.yml;solo 开发不走 PR,去掉 pull_request 避免同一提交双跑
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest-amd64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.26.5"
|
||||||
|
- name: gofmt 检查
|
||||||
|
run: test -z "$(gofmt -l .)"
|
||||||
|
- name: go vet
|
||||||
|
run: go vet ./...
|
||||||
|
- name: 测试
|
||||||
|
run: go test ./...
|
||||||
|
- name: 编译检查(runner 即 linux/amd64,验证 dist 嵌入与链接)
|
||||||
|
run: go build ./...
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILDX_NO_DEFAULT_ATTESTATIONS: "1"
|
||||||
|
DASH_REPO: wangdefa/oci-portal-dash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest-amd64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 准备 buildx 与镜像地址
|
||||||
|
run: |
|
||||||
|
# 多平台构建需要 docker-container driver;Dockerfile 交叉编译,无需 QEMU
|
||||||
|
docker buildx create --use --driver docker-container --driver-opt network=host || true
|
||||||
|
REGISTRY=$(echo "${{ gitea.server_url }}" | sed 's|https\?://||')
|
||||||
|
echo "REGISTRY=${REGISTRY}" >> "$GITHUB_ENV"
|
||||||
|
echo "IMAGE=${REGISTRY}/${{ gitea.repository }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: 下载前端最新 dist.zip 解压进嵌入目录
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
DASH_TAG=$(curl -fsSL -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${DASH_REPO}/releases/latest" | jq -r '.tag_name')
|
||||||
|
curl -fsSL -H "Authorization: token ${TOKEN}" \
|
||||||
|
-o dist.zip "${{ gitea.server_url }}/${DASH_REPO}/releases/download/${DASH_TAG}/dist.zip"
|
||||||
|
rm -rf internal/webui/dist && mkdir -p internal/webui/dist
|
||||||
|
unzip -q dist.zip -d internal/webui/dist
|
||||||
|
test -f internal/webui/dist/index.html
|
||||||
|
|
||||||
|
- name: 构建双架构二进制(artifact stage 导出)
|
||||||
|
run: |
|
||||||
|
docker buildx build --pull \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--target artifact \
|
||||||
|
--output type=local,dest=./output .
|
||||||
|
mkdir -p assets
|
||||||
|
mv output/linux_amd64/oci-portal-server assets/oci-portal-server-linux-amd64
|
||||||
|
mv output/linux_arm64/oci-portal-server assets/oci-portal-server-linux-arm64
|
||||||
|
|
||||||
|
- name: 构建并推送多平台镜像(与上一步共享构建缓存)
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "${TOKEN}" | docker login "${REGISTRY}" -u "${{ gitea.repository_owner }}" --password-stdin
|
||||||
|
docker buildx build --pull \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
-t "${IMAGE}:${{ gitea.ref_name }}" -t "${IMAGE}:latest" \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
- name: 提取 CHANGELOG 版本段作为 Release 描述
|
||||||
|
run: |
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
awk -v ver="${TAG#v}" '$0 ~ "^## \\[" ver "\\]" {flag=1; next} /^## \[/ && flag {exit} flag {print}' CHANGELOG.md > release-notes.md
|
||||||
|
[ -s release-notes.md ] || echo "Release ${TAG}" > release-notes.md
|
||||||
|
|
||||||
|
- name: 创建 Release 并上传二进制附件(重跑幂等,同名附件自动替换)
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
body_path: release-notes.md
|
||||||
|
files: assets/*
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.26.5"
|
||||||
|
- name: gofmt 检查
|
||||||
|
run: test -z "$(gofmt -l .)"
|
||||||
|
- name: go vet
|
||||||
|
run: go vet ./...
|
||||||
|
- name: 测试
|
||||||
|
run: go test ./...
|
||||||
|
- name: 交叉编译(嵌入当前 dist,占位亦可)
|
||||||
|
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o /tmp/oci-portal-server ./cmd/server
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILDX_NO_DEFAULT_ATTESTATIONS: "1"
|
||||||
|
DASH_DOWNLOAD: https://github.com/wangdefaa/oci-portal-dash/releases/latest/download/dist.zip
|
||||||
|
IMAGE: ghcr.io/wangdefaa/oci-portal
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: 下载前端最新 dist.zip 解压进嵌入目录
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o dist.zip "$DASH_DOWNLOAD"
|
||||||
|
rm -rf internal/webui/dist && mkdir -p internal/webui/dist
|
||||||
|
unzip -q dist.zip -d internal/webui/dist
|
||||||
|
test -f internal/webui/dist/index.html
|
||||||
|
|
||||||
|
- name: 构建双架构二进制(artifact stage 导出)
|
||||||
|
run: |
|
||||||
|
docker buildx build --pull \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--target artifact \
|
||||||
|
--output type=local,dest=./output .
|
||||||
|
mkdir -p assets
|
||||||
|
mv output/linux_amd64/oci-portal-server assets/oci-portal-server-linux-amd64
|
||||||
|
mv output/linux_arm64/oci-portal-server assets/oci-portal-server-linux-arm64
|
||||||
|
ls -lh assets/
|
||||||
|
|
||||||
|
- name: 提取 CHANGELOG 版本段作为 Release 描述
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
awk -v ver="$VERSION" '$0 ~ "^## \\[" ver "\\]" {flag=1; next} /^## \[/ && flag {exit} flag {print}' CHANGELOG.md > release-notes.md
|
||||||
|
[ -s release-notes.md ] || echo "Release ${TAG}" > release-notes.md
|
||||||
|
|
||||||
|
- name: 创建 Release 并上传二进制附件
|
||||||
|
uses: softprops/action-gh-release@v3
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
body_path: release-notes.md
|
||||||
|
files: assets/*
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: 构建并推送多平台镜像
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
provenance: false
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE }}:${{ github.ref_name }}
|
||||||
|
${{ env.IMAGE }}:latest
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
/onsspike
|
__debug_bin*
|
||||||
|
|
||||||
|
# 嵌入的前端产物不入库,仅保留占位 index.html(保 go build)
|
||||||
|
/internal/webui/dist/*
|
||||||
|
!/internal/webui/dist/index.html
|
||||||
|
|
||||||
# 本地数据与环境
|
# 本地数据与环境
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
0.6.5
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 并发规范
|
||||||
|
|
||||||
|
> Go 后端并发约定。
|
||||||
|
|
||||||
|
- 每个 goroutine 必须有明确退出路径:外部用 `context.Context` 或 stop channel 通知,内部 `select` 响应;禁止 fire-and-forget,不允许 goroutine 泄漏。
|
||||||
|
- 调用方能等待 goroutine 退出:`sync.WaitGroup` 或 `done` channel。
|
||||||
|
- `init()` 中不启动 goroutine、不做 IO。
|
||||||
|
- channel 容量只用 0 或 1,更大缓冲须注释说明理由。
|
||||||
|
- 涉及阻塞或远程调用的函数第一个参数是 `ctx context.Context`,不把 ctx 存进 struct。
|
||||||
|
|
||||||
|
## 模式:服务级后台 goroutine 的落地形态(2026-07 起为既定写法)
|
||||||
|
|
||||||
|
面板内「异步旁路」(通知发送 `Notifier.SendAsync`、日志异步落库 `SystemLogService.Record`、定期清理 `StartCleanup`)统一三件套:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // 自带超时,不复用请求 ctx
|
||||||
|
defer cancel()
|
||||||
|
if err := s.doWork(ctx); err != nil {
|
||||||
|
log.Printf("xxx: %v", err) // 旁路失败只记日志,绝不影响主流程
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
- 服务暴露 `Wait()`(内部 `wg.Wait()`);常驻循环(如清理 ticker)额外接收 ctx,`select` 响应退出。
|
||||||
|
- cmd/server 装配的 defer 顺序必须是「先停生产者、再等消费者」(LIFO):`defer notifier.Wait()` / `defer systemLogs.Wait()` 写在 `defer tasks.Stop()` / `defer stopCleanup()` **之前**。
|
||||||
|
- 陷阱:`cron.Stop()` 返回的 context 要等(`<-s.cron.Stop().Done()`),否则仍在执行的任务尚未 `wg.Add` 时 `Wait()` 会提前返回,goroutine 泄漏且违反 WaitGroup 的 Add/Wait happens-before 约束。
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Database Guidelines
|
||||||
|
|
||||||
|
> Database patterns and conventions for this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Document your project's database conventions here.
|
||||||
|
|
||||||
|
Questions to answer:
|
||||||
|
- What ORM/query library do you use?
|
||||||
|
- How are migrations managed?
|
||||||
|
- What are the naming conventions for tables/columns?
|
||||||
|
- How do you handle transactions?
|
||||||
|
-->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Query Patterns
|
||||||
|
|
||||||
|
<!-- How should queries be written? Batch operations? -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
<!-- How to create and run migrations -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
<!-- Table names, column names, index names -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
<!-- Database-related mistakes your team has made -->
|
||||||
|
|
||||||
|
### Common Mistake: gorm 读 NULL 列到已有值的结构体字段不会清零
|
||||||
|
|
||||||
|
**Symptom**:UPDATE 把可空列(如 `*time.Time`)写成 NULL 后,用**同一个结构体变量**再次 `First()` 读回,该字段仍是旧值;而新变量读取正常。断言/返回值出现「幽灵旧值」。
|
||||||
|
|
||||||
|
**Cause**:gorm Scan 遇到 NULL 列时跳过赋值(保持字段现状),不会主动置 nil。复用变量(循环内重读、更新后回读同一 &ch)时旧值残留。
|
||||||
|
|
||||||
|
**Fix**:更新后回读一律用新变量(`var fresh model.X; db.First(&fresh, id)`)。
|
||||||
|
|
||||||
|
**Prevention**:凡「UPDATE 后回读返回」的服务方法,都声明新变量接收;测试中多次 `First` 同一行也各用独立变量。另:map Updates 写 NULL 用 `gorm.Expr("NULL")` 最稳(无类型 `nil` 在部分路径下不生效)。
|
||||||
|
|
||||||
|
### Common Mistake: 需要"条件更新多列含 NULL"时的写法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 正确:零写库条件 + 显式 NULL
|
||||||
|
db.Model(&model.AiChannel{}).
|
||||||
|
Where("id = ? AND (fail_count > 0 OR disabled_until IS NOT NULL)", id).
|
||||||
|
Updates(map[string]any{"fail_count": 0, "disabled_until": gorm.Expr("NULL")})
|
||||||
|
```
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 后端目录结构
|
||||||
|
|
||||||
|
> Go 后端工程的目录职责与代码组织约定。
|
||||||
|
|
||||||
|
## 目录职责
|
||||||
|
|
||||||
|
```text
|
||||||
|
oci-portal/
|
||||||
|
├── go.mod 模块名 oci-portal,依赖 oracle/oci-go-sdk/v65
|
||||||
|
├── go.sum
|
||||||
|
├── cmd/
|
||||||
|
│ └── server/ 启动入口 main.go
|
||||||
|
└── internal/
|
||||||
|
├── api/ HTTP 路由、参数绑定、响应、中间件
|
||||||
|
├── config/ 环境变量读取
|
||||||
|
├── crypto/ 敏感字段 AES-GCM 加解密
|
||||||
|
├── database/ SQLite 连接和 AutoMigrate
|
||||||
|
├── model/ GORM 数据模型
|
||||||
|
├── oci/ OCI SDK 封装,真实云请求只出现在此包
|
||||||
|
└── service/ 业务逻辑和本地快照同步
|
||||||
|
```
|
||||||
|
|
||||||
|
已实现:API Key 导入(ini 原文或显式字段)、测活(Identity GetTenancy)、账户类别判定(Organizations CLOUDCM 订阅)、重新测活时的快照差异同步。
|
||||||
|
|
||||||
|
## 代码组织约定
|
||||||
|
|
||||||
|
- 布局遵循官方约定:入口在 `cmd/<binary>/main.go`,非导出实现放 `internal/`(出现对外复用库时才建 `pkg/`),不过度分层。
|
||||||
|
- 接口定义在使用方包中;先有第二个实现或测试替身需求,再抽接口。
|
||||||
|
- 用 guard clause 尽早 return,减少嵌套。
|
||||||
|
- `defer` 紧跟资源获取语句,成对管理释放。
|
||||||
|
- 结构体初始化写字段名;已知容量的 slice/map 用 `make(T, 0, n)` 预分配。
|
||||||
|
- 注释解释"为什么"而非"是什么";导出符号写以名字开头的 godoc 注释。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 错误处理规范
|
||||||
|
|
||||||
|
> Go 后端错误处理约定。
|
||||||
|
|
||||||
|
- error 向上返回并用 `fmt.Errorf("...: %w", err)` 附加上下文,不吞错。
|
||||||
|
- 一个错误只处理一次:要么就地处理(重试、降级),要么包装返回;不要既打日志又返回。
|
||||||
|
- 包装消息描述当前操作(如 `load oci config`),不堆叠 `failed to` 前缀。
|
||||||
|
- 需要调用方区分的错误用 sentinel(`errors.Is`)或自定义类型(`errors.As`),不比对错误字符串。
|
||||||
|
- 正常流程不使用 `panic`;类型断言一律 `v, ok := x.(T)` 双返回值形式。
|
||||||
|
|
||||||
|
## 禁止:让错误消息携带敏感信息外流
|
||||||
|
|
||||||
|
**问题**:错误消息会流向后端日志与 HTTP 响应(respondError),其中可能嵌着凭据。典型案例:`http.Client.Do` 失败返回 `*url.Error`,其 `Error()` 拼出**完整请求 URL**——对 Telegram 这类把 token 放路径的 API(`/bot<token>/sendMessage`),token 直接进日志和接口响应。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 错误:url.Error 原样外传,token 泄漏
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send telegram message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正确:剥掉含 URL 的外壳,只保留底层原因(保持 %w 链)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send telegram message: %w", sanitizeURLError(err))
|
||||||
|
}
|
||||||
|
func sanitizeURLError(err error) error {
|
||||||
|
var ue *url.Error
|
||||||
|
if errors.As(err, &ue) { return ue.Err }
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**预防**:凡外发 HTTP 且 URL/头中含凭据(token、签名、secret 路径段)的客户端,错误必须先脱敏再包装;新增此类客户端时补一条「错误不含凭据」的回归测试(参照 internal/service/notify_test.go 的 TestNotifierSendErrorHidesToken)。
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Backend Development Guidelines(oci-portal 后端规范)
|
||||||
|
|
||||||
|
> Go 后端(`oci-portal/`)编码规范入口。规范提炼自 Google / Uber Go Style Guide、Effective Go 与 Go 官方模块布局指南,按本项目裁剪;条目与项目约定冲突时,以本 spec 为准。迁自 docs/开发指南.md 与根目录 AGENTS.md(2026-07)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术栈约束
|
||||||
|
|
||||||
|
Gin + GORM + SQLite(纯 Go 驱动 `glebarez/sqlite`,免 CGO;默认与推荐)+ AES-GCM 字段加密 + `oracle/oci-go-sdk/v65`;JWT + bcrypt 用于登录。可选 MySQL / PostgreSQL(`DB_DRIVER`/`DB_DSN`,纯 Go 驱动,experimental);模型新增长文本字段时注意 MySQL 映射:确定 <64KB 用 `type:text`,可能超 64KB 用 `size:16777216`(→MEDIUMTEXT),短字段走 DefaultStringSize=512。**不引入 Redis**,单实例部署按需用进程内缓存;SQLite 快照承担"最近一次云端状态"。前端产物经 `internal/webui` go:embed 嵌入,来源为前端仓库 Release 的 `dist.zip`(release.yml 自动下载;本地构建方式见 README)。选型理由与本地运行方式(环境变量等)见 `docs/开发指南.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guidelines Index
|
||||||
|
|
||||||
|
| Guide | 内容 | 状态 |
|
||||||
|
|-------|------|------|
|
||||||
|
| [Directory Structure](./directory-structure.md) | 目录职责与代码组织约定 | 已填 |
|
||||||
|
| [Error Handling](./error-handling.md) | 错误包装、传播与 panic 纪律 | 已填 |
|
||||||
|
| [Quality Guidelines](./quality-guidelines.md) | 工具链检查与命名规范 | 已填 |
|
||||||
|
| [Concurrency](./concurrency.md) | goroutine 生命周期与 context 传递 | 已填 |
|
||||||
|
| [Testing](./testing.md) | table-driven 测试要求 | 已填 |
|
||||||
|
| [Database Guidelines](./database-guidelines.md) | ORM 模式、查询、迁移 | 待填 |
|
||||||
|
| [Logging Guidelines](./logging-guidelines.md) | 结构化日志、日志级别 | 待填 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Development Checklist
|
||||||
|
|
||||||
|
写代码前确认:
|
||||||
|
|
||||||
|
- [ ] 读过 [Directory Structure](./directory-structure.md),新代码放对了包(云请求只出现在 `internal/oci/`)
|
||||||
|
- [ ] 阻塞 / 远程调用函数第一个参数是 `ctx context.Context`(见 [Concurrency](./concurrency.md))
|
||||||
|
- [ ] 错误按 [Error Handling](./error-handling.md) 用 `%w` 包装向上返回
|
||||||
|
- [ ] 复用已有封装,不重复造轮子(见 `../guides/code-reuse-thinking-guide.md`)
|
||||||
|
|
||||||
|
## Quality Check
|
||||||
|
|
||||||
|
提交前必过:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -l . && goimports -l . # 格式(或由编辑器保存时完成)
|
||||||
|
go vet ./...
|
||||||
|
go test ./...
|
||||||
|
go mod tidy # 依赖有变更时
|
||||||
|
```
|
||||||
|
|
||||||
|
有 `golangci-lint` 配置时以其为准。新逻辑必须带 table-driven 测试,`go test ./...` 全绿才算完成。
|
||||||
|
|
||||||
|
## API 文档(swagger)
|
||||||
|
|
||||||
|
全部 HTTP 接口带 swaggo 注释(@Summary 中文/@Tags 按域/@Param/@Success/@Router;JWT 组接口标 @Security BearerAuth)。**新增或修改接口必须同步注释**,并重新生成 spec:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的 `docs/` 一并提交;UI 由 `SWAGGER=1` 开启(/swagger/index.html)。同名 handler 方法(list/create/get/update/remove)分布在多个 struct 上,写注释时确认 @Router 路径与该 receiver 的真实注册一致(routes_*.go)。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Logging Guidelines
|
||||||
|
|
||||||
|
> How logging is done in this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Document your project's logging conventions here.
|
||||||
|
|
||||||
|
Questions to answer:
|
||||||
|
- What logging library do you use?
|
||||||
|
- What are the log levels and when to use each?
|
||||||
|
- What should be logged?
|
||||||
|
- What should NOT be logged (PII, secrets)?
|
||||||
|
-->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log Levels
|
||||||
|
|
||||||
|
<!-- When to use each level: debug, info, warn, error -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structured Logging
|
||||||
|
|
||||||
|
<!-- Log format, required fields -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to Log
|
||||||
|
|
||||||
|
<!-- Important events to log -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What NOT to Log
|
||||||
|
|
||||||
|
<!-- Sensitive data, PII, secrets -->
|
||||||
|
|
||||||
|
(To be filled by the team)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 质量与命名规范
|
||||||
|
|
||||||
|
> Go 后端工具链检查与命名约定。
|
||||||
|
|
||||||
|
## 工具链(提交前必过)
|
||||||
|
|
||||||
|
- 提交前必须通过 `gofmt`/`goimports`、`go vet ./...`、`go test ./...`。
|
||||||
|
- 仓库有 `golangci-lint` 配置时以其为准。
|
||||||
|
- 依赖变更后运行 `go mod tidy`,`go.mod` 与 `go.sum` 一起提交。
|
||||||
|
|
||||||
|
## 命名
|
||||||
|
|
||||||
|
- 一律 MixedCaps,不用下划线;缩写词大小写保持一致:`ociConfigID`、`ParseURL`、`HTTPClient`。
|
||||||
|
- 包名小写单个词,不用 `util`、`common`、`base` 这类空泛名。
|
||||||
|
- 变量名长度与作用域成正比:循环变量可用 `i`、`c`,包级变量用完整单词。
|
||||||
|
- Getter 不加 `Get` 前缀:`user.Name()` 而非 `user.GetName()`。
|
||||||
|
- 方法接收者 1~2 个字母且同一类型内保持一致,如 `func (s *InstanceService)`。
|
||||||
|
- 错误变量:导出 sentinel 用 `ErrXxx`,包内用 `errXxx`;自定义错误类型以 `Error` 结尾。
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 测试规范
|
||||||
|
|
||||||
|
> Go 后端测试约定。
|
||||||
|
|
||||||
|
- 新逻辑配 table-driven 测试,用例含名字、输入、期望值;`go test ./...` 全绿才算完成。
|
||||||
|
- 断言失败时输出 `got/want` 对比,便于定位。
|
||||||
|
- 辅助函数标注 `t.Helper()`,清理动作用 `t.Cleanup()`。
|
||||||
|
|
||||||
|
## 常见坑:`:memory:` SQLite 每个连接是独立库
|
||||||
|
|
||||||
|
**症状**:被测代码里有异步 goroutine 写库(如日志异步落库)时,测试偶发 `no such table: xxx`。
|
||||||
|
|
||||||
|
**原因**:glebarez/sqlite 的 `:memory:` 数据库按连接隔离;GORM 连接池为新 goroutine 开新连接时,拿到的是一个没跑过 AutoMigrate 的空库。
|
||||||
|
|
||||||
|
**修复**:测试 helper 建库后锁单连接,生产文件库不受影响:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
sqlDB.SetMaxOpenConns(1) // :memory: 每连接独立库,异步写复用同一连接
|
||||||
|
```
|
||||||
|
|
||||||
|
**预防**:测试涉及「后台 goroutine 写库」时一律加此设置(参照 internal/api/router_test.go 与 internal/service 各测试 helper);异步写路径同时拆出同步内核函数(如 `record` / `Record`)供测试直调断言。
|
||||||
|
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# Code Reuse Thinking Guide
|
||||||
|
|
||||||
|
> **Purpose**: Stop and think before creating new code - does it already exist?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
**Duplicated code is the #1 source of inconsistency bugs.**
|
||||||
|
|
||||||
|
When you copy-paste or rewrite existing logic:
|
||||||
|
- Bug fixes don't propagate
|
||||||
|
- Behavior diverges over time
|
||||||
|
- Codebase becomes harder to understand
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before Writing New Code
|
||||||
|
|
||||||
|
### Step 1: Search First
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for similar function names
|
||||||
|
grep -r "functionName" .
|
||||||
|
|
||||||
|
# Search for similar logic
|
||||||
|
grep -r "keyword" .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Ask These Questions
|
||||||
|
|
||||||
|
| Question | If Yes... |
|
||||||
|
|----------|-----------|
|
||||||
|
| Does a similar function exist? | Use or extend it |
|
||||||
|
| Is this pattern used elsewhere? | Follow the existing pattern |
|
||||||
|
| Could this be a shared utility? | Create it in the right place |
|
||||||
|
| Am I copying code from another file? | **STOP** - extract to shared |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Duplication Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Copy-Paste Functions
|
||||||
|
|
||||||
|
**Bad**: Copying a validation function to another file
|
||||||
|
|
||||||
|
**Good**: Extract to shared utilities, import where needed
|
||||||
|
|
||||||
|
### Pattern 2: Similar Components
|
||||||
|
|
||||||
|
**Bad**: Creating a new component that's 80% similar to existing
|
||||||
|
|
||||||
|
**Good**: Extend existing component with props/variants
|
||||||
|
|
||||||
|
### Pattern 3: Repeated Constants
|
||||||
|
|
||||||
|
**Bad**: Defining the same constant in multiple files
|
||||||
|
|
||||||
|
**Good**: Single source of truth, import everywhere
|
||||||
|
|
||||||
|
### Pattern 4: Repeated Payload Field Extraction
|
||||||
|
|
||||||
|
**Bad**: Multiple consumers cast the same JSON/event fields locally:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const description = (ev as { description?: string }).description;
|
||||||
|
const context = (ev as { context?: ContextEntry[] }).context;
|
||||||
|
```
|
||||||
|
|
||||||
|
This is duplicated contract logic even when the code is only two lines. Each
|
||||||
|
consumer now has its own definition of what a valid payload means.
|
||||||
|
|
||||||
|
**Good**: Put the decoder, type guard, or projection next to the data owner:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (isThreadEvent(ev)) {
|
||||||
|
renderThreadEvent(ev);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule**: If the same untyped payload field is read in 2+ places, create a
|
||||||
|
shared type guard / normalizer / projection before adding a third reader.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Abstract
|
||||||
|
|
||||||
|
**Abstract when**:
|
||||||
|
- Same code appears 3+ times
|
||||||
|
- Logic is complex enough to have bugs
|
||||||
|
- Multiple people might need this
|
||||||
|
|
||||||
|
**Don't abstract when**:
|
||||||
|
- Only used once
|
||||||
|
- Trivial one-liner
|
||||||
|
- Abstraction would be more complex than duplication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Batch Modifications
|
||||||
|
|
||||||
|
When you've made similar changes to multiple files:
|
||||||
|
|
||||||
|
1. **Review**: Did you catch all instances?
|
||||||
|
2. **Search**: Run grep to find any missed
|
||||||
|
3. **Consider**: Should this be abstracted?
|
||||||
|
|
||||||
|
### Reducers Should Use Exhaustive Structure
|
||||||
|
|
||||||
|
When state is derived from action-like values (`action`, `kind`, `status`,
|
||||||
|
`phase`), prefer a reducer with one `switch` over scattered `if/else` updates.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD - action-specific state transitions are hard to audit
|
||||||
|
if (action === "opened") { ... }
|
||||||
|
else if (action === "comment") { ... }
|
||||||
|
else if (action === "status") { ... }
|
||||||
|
|
||||||
|
// GOOD - one reducer owns the transition table
|
||||||
|
switch (event.action) {
|
||||||
|
case "opened":
|
||||||
|
...
|
||||||
|
return;
|
||||||
|
case "comment":
|
||||||
|
...
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This matters when the event log is the source of truth. A reducer is the
|
||||||
|
documented replay model; display code and commands should not duplicate pieces
|
||||||
|
of that replay model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist Before Commit
|
||||||
|
|
||||||
|
- [ ] Searched for existing similar code
|
||||||
|
- [ ] No copy-pasted logic that should be shared
|
||||||
|
- [ ] No repeated untyped payload field extraction outside a shared decoder
|
||||||
|
- [ ] Constants defined in one place
|
||||||
|
- [ ] Similar patterns follow same structure
|
||||||
|
- [ ] Reducer/action transitions live in one reducer or command dispatcher
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotcha: Python if/elif/else Exhaustive Check
|
||||||
|
|
||||||
|
**Problem**: Python's if/elif/else chains have no compile-time exhaustive check. When you add a new value to a `Literal` type (e.g., `Platform`), existing if/elif/else chains silently fall through to `else` with wrong defaults.
|
||||||
|
|
||||||
|
**Symptom**: New platform works partially — some methods return Claude defaults instead of platform-specific values. No error is raised.
|
||||||
|
|
||||||
|
**Example** (`cli_adapter.py`):
|
||||||
|
```python
|
||||||
|
# BAD: "gemini" falls through to else, returns "claude"
|
||||||
|
@property
|
||||||
|
def cli_name(self) -> str:
|
||||||
|
if self.platform == "opencode":
|
||||||
|
return "opencode"
|
||||||
|
else:
|
||||||
|
return "claude" # gemini silently gets "claude"!
|
||||||
|
|
||||||
|
# GOOD: explicit branch for every platform
|
||||||
|
@property
|
||||||
|
def cli_name(self) -> str:
|
||||||
|
if self.platform == "opencode":
|
||||||
|
return "opencode"
|
||||||
|
elif self.platform == "gemini":
|
||||||
|
return "gemini"
|
||||||
|
else:
|
||||||
|
return "claude"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevention**: When adding a new value to a Python `Literal` type, search for ALL if/elif/else chains that switch on that type and add explicit branches. Don't rely on `else` being correct for new values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotcha: Asymmetric Mechanisms Producing Same Output
|
||||||
|
|
||||||
|
**Problem**: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual `files.set()` for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts.
|
||||||
|
|
||||||
|
**Symptom**: Init works perfectly, but update creates files at wrong paths or misses files entirely.
|
||||||
|
|
||||||
|
**Prevention**:
|
||||||
|
- **Best**: Eliminate the asymmetry — have the manual path call the automatic one (e.g., `collectTemplateFiles()` calls `getAllScripts()` instead of maintaining its own list)
|
||||||
|
- **If asymmetry is unavoidable**: Add a regression test that compares outputs from both mechanisms
|
||||||
|
- When migrating directory structures, search for ALL code paths that reference the old structure
|
||||||
|
|
||||||
|
**Real example**: `trellis update` had a manual `files.set()` list for 11 scripts that `getAllScripts()` already tracked. Fix: replaced the manual list with a `for..of getAllScripts()` loop. See `update.ts` refactor in v0.4.0-beta.3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template File Registration (Trellis-specific)
|
||||||
|
|
||||||
|
When adding new files to `src/templates/trellis/scripts/`:
|
||||||
|
|
||||||
|
**Single registration point**: `src/templates/trellis/index.ts`
|
||||||
|
|
||||||
|
1. Add `export const xxxScript = readTemplate("scripts/path/file.py");`
|
||||||
|
2. Add to `getAllScripts()` Map
|
||||||
|
|
||||||
|
That's it. `commands/update.ts` uses `getAllScripts()` directly — no manual sync needed.
|
||||||
|
|
||||||
|
**Why this matters**: Without registration in `getAllScripts()`, `trellis update` won't sync the file to user projects. Bug fixes and features won't propagate.
|
||||||
|
|
||||||
|
**History**: Before v0.4.0-beta.3, `update.ts` had its own hand-maintained file list that frequently fell out of sync with `getAllScripts()`. This caused 11 Python files to be silently skipped during `trellis update`. The fix was to eliminate the duplicate list and use `getAllScripts()` as the single source of truth.
|
||||||
|
|
||||||
|
### Quick Checklist for New Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After adding a new .py file, verify it's in getAllScripts():
|
||||||
|
grep -l "newFileName" src/templates/trellis/index.ts # Should match
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Sync Convention
|
||||||
|
|
||||||
|
`.trellis/scripts/` (dogfooded) and `packages/cli/src/templates/trellis/scripts/` (template) must stay identical. After editing `.trellis/scripts/`, always sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -av --delete --exclude='__pycache__' .trellis/scripts/ packages/cli/src/templates/trellis/scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gotcha**: Running rsync with wrong source/destination paths can create nested garbage directories (e.g., `.trellis/scripts/packages/cli/...`). Always double-check paths before running.
|
||||||
@@ -0,0 +1,708 @@
|
|||||||
|
# Development Workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Plan before code** — figure out what to do before you start
|
||||||
|
2. **Specs injected, not remembered** — guidelines are injected via hook/skill, not recalled from memory
|
||||||
|
3. **Persist everything** — research, decisions, and lessons all go to files; conversations get compacted, files don't
|
||||||
|
4. **Incremental development** — one task at a time
|
||||||
|
5. **Capture learnings** — after each task, review and write new knowledge back to spec
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trellis System
|
||||||
|
|
||||||
|
### Developer Identity
|
||||||
|
|
||||||
|
On first use, initialize your identity:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/init_developer.py <your-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `.trellis/.developer` (gitignored) + `.trellis/workspace/<your-name>/`.
|
||||||
|
|
||||||
|
### Spec System
|
||||||
|
|
||||||
|
`.trellis/spec/` holds coding guidelines organized by package and layer.
|
||||||
|
|
||||||
|
- `.trellis/spec/<package>/<layer>/index.md` — entry point with **Pre-Development Checklist** + **Quality Check**. Actual guidelines live in the `.md` files it points to.
|
||||||
|
- `.trellis/spec/guides/index.md` — cross-package thinking guides.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/get_context.py --mode packages # list packages / layers
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to update spec**: new pattern/convention found · bug-fix prevention to codify · new technical decision.
|
||||||
|
|
||||||
|
### Task System
|
||||||
|
|
||||||
|
Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `task.json`, `prd.md`, optional `design.md`, optional `implement.md`, optional `research/`, and context manifests (`implement.jsonl`, `check.jsonl`) for sub-agent-capable platforms.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Task lifecycle
|
||||||
|
python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] [--parent <dir>]
|
||||||
|
python3 ./.trellis/scripts/task.py start <name> # set active task (session-scoped when available)
|
||||||
|
python3 ./.trellis/scripts/task.py current --source # show active task and source
|
||||||
|
python3 ./.trellis/scripts/task.py finish # clear active task (triggers after_finish hooks)
|
||||||
|
python3 ./.trellis/scripts/task.py archive <name> # move to archive/{year-month}/
|
||||||
|
python3 ./.trellis/scripts/task.py list [--mine] [--status <s>]
|
||||||
|
python3 ./.trellis/scripts/task.py list-archive
|
||||||
|
|
||||||
|
# Code-spec context (injected into implement/check agents via JSONL).
|
||||||
|
# `implement.jsonl` / `check.jsonl` are seeded on `task create` for sub-agent-capable
|
||||||
|
# platforms; the AI curates real spec + research entries during planning when needed.
|
||||||
|
python3 ./.trellis/scripts/task.py add-context <name> <action> <file> <reason>
|
||||||
|
python3 ./.trellis/scripts/task.py list-context <name> [action]
|
||||||
|
python3 ./.trellis/scripts/task.py validate <name>
|
||||||
|
|
||||||
|
# Task metadata
|
||||||
|
python3 ./.trellis/scripts/task.py set-branch <name> <branch>
|
||||||
|
python3 ./.trellis/scripts/task.py set-base-branch <name> <branch> # PR target
|
||||||
|
python3 ./.trellis/scripts/task.py set-scope <name> <scope>
|
||||||
|
|
||||||
|
# Hierarchy (parent/child)
|
||||||
|
python3 ./.trellis/scripts/task.py add-subtask <parent> <child>
|
||||||
|
python3 ./.trellis/scripts/task.py remove-subtask <parent> <child>
|
||||||
|
|
||||||
|
# PR creation
|
||||||
|
python3 ./.trellis/scripts/task.py create-pr [name] [--dry-run]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Run `python3 ./.trellis/scripts/task.py --help` to see the authoritative, up-to-date list.
|
||||||
|
|
||||||
|
**Current-task mechanism**: `task.py create` creates the task directory and (when session identity is available) auto-sets the per-session active-task pointer so the planning breadcrumb fires immediately. `task.py start` writes the same pointer (idempotent if already set) and flips `task.json.status` from `planning` to `in_progress`. State is stored under `.trellis/.runtime/sessions/`. If no context key is available from hook input, `TRELLIS_CONTEXT_ID`, or a platform-native session environment variable, there is no active task and `task.py start` fails with a session identity hint. `task.py finish` deletes the current session file (status unchanged). `task.py archive <task>` writes `status=completed`, moves the directory to `archive/`, and deletes any runtime session files that still point at the archived task.
|
||||||
|
|
||||||
|
### Workspace System
|
||||||
|
|
||||||
|
Records every AI session for cross-session tracking under `.trellis/workspace/<developer>/`.
|
||||||
|
|
||||||
|
- `journal-N.md` — session log. **Max 2000 lines per file**; a new `journal-(N+1).md` is auto-created when exceeded.
|
||||||
|
- `index.md` — personal index (total sessions, last active).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash" --summary "Summary"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/get_context.py # full session runtime
|
||||||
|
python3 ./.trellis/scripts/get_context.py --mode packages # available packages + spec layers
|
||||||
|
python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed guide for a workflow step
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below)
|
||||||
|
|
||||||
|
The [workflow-state:STATUS] blocks embedded in the ## Phase Index section
|
||||||
|
below are the SINGLE source of truth for the per-turn `<workflow-state>`
|
||||||
|
breadcrumb that every supported AI platform's UserPromptSubmit hook
|
||||||
|
reads. inject-workflow-state.py (Python platforms) and
|
||||||
|
inject-workflow-state.js (OpenCode plugin) only parse them — there is no
|
||||||
|
fallback dict baked into the scripts after v0.5.0-rc.0.
|
||||||
|
|
||||||
|
STATUS charset: [A-Za-z0-9_-]+. When the hook can't find a tag, it
|
||||||
|
degrades to a generic "Refer to workflow.md for current step." line —
|
||||||
|
intentionally visible so users notice and fix a broken workflow.md.
|
||||||
|
|
||||||
|
INVARIANT (test/regression.test.ts):
|
||||||
|
Every workflow-walkthrough step marked `[required · once]` must have a
|
||||||
|
matching enforcement line in its phase's [workflow-state:*] block. The
|
||||||
|
breadcrumb is the only per-turn channel; if a mandatory step isn't
|
||||||
|
mentioned there, the AI silently skips it (Phase 1 planning gate
|
||||||
|
skip and Phase 3.4 commit skip both manifested via this gap).
|
||||||
|
|
||||||
|
TAG ↔ PHASE scoping:
|
||||||
|
[workflow-state:no_task] → no active task; before Phase 1
|
||||||
|
[workflow-state:planning] → all of Phase 1 (status='planning')
|
||||||
|
[workflow-state:planning-inline] → Codex inline variant of Phase 1
|
||||||
|
[workflow-state:in_progress] → Phase 2 + Phase 3.2-3.4
|
||||||
|
(status stays 'in_progress' from
|
||||||
|
task.py start until task.py archive)
|
||||||
|
[workflow-state:in_progress-inline] → Codex inline variant of Phase 2/3
|
||||||
|
[workflow-state:completed] → currently DEAD: cmd_archive flips
|
||||||
|
status and moves the dir in the same
|
||||||
|
call, so the resolver loses the
|
||||||
|
pointer (block kept for a future
|
||||||
|
explicit in_progress→completed
|
||||||
|
transition)
|
||||||
|
|
||||||
|
Editing checklist:
|
||||||
|
- When you change a [workflow-state:STATUS] block, also check the
|
||||||
|
matching phase's `[required · once]` walkthrough steps for sync
|
||||||
|
- Run `trellis update` after editing to push the new bodies to
|
||||||
|
downstream user projects (block-level managed replacement)
|
||||||
|
- Full runtime contract:
|
||||||
|
.trellis/spec/cli/backend/workflow-state-contract.md
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Phase Index
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: Plan → classify, get task-creation consent, then write planning artifacts
|
||||||
|
Phase 2: Execute → implement only after task status is in_progress
|
||||||
|
Phase 3: Finish → verify, update spec, commit, and wrap up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Triage
|
||||||
|
|
||||||
|
- Simple conversation or small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session.
|
||||||
|
- Complex task: ask whether you may create a Trellis task and enter planning. If the user says no, do not do broad inline implementation; explain, clarify scope, or suggest a smaller split.
|
||||||
|
- User approval to create a task is not approval to start implementation. Planning still happens first.
|
||||||
|
|
||||||
|
### Planning Artifacts
|
||||||
|
|
||||||
|
- `prd.md` — requirements, constraints, and acceptance criteria. Do not put technical design or execution checklists here.
|
||||||
|
- `design.md` — technical design for complex tasks: boundaries, contracts, data flow, tradeoffs, compatibility, rollout / rollback shape.
|
||||||
|
- `implement.md` — execution plan for complex tasks: ordered checklist, validation commands, review gates, and rollback points.
|
||||||
|
- `implement.jsonl` / `check.jsonl` — spec and research manifests for sub-agent context. They do not replace `implement.md`.
|
||||||
|
- Lightweight tasks may be PRD-only. Complex tasks must have `prd.md`, `design.md`, and `implement.md` before `task.py start`.
|
||||||
|
|
||||||
|
### Parent / Child Task Trees
|
||||||
|
|
||||||
|
Use a parent task when one user request contains several independently verifiable deliverables. The parent task owns the source requirement set, the task map, cross-child acceptance criteria, and final integration review; it normally should not be the implementation target unless it also has direct work.
|
||||||
|
|
||||||
|
Use child tasks for deliverables that can be planned, implemented, checked, and archived independently. Parent/child structure is not a dependency system: if one child must wait for another, write that ordering in the child `prd.md` / `implement.md` and keep each child's acceptance criteria testable.
|
||||||
|
|
||||||
|
Create new children with `task.py create "<title>" --slug <name> --parent <parent-dir>`. Link existing tasks with `task.py add-subtask <parent> <child>`, and unlink mistakes with `task.py remove-subtask <parent> <child>`.
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown when there is no active task (before Phase 1) -->
|
||||||
|
|
||||||
|
[workflow-state:no_task]
|
||||||
|
No active task. First classify the current turn and ask for task-creation consent before creating any Trellis task.
|
||||||
|
Simple conversation / small task: ask only whether this turn should create a Trellis task. If the user says no, skip Trellis for this session.
|
||||||
|
Complex task: ask the user if you can create a Trellis task and enter the planning phase. If the user says no, explain, clarify scope, or suggest a smaller split.
|
||||||
|
[/workflow-state:no_task]
|
||||||
|
|
||||||
|
### Phase 1: Plan
|
||||||
|
- 1.0 Create task `[required · once]` (only after task-creation consent)
|
||||||
|
- 1.1 Requirement exploration `[required · repeatable]` (`prd.md`; complex tasks also need `design.md` + `implement.md`)
|
||||||
|
- 1.2 Research `[optional · repeatable]`
|
||||||
|
- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix (sub-agent-dispatch platforms only; inline platforms skip)
|
||||||
|
- 1.4 Activate task `[required · once]` (review gate, then `task.py start`; status → in_progress)
|
||||||
|
- 1.5 Completion criteria
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown throughout Phase 1 (status='planning') -->
|
||||||
|
|
||||||
|
[workflow-state:planning]
|
||||||
|
Load `trellis-brainstorm`; stay in planning.
|
||||||
|
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`.
|
||||||
|
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
|
||||||
|
Sub-agent mode: curate `implement.jsonl` and `check.jsonl` as spec/research manifests before start.
|
||||||
|
[/workflow-state:planning]
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown throughout Phase 1 when codex.dispatch_mode=inline.
|
||||||
|
Codex-only opt-in alternate to [workflow-state:planning]. The main agent
|
||||||
|
edits code directly in Phase 2, so jsonl curation is skipped —
|
||||||
|
the inline workflow loads `trellis-before-dev` instead of injecting JSONL
|
||||||
|
into a sub-agent. -->
|
||||||
|
|
||||||
|
[workflow-state:planning-inline]
|
||||||
|
Load `trellis-brainstorm`; stay in planning.
|
||||||
|
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; ask for review before `task.py start`.
|
||||||
|
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
|
||||||
|
Inline mode: skip jsonl curation; Phase 2 reads artifacts/specs via `trellis-before-dev`.
|
||||||
|
[/workflow-state:planning-inline]
|
||||||
|
|
||||||
|
### Phase 2: Execute
|
||||||
|
- 2.1 Implement `[required · repeatable]`
|
||||||
|
- 2.2 Quality check `[required · repeatable]`
|
||||||
|
- 2.3 Rollback `[on demand]`
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown while status='in_progress'.
|
||||||
|
Scope: all of Phase 2 + Phase 3.2-3.4 (status stays 'in_progress' from
|
||||||
|
task.py start until task.py archive; only archive flips it). The body
|
||||||
|
therefore must cover every required step from implementation through
|
||||||
|
commit, including Phase 3.3 spec update and Phase 3.4 commit. -->
|
||||||
|
|
||||||
|
Sub-agent dispatch protocol applies to all platforms and all sub-agents, including class-2 Codex/Gemini/Qoder/Copilot/ZCode/Reasonix/Trae and `trellis-research`: every dispatch prompt starts with `Active task: <task path from task.py current>` before role-specific instructions.
|
||||||
|
|
||||||
|
[workflow-state:in_progress]
|
||||||
|
Tools: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill; there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes.
|
||||||
|
Flow: `trellis-implement` -> `trellis-check` -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`.
|
||||||
|
Main-session default: dispatch implement/check sub-agents. Sub-agent self-exemption: if already running as `trellis-implement`, do NOT spawn another `trellis-implement` or `trellis-check`; if already running as `trellis-check`, do NOT spawn another `trellis-check` or `trellis-implement`. Dispatch is main session only.
|
||||||
|
Dispatch prompt starts with `Active task: <task path from task.py current>`. Read context: jsonl entries -> `prd.md` -> `design.md if present` -> `implement.md if present`.
|
||||||
|
[/workflow-state:in_progress]
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown while status='in_progress' when
|
||||||
|
codex.dispatch_mode=inline. Codex-only opt-in alternate to
|
||||||
|
[workflow-state:in_progress]. The main session edits code directly
|
||||||
|
instead of dispatching sub-agents. -->
|
||||||
|
|
||||||
|
[workflow-state:in_progress-inline]
|
||||||
|
Flow: `trellis-before-dev` -> edit -> `trellis-check` -> validation -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`.
|
||||||
|
Do not dispatch implement/check sub-agents in inline mode.
|
||||||
|
Read context: `prd.md` -> `design.md if present` -> `implement.md if present`, plus relevant spec/research loaded by skills.
|
||||||
|
[/workflow-state:in_progress-inline]
|
||||||
|
|
||||||
|
### Phase 3: Finish
|
||||||
|
- 3.2 Debug retrospective `[on demand]`
|
||||||
|
- 3.3 Spec update `[required · once]`
|
||||||
|
- 3.4 Commit changes `[required · once]`
|
||||||
|
- 3.5 Wrap-up reminder
|
||||||
|
|
||||||
|
> Note: step 3.1 was folded into 2.2 (last-iteration full-scope check) and 3.4 (commit preamble). Numbering kept stable to avoid breaking external references.
|
||||||
|
|
||||||
|
<!-- Per-turn breadcrumb: shown while status='completed'.
|
||||||
|
Currently DEAD in normal flow: cmd_archive writes status='completed' in
|
||||||
|
the same call that moves the task dir to archive/, so the active-task
|
||||||
|
resolver loses the pointer and the hook never fires on archived tasks.
|
||||||
|
Block preserved for a future status-transition redesign (e.g. an
|
||||||
|
explicit in_progress→completed command). Edit through the same spec
|
||||||
|
channel as the live blocks. -->
|
||||||
|
|
||||||
|
[workflow-state:completed]
|
||||||
|
Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first.
|
||||||
|
[/workflow-state:completed]
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
1. Identify which Phase you're in, then continue from the next step there
|
||||||
|
2. Run steps in order inside each Phase; `[required]` steps can't be skipped
|
||||||
|
3. Phases can roll back (e.g., Execute reveals a prd defect → return to Plan to fix, then re-enter Execute)
|
||||||
|
4. Steps tagged `[once]` are skipped if the output already exists; don't re-run
|
||||||
|
5. Artifact presence informs the next step; missing `design.md` / `implement.md` is valid for lightweight tasks and incomplete planning for complex tasks.
|
||||||
|
|
||||||
|
### Active Task Routing
|
||||||
|
|
||||||
|
When a user request matches one of these intents inside an active task, route first, then load the detailed phase step if needed.
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
- Planning or unclear requirements -> `trellis-brainstorm`.
|
||||||
|
- `in_progress` implementation/check -> dispatch `trellis-implement` / `trellis-check`.
|
||||||
|
- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`.
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
[codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
- Planning or unclear requirements -> `trellis-brainstorm`.
|
||||||
|
- Before editing -> `trellis-before-dev`; after editing -> `trellis-check`.
|
||||||
|
- Repeated debugging -> `trellis-break-loop`; spec updates -> `trellis-update-spec`.
|
||||||
|
|
||||||
|
[/codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
### Guardrails
|
||||||
|
|
||||||
|
- Task creation approval is not implementation approval; implementation waits for `task.py start` after artifact review.
|
||||||
|
- PRD-only is valid for lightweight tasks; complex tasks need `design.md` + `implement.md`.
|
||||||
|
- Planning must be persisted to task artifacts; checks must run before reporting completion.
|
||||||
|
|
||||||
|
### Loading Step Detail
|
||||||
|
|
||||||
|
At each step, run this to fetch detailed guidance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/get_context.py --mode phase --step <step>
|
||||||
|
# e.g. python3 ./.trellis/scripts/get_context.py --mode phase --step 1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Plan
|
||||||
|
|
||||||
|
Goal: classify the request, get task-creation consent when a task is needed, and produce the planning artifacts required before implementation.
|
||||||
|
|
||||||
|
#### 1.0 Create task `[required · once]`
|
||||||
|
|
||||||
|
Create the task directory only after task-creation consent. The command sets status to `planning`, writes `task.json`, creates a default `prd.md`, and auto-targets the new task when session identity is available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/task.py create "<task title>" --slug <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
`--slug` is the human-readable name only. Do **not** include the `MM-DD-` date prefix; `task.py create` adds that prefix automatically.
|
||||||
|
|
||||||
|
For task trees, create the parent task first and then create each child with `--parent <parent-dir>`. Do not start the parent just because children exist; start the child that owns the next independently verifiable deliverable.
|
||||||
|
|
||||||
|
After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to stay in planning.
|
||||||
|
|
||||||
|
Run only `create` here — do not also run `start`. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before planning artifacts are reviewed. Save `start` for step 1.4.
|
||||||
|
|
||||||
|
Skip when `python3 ./.trellis/scripts/task.py current --source` already points to a task.
|
||||||
|
|
||||||
|
#### 1.1 Requirement exploration `[required · repeatable]`
|
||||||
|
|
||||||
|
Load the `trellis-brainstorm` skill and explore requirements interactively with the user per the skill's guidance.
|
||||||
|
|
||||||
|
The brainstorm skill will guide you to:
|
||||||
|
- Ask one question at a time
|
||||||
|
- Prefer researching over asking the user
|
||||||
|
- Prefer offering options over open-ended questions
|
||||||
|
- Update `prd.md` immediately after each user answer
|
||||||
|
- Split large scopes into a parent task plus child tasks when the deliverables can be verified independently
|
||||||
|
- Keep `prd.md` focused on requirements and acceptance criteria
|
||||||
|
- For complex tasks, produce `design.md` and `implement.md` before implementation starts
|
||||||
|
|
||||||
|
When considering a parent/child split:
|
||||||
|
- Use a parent task when one request contains several independently verifiable deliverables.
|
||||||
|
- Parent tasks own source requirements, child-task mapping, cross-child acceptance criteria, and final integration review.
|
||||||
|
- Child tasks own actual deliverables that can be planned, implemented, checked, and archived independently.
|
||||||
|
- Parent/child structure is not a dependency system. If child B depends on child A, write that ordering in child B's `prd.md` / `implement.md`.
|
||||||
|
- Start the child task that owns the next deliverable. Do not start the parent unless the parent itself has direct implementation work.
|
||||||
|
|
||||||
|
Return to this step whenever requirements change and revise the relevant artifact.
|
||||||
|
|
||||||
|
#### 1.2 Research `[optional · repeatable]`
|
||||||
|
|
||||||
|
Research can happen at any time during requirement exploration. It isn't limited to local code — you can use any available tool (MCP servers, skills, web search, etc.) to look up external information, including third-party library docs, industry practices, API references, etc.
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
Spawn the research sub-agent:
|
||||||
|
|
||||||
|
- **Agent type**: `trellis-research`
|
||||||
|
- **Task description**: Research <specific question>
|
||||||
|
- **Key requirement**: Research output MUST be persisted to `{TASK_DIR}/research/`
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
[codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. (For `codex-inline` this avoids the `fork_turns="none"` isolation that prevents `trellis-research` sub-agents from resolving the active task path.)
|
||||||
|
|
||||||
|
[/codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
**Research artifact conventions**:
|
||||||
|
- One file per research topic (e.g. `research/auth-library-comparison.md`)
|
||||||
|
- Record third-party library usage examples, API references, version constraints in files
|
||||||
|
- Note relevant spec file paths you discovered for later reference
|
||||||
|
|
||||||
|
Brainstorm and research can interleave freely — pause to research a technical question, then return to talk with the user.
|
||||||
|
|
||||||
|
**Key principle**: Research output must be written to files, not left only in the chat. Conversations get compacted; files don't.
|
||||||
|
|
||||||
|
#### 1.3 Configure context `[required · once]`
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec/research context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries.
|
||||||
|
|
||||||
|
**Location**: `{TASK_DIR}/implement.jsonl` and `{TASK_DIR}/check.jsonl` (already exist).
|
||||||
|
|
||||||
|
**Format**: one JSON object per line — `{"file": "<path>", "reason": "<why>"}`. Paths are repo-root relative.
|
||||||
|
|
||||||
|
**What to put in**:
|
||||||
|
- **Spec files** — `.trellis/spec/<package>/<layer>/index.md` and any specific guideline files (`error-handling.md`, `conventions.md`, etc.) relevant to this task
|
||||||
|
- **Research files** — `{TASK_DIR}/research/*.md` that the sub-agent will need to consult
|
||||||
|
|
||||||
|
**What NOT to put in**:
|
||||||
|
- Code files (`src/**`, `packages/**/*.ts`, etc.) — those are read by the sub-agent during implementation, not pre-registered here
|
||||||
|
- Files you're about to modify — same reason
|
||||||
|
|
||||||
|
**Split between the two files**:
|
||||||
|
- `implement.jsonl` → specs + research the implement sub-agent needs to write code correctly
|
||||||
|
- `check.jsonl` → specs for the check sub-agent (quality guidelines, check conventions, same research if needed)
|
||||||
|
|
||||||
|
These manifests do not replace `implement.md`. `implement.md` is the human-readable execution plan for a complex task; jsonl files only list context files to inject or load.
|
||||||
|
|
||||||
|
**How to discover relevant specs**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/get_context.py --mode packages
|
||||||
|
```
|
||||||
|
|
||||||
|
Lists every package + its spec layers with paths. Pick the entries that match this task's domain.
|
||||||
|
|
||||||
|
**How to append entries**:
|
||||||
|
|
||||||
|
Either edit the jsonl file directly in your editor, or use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" implement "<path>" "<reason>"
|
||||||
|
python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reason>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete the seed `_example` line once real entries exist (optional — it's skipped automatically by consumers).
|
||||||
|
|
||||||
|
Ready gate: both `implement.jsonl` and `check.jsonl` must contain at least one real `{"file": "...", "reason": "..."}` entry before `task.py start`. The seed `_example` row alone is not ready.
|
||||||
|
|
||||||
|
Skip this step only when both files already have real curated entries.
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
[codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
Skip this step. Context is loaded directly by the `trellis-before-dev` skill in Phase 2.
|
||||||
|
|
||||||
|
[/codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
#### 1.4 Activate task `[required · once]`
|
||||||
|
|
||||||
|
After artifact review, flip the task status to `in_progress`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./.trellis/scripts/task.py start <task-dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
For lightweight tasks, `prd.md` can be enough. For complex tasks, `prd.md`, `design.md`, and `implement.md` must exist and be reviewed before start. On sub-agent-dispatch platforms, `implement.jsonl` and `check.jsonl` must both have real curated entries before start. Runtime consumers tolerate missing or seed-only manifests for compatibility, but that tolerance is not a planning-ready state.
|
||||||
|
|
||||||
|
After this command succeeds, the breadcrumb auto-switches to `[workflow-state:in_progress]`, and the rest of Phase 2 / 3 follows.
|
||||||
|
|
||||||
|
If `task.py start` errors with a session-identity message (no context key from hook input, `TRELLIS_CONTEXT_ID`, or platform-native session env), follow the hint in the error to set up session identity, then retry.
|
||||||
|
|
||||||
|
#### 1.5 Completion criteria
|
||||||
|
|
||||||
|
| Condition | Required |
|
||||||
|
|------|:---:|
|
||||||
|
| `prd.md` exists | ✅ |
|
||||||
|
| User confirms task should enter implementation | ✅ |
|
||||||
|
| `task.py start` has been run (status = in_progress) | ✅ |
|
||||||
|
| `research/` has artifacts (complex tasks) | recommended |
|
||||||
|
| `design.md` exists (complex tasks) | ✅ |
|
||||||
|
| `implement.md` exists (complex tasks) | ✅ |
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
| `implement.jsonl` and `check.jsonl` each contain at least one real curated entry (seed row does not count) | ✅ |
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Execute
|
||||||
|
|
||||||
|
Goal: turn reviewed planning artifacts into code that passes quality checks.
|
||||||
|
|
||||||
|
#### 2.1 Implement `[required · repeatable]`
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi]
|
||||||
|
|
||||||
|
Spawn the implement sub-agent:
|
||||||
|
|
||||||
|
- **Agent type**: `trellis-implement`
|
||||||
|
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
|
||||||
|
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`.
|
||||||
|
|
||||||
|
The platform hook/plugin auto-handles:
|
||||||
|
- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt
|
||||||
|
- Injects `prd.md`, `design.md` if present, and `implement.md` if present
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi]
|
||||||
|
|
||||||
|
[codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
Spawn the implement sub-agent:
|
||||||
|
|
||||||
|
- **Agent type**: `trellis-implement`
|
||||||
|
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
|
||||||
|
- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then explicitly say the spawned agent is already `trellis-implement` and must implement directly without spawning another `trellis-implement` / `trellis-check`.
|
||||||
|
|
||||||
|
The pull-based sub-agent definition auto-handles the context load requirement:
|
||||||
|
- Resolves the active task with `task.py current --source`, then reads `prd.md`, `design.md` if present, and `implement.md` if present
|
||||||
|
- Reads `implement.jsonl` and requires the agent to load each referenced spec/research file before coding
|
||||||
|
|
||||||
|
[/codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
[Kiro]
|
||||||
|
|
||||||
|
Spawn the implement sub-agent:
|
||||||
|
|
||||||
|
- **Agent type**: `trellis-implement`
|
||||||
|
- **Task description**: Implement the reviewed task artifacts, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check
|
||||||
|
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`.
|
||||||
|
|
||||||
|
The platform prelude auto-handles the context load requirement:
|
||||||
|
- Reads `implement.jsonl` and injects referenced spec/research files into the agent prompt
|
||||||
|
- Injects `prd.md`, `design.md` if present, and `implement.md` if present
|
||||||
|
|
||||||
|
[/Kiro]
|
||||||
|
|
||||||
|
[codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
1. Load the `trellis-before-dev` skill to read project guidelines
|
||||||
|
2. Read `{TASK_DIR}/prd.md`, then `design.md` if present, then `implement.md` if present
|
||||||
|
3. Consult materials under `{TASK_DIR}/research/`
|
||||||
|
4. Implement the code per reviewed artifacts
|
||||||
|
5. Run project lint and type-check
|
||||||
|
|
||||||
|
[/codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
#### 2.2 Quality check `[required · repeatable]`
|
||||||
|
|
||||||
|
[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
Spawn the check sub-agent:
|
||||||
|
|
||||||
|
- **Agent type**: `trellis-check`
|
||||||
|
- **Task description**: Review all code changes against specs and task artifacts; fix any findings directly; ensure lint and type-check pass
|
||||||
|
- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`.
|
||||||
|
|
||||||
|
The check agent's job:
|
||||||
|
- Review code changes against specs
|
||||||
|
- Review code changes against `prd.md`, `design.md` if present, and `implement.md` if present
|
||||||
|
- Auto-fix issues it finds
|
||||||
|
- Run lint and typecheck to verify
|
||||||
|
|
||||||
|
[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae]
|
||||||
|
|
||||||
|
[codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
Load the `trellis-check` skill and verify the code per its guidance:
|
||||||
|
- Spec compliance
|
||||||
|
- lint / type-check / tests
|
||||||
|
- Cross-layer consistency (when changes span layers)
|
||||||
|
|
||||||
|
If issues are found → fix → re-check, until green.
|
||||||
|
|
||||||
|
[/codex-inline, Kilo, Antigravity, Devin]
|
||||||
|
|
||||||
|
**Final pass (before Phase 3.4 commit)**: the last 2.2 of a task must run full-scope, not just on the latest implement chunk. List all affected packages with `python3 ./.trellis/scripts/get_context.py --mode packages`, then load each package's spec index Quality Check section. This catches cross-layer / multi-package issues a mid-iteration local 2.2 cannot.
|
||||||
|
|
||||||
|
#### 2.3 Rollback `[on demand]`
|
||||||
|
|
||||||
|
- `check` reveals a prd defect → return to Phase 1, fix `prd.md`, then redo 2.1
|
||||||
|
- Implementation went wrong → revert code, redo 2.1
|
||||||
|
- Need more research → research (same as Phase 1.2), write findings into `research/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Finish
|
||||||
|
|
||||||
|
Goal: ensure code quality, capture lessons, record the work.
|
||||||
|
|
||||||
|
#### 3.2 Debug retrospective `[on demand]`
|
||||||
|
|
||||||
|
If this task involved repeated debugging (the same issue was fixed multiple times), load the `trellis-break-loop` skill to:
|
||||||
|
- Classify the root cause
|
||||||
|
- Explain why earlier fixes failed
|
||||||
|
- Propose prevention
|
||||||
|
|
||||||
|
The goal is to capture debugging lessons so the same class of issue doesn't recur.
|
||||||
|
|
||||||
|
#### 3.3 Spec update `[required · once]`
|
||||||
|
|
||||||
|
Load the `trellis-update-spec` skill and review whether this task produced new knowledge worth recording:
|
||||||
|
- Newly discovered patterns or conventions
|
||||||
|
- Pitfalls you hit
|
||||||
|
- New technical decisions
|
||||||
|
|
||||||
|
Update the docs under `.trellis/spec/` accordingly. Even if the conclusion is "nothing to update", walk through the judgment.
|
||||||
|
|
||||||
|
#### 3.4 Commit changes `[required · once]`
|
||||||
|
|
||||||
|
**Spec-sync preamble**: before drafting commits, ask: did this task fix a bug or surface non-obvious knowledge that should land in `.trellis/spec/` so future-you (or future-AI) doesn't repeat the mistake? If yes, return to Phase 3.3 first — spec writes belong in the same task's commit batch, not as a forgotten follow-up.
|
||||||
|
|
||||||
|
The AI drives a batched commit of this task's code changes so `/finish-work` can run cleanly afterwards. Goal: produce work commits FIRST, then bookkeeping (archive + journal) commits land after — never interleaved.
|
||||||
|
|
||||||
|
**Step-by-step**:
|
||||||
|
|
||||||
|
1. **Inspect dirty state**:
|
||||||
|
```bash
|
||||||
|
git status --porcelain
|
||||||
|
```
|
||||||
|
Snapshot every dirty path. If the working tree is clean, skip to 3.5.
|
||||||
|
|
||||||
|
2. **Learn commit style** from recent history (so drafted messages blend in):
|
||||||
|
```bash
|
||||||
|
git log --oneline -5
|
||||||
|
```
|
||||||
|
Note the prefix convention (`feat:` / `fix:` / `chore:` / `docs:` ...), language (中文/English), and length style.
|
||||||
|
|
||||||
|
3. **Classify dirty files into two groups**:
|
||||||
|
- **AI-edited this session** — files you wrote/edited via Edit/Write/Bash tool calls in this session. You know what changed and why.
|
||||||
|
- **Unrecognized** — dirty files you did NOT touch this session (could be the user's manual edits, leftover WIP from a previous session, or unrelated work). Do NOT silently include these.
|
||||||
|
|
||||||
|
4. **Draft a commit plan**. Group AI-edited files into logical commits (1 commit per coherent change unit, not 1 commit per file). Each entry: `<commit message>` + file list. List unrecognized files separately at the bottom.
|
||||||
|
|
||||||
|
5. **Present the plan once, ask for one-shot confirmation**. Format:
|
||||||
|
```
|
||||||
|
Proposed commits (in order):
|
||||||
|
1. <message>
|
||||||
|
- <file>
|
||||||
|
- <file>
|
||||||
|
2. <message>
|
||||||
|
- <file>
|
||||||
|
|
||||||
|
Unrecognized dirty files (NOT in any commit — confirm include/exclude):
|
||||||
|
- <file>
|
||||||
|
- <file>
|
||||||
|
|
||||||
|
Reply 'ok' / '行' to execute. Reply with edits, or '我自己来' / 'manual' to abort.
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **On confirmation**: run `git add <files>` + `git commit -m "<msg>"` for each batch in order. Do not amend. Do not push.
|
||||||
|
|
||||||
|
7. **On rejection** (user replies "不行" / "我自己来" / "manual" / any pushback on the plan): stop. Do not attempt a second plan. The user will commit by hand; you skip ahead to 3.5 once they confirm.
|
||||||
|
|
||||||
|
**Rules**:
|
||||||
|
- No `git commit --amend` anywhere — three-stage three-commit flow (work commits → archive commit → journal commit).
|
||||||
|
- Never push to remote in this step.
|
||||||
|
- If the user wants different message wording but accepts the file grouping, edit the message and re-confirm once — but if they reject the grouping, exit to manual mode.
|
||||||
|
- The batched plan is one prompt; do not prompt per commit.
|
||||||
|
|
||||||
|
#### 3.5 Wrap-up reminder
|
||||||
|
|
||||||
|
After the above, remind the user they can run `/finish-work` to wrap up (archive the task, record the session).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customizing Trellis (for forks)
|
||||||
|
|
||||||
|
This section is for developers who want to modify the Trellis workflow itself. All customization is done by editing this file; the scripts are parsers only.
|
||||||
|
|
||||||
|
### Changing what a step means
|
||||||
|
|
||||||
|
Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. Critical invariants:
|
||||||
|
- No active task must triage first and ask for task-creation consent before creating a Trellis task.
|
||||||
|
- Planning must distinguish lightweight PRD-only tasks from complex tasks that require `prd.md`, `design.md`, and `implement.md` before start.
|
||||||
|
- Every required execution path must keep the Phase 3.4 commit reminder reachable before `/trellis:finish-work`.
|
||||||
|
|
||||||
|
All tag blocks live in the `## Phase Index` section above, immediately after each phase summary:
|
||||||
|
|
||||||
|
| Scope | Corresponding tag |
|
||||||
|
|---|---|
|
||||||
|
| No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) |
|
||||||
|
| All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) |
|
||||||
|
| Codex inline Phase 1 | `[workflow-state:planning-inline]` |
|
||||||
|
| Phase 2 + Phase 3.2–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) |
|
||||||
|
| Codex inline Phase 2 + Phase 3.2–3.4 | `[workflow-state:in_progress-inline]` |
|
||||||
|
| After Phase 3.5 (archived) | `[workflow-state:completed]` (after Phase 3 summary; **currently DEAD**) |
|
||||||
|
|
||||||
|
### Changing the per-turn prompt text
|
||||||
|
|
||||||
|
Directly edit the body of the corresponding `[workflow-state:STATUS]` block. After editing, run `trellis update` (if you're a template maintainer) or restart your AI session (if you're customizing your own project) — no script changes required.
|
||||||
|
|
||||||
|
### Adding a custom status
|
||||||
|
|
||||||
|
Add a new block:
|
||||||
|
|
||||||
|
```
|
||||||
|
[workflow-state:my-status]
|
||||||
|
your per-turn prompt text
|
||||||
|
[/workflow-state:my-status]
|
||||||
|
```
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
- STATUS charset: `[A-Za-z0-9_-]+` (underscores and hyphens allowed, e.g. `in-review`, `blocked-by-team`)
|
||||||
|
- A lifecycle hook must write `task.json.status` to your custom value, otherwise the tag is never read
|
||||||
|
- Lifecycle hooks live in `task.json.hooks.after_*` and bind to one of `after_create / after_start / after_finish / after_archive`
|
||||||
|
|
||||||
|
### Adding a lifecycle hook
|
||||||
|
|
||||||
|
Add a `hooks` field to your `task.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"after_finish": [
|
||||||
|
"your-script-or-command-here"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported events: `after_create / after_start / after_finish / after_archive`. Note that `after_finish` ≠ a status change (it only clears the active-task pointer); use `after_archive` for "task is done" notifications.
|
||||||
|
|
||||||
|
### Full contract
|
||||||
|
|
||||||
|
For the workflow state machine's runtime contract, the locations of all status writers, pseudo-statuses (`no_task` / `stale_<source_type>`), the hook reachability matrix, and other deep details, see:
|
||||||
|
|
||||||
|
- `.trellis/spec/cli/backend/workflow-state-contract.md` — runtime contract + writer table + test invariants
|
||||||
|
- `.trellis/scripts/inject-workflow-state.py` — actual parser (reads workflow.md only, no embedded text)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["golang.go"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "启动 oci-portal 后端",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/server",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"DATA_KEY": "dev-data-key",
|
||||||
|
"JWT_SECRET": "dev-jwt-secret",
|
||||||
|
"ADMIN_PASSWORD": "admin123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "调试当前测试文件",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "test",
|
||||||
|
"program": "${fileDirname}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"go.toolsManagement.autoUpdate": true,
|
||||||
|
"go.testFlags": ["-count=1"],
|
||||||
|
"[go]": {
|
||||||
|
"editor.defaultFormatter": "golang.go",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[go.mod]": {
|
||||||
|
"editor.defaultFormatter": "golang.go",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"gopls": {
|
||||||
|
"ui.diagnostic.staticcheck": true,
|
||||||
|
"formatting.local": "oci-portal"
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.DS_Store": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "go: build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go build ./...",
|
||||||
|
"group": { "kind": "build", "isDefault": true },
|
||||||
|
"problemMatcher": ["$go"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go test ./...",
|
||||||
|
"group": { "kind": "test", "isDefault": true },
|
||||||
|
"problemMatcher": ["$go"]
|
||||||
|
},
|
||||||
|
{ "label": "go: vet", "type": "shell", "command": "go vet ./...", "problemMatcher": ["$go"] },
|
||||||
|
{ "label": "go: tidy", "type": "shell", "command": "go mod tidy", "problemMatcher": [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
oci-portal:自托管 OCI 多租户管理面板后端(Go + Gin + GORM + SQLite,可选 MySQL/PostgreSQL)。本文件面向 AI 编码助手;人类文档见 [README.md](README.md)。
|
||||||
|
|
||||||
|
## 语言与交付
|
||||||
|
|
||||||
|
- 用中文回答与写注释
|
||||||
|
- commit message 用中文,不超过 50 字
|
||||||
|
- 函数不超过 30 行
|
||||||
|
|
||||||
|
## 仓库操作铁律
|
||||||
|
|
||||||
|
- **仅当用户明确提出「提交」「推送」时才执行 git 操作**
|
||||||
|
- 双 remote:`gitea`(日常默认)与 `github`(公共镜像);推送默认到 `gitea`,仅用户明示「发版」或「推到 github」时才推 `github`
|
||||||
|
|
||||||
|
## 编码规范(写代码前读)
|
||||||
|
|
||||||
|
- 规范入口:[.trellis/spec/backend/index.md](.trellis/spec/backend/index.md)(目录结构 / 错误处理 / 并发 / 测试等分篇)
|
||||||
|
- 云 API 调用只出现在 `internal/oci/`;阻塞/远程调用第一参数 `ctx context.Context`;错误用 `%w` 包装向上返回
|
||||||
|
- 错误消息与日志绝不携带私钥、口令、令牌等敏感信息
|
||||||
|
- 新逻辑必须带 table-driven 测试
|
||||||
|
|
||||||
|
## 质量门(提交前必过)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -l . && go vet ./... && go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档同步
|
||||||
|
|
||||||
|
- 改环境变量 / 部署形态 / 对外行为时,同步更新 README.md;CHANGELOG.md 只在发版时写对应版本段,日常提交不记录
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TRELLIS:START -->
|
||||||
|
# Trellis Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
This project is managed by Trellis. The working knowledge you need lives under `.trellis/`:
|
||||||
|
|
||||||
|
- `.trellis/workflow.md` — development phases, when to create tasks, skill routing
|
||||||
|
- `.trellis/spec/` — package- and layer-scoped coding guidelines (read before writing code in a given layer)
|
||||||
|
- `.trellis/workspace/` — per-developer journals and session traces
|
||||||
|
- `.trellis/tasks/` — active and archived tasks (PRDs, research, jsonl context)
|
||||||
|
|
||||||
|
If a Trellis command is available on your platform (e.g. `/trellis:finish-work`, `/trellis:continue`), prefer it over manual steps. Not every platform exposes every command.
|
||||||
|
|
||||||
|
If you're using Codex or another agent-capable tool, additional project-scoped helpers may live in:
|
||||||
|
- `.agents/skills/` — reusable Trellis skills
|
||||||
|
- `.codex/agents/` — optional custom subagents
|
||||||
|
|
||||||
|
Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`.
|
||||||
|
|
||||||
|
<!-- TRELLIS:END -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
||||||
|
|
||||||
|
## [0.0.1] - 2026-07-09
|
||||||
|
|
||||||
|
首个版本:自托管 OCI 多租户管理面板后端。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 多租户管理:OCI API Key 配置集中管理(AES-256-GCM 加密落库)、分组、批量测活、账户画像
|
||||||
|
- 资源管理:实例、网络(VCN/子网/安全列表/IPv6)、块存储、限额与成本,多区域/多区间
|
||||||
|
- 自动化任务:抢机、测活、成本同步、AI 渠道探测四类 cron 任务,支持熔断与执行日志
|
||||||
|
- 网页控制台:串行控制台(xterm)与 VNC(noVNC),两跳 SSH 隧道
|
||||||
|
- AI 网关:OpenAI / Anthropic 兼容端点转发 OCI GenAI,号池负载均衡、熔断探测、密钥与用量日志
|
||||||
|
- 日志回传:OCI Audit 事件经 Connector Hub → Notifications HTTPS 订阅入库,一键创建链路
|
||||||
|
- 租户治理:IAM 用户/MFA/API Key、密码策略、SAML 身份提供商、通知收件人
|
||||||
|
- 登录安全:JWT + bcrypt、TOTP、OIDC/GitHub 外部登录、登录锁定、IP 限速、操作审计;Telegram 模板化通知
|
||||||
|
- 交付形态:前端产物 go:embed 单文件、SQLite 默认(MySQL/PostgreSQL experimental)、Swagger 文档、Docker/Compose、多架构(amd64/arm64)Release 流水线
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
oci-portal:自托管 OCI 多租户管理面板后端(Go + Gin + GORM + SQLite,可选 MySQL/PostgreSQL)。本文件面向 AI 编码助手;人类文档见 [README.md](README.md)。
|
||||||
|
|
||||||
|
## 语言与交付
|
||||||
|
|
||||||
|
- 用中文回答与写注释
|
||||||
|
- commit message 用中文,不超过 50 字
|
||||||
|
- 函数不超过 30 行
|
||||||
|
|
||||||
|
## 仓库操作铁律
|
||||||
|
|
||||||
|
- **仅当用户明确提出「提交」「推送」时才执行 git 操作**
|
||||||
|
- 双 remote:`gitea`(日常默认)与 `github`(公共镜像);推送默认到 `gitea`,仅用户明示「发版」或「推到 github」时才推 `github`
|
||||||
|
|
||||||
|
## 编码规范(写代码前读)
|
||||||
|
|
||||||
|
- 规范入口:[.trellis/spec/backend/index.md](.trellis/spec/backend/index.md)(目录结构 / 错误处理 / 并发 / 测试等分篇)
|
||||||
|
- 云 API 调用只出现在 `internal/oci/`;阻塞/远程调用第一参数 `ctx context.Context`;错误用 `%w` 包装向上返回
|
||||||
|
- 错误消息与日志绝不携带私钥、口令、令牌等敏感信息
|
||||||
|
- 新逻辑必须带 table-driven 测试
|
||||||
|
|
||||||
|
## 质量门(提交前必过)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -l . && go vet ./... && go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档同步
|
||||||
|
|
||||||
|
- 改环境变量 / 部署形态 / 对外行为时,同步更新 README.md;CHANGELOG.md 只在发版时写对应版本段,日常提交不记录
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TRELLIS:START -->
|
||||||
|
# Trellis Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
This project is managed by Trellis. The working knowledge you need lives under `.trellis/`:
|
||||||
|
|
||||||
|
- `.trellis/workflow.md` — development phases, when to create tasks, skill routing
|
||||||
|
- `.trellis/spec/` — package- and layer-scoped coding guidelines (read before writing code in a given layer)
|
||||||
|
- `.trellis/workspace/` — per-developer journals and session traces
|
||||||
|
- `.trellis/tasks/` — active and archived tasks (PRDs, research, jsonl context)
|
||||||
|
|
||||||
|
If a Trellis command is available on your platform (e.g. `/trellis:finish-work`, `/trellis:continue`), prefer it over manual steps. Not every platform exposes every command.
|
||||||
|
|
||||||
|
If you're using Codex or another agent-capable tool, additional project-scoped helpers may live in:
|
||||||
|
- `.agents/skills/` — reusable Trellis skills
|
||||||
|
- `.codex/agents/` — optional custom subagents
|
||||||
|
|
||||||
|
Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`.
|
||||||
|
|
||||||
|
<!-- TRELLIS:END -->
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 两阶段构建 + artifact 导出层(build stage 交叉编译,多平台构建无需 QEMU):
|
||||||
|
# 镜像: docker build -t oci-portal .
|
||||||
|
# 二进制: docker buildx build --platform linux/amd64,linux/arm64 --target artifact \
|
||||||
|
# --output type=local,dest=./output . # 产物在 output/linux_{amd64,arm64}/
|
||||||
|
#
|
||||||
|
# 完整产物需先把前端 dist 放进 internal/webui/dist(否则嵌入的是占位页):
|
||||||
|
# 本地: 前端仓库 npm run build 后拷入,或从前端 Release 下载 dist.zip 解压
|
||||||
|
# CI: release.yml 从前端仓库 latest Release 下载 dist.zip(见 .gitea/.github workflows)
|
||||||
|
|
||||||
|
# stage 1: 后端(嵌入 dist);跑在构建机原生平台上交叉编译,多平台构建无需 QEMU
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . ./
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o /oci-portal-server ./cmd/server
|
||||||
|
|
||||||
|
# artifact: 仅承载二进制,供 buildx --output type=local 导出(release 流程用)
|
||||||
|
FROM scratch AS artifact
|
||||||
|
COPY --from=build /oci-portal-server /oci-portal-server
|
||||||
|
|
||||||
|
# stage 2: 运行层(distroless/static 自带系统 CA,OCI API 的 HTTPS 依赖它;nonroot 用户)
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
COPY --from=build /oci-portal-server /oci-portal-server
|
||||||
|
# SQLite 数据卷;DB_PATH 指向卷内路径(见 docker-compose.yml)
|
||||||
|
VOLUME ["/data"]
|
||||||
|
EXPOSE 8080
|
||||||
|
# distroless 无 shell,探活用二进制自检 flag
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
|
||||||
|
CMD ["/oci-portal-server", "-healthcheck"]
|
||||||
|
ENTRYPOINT ["/oci-portal-server"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Wang Defa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<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 / Anthropic 兼容端点转发 OCI GenAI(对话/Responses/Embeddings),号池渠道加权负载均衡、熔断探测、密钥管理与用量日志
|
||||||
|
- **日志回传**:OCI Audit 事件经 Connector Hub → Notifications HTTPS 订阅回传入库,一键创建链路
|
||||||
|
- **租户治理**:IAM 用户/MFA/API Key 管理、密码策略、身份提供商(SAML)、通知收件人
|
||||||
|
- **安全**:JWT + bcrypt、TOTP 两步验证、OIDC/GitHub 外部登录、登录锁定、IP 限速、真实 IP 头可配、系统操作审计
|
||||||
|
- **通知**:Telegram 模板化推送(测活/抢机/成本/锁定等事件)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 二进制运行
|
||||||
|
|
||||||
|
从 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
|
||||||
|
DATA_KEY=$(openssl rand -hex 32) JWT_SECRET=$(openssl rand -hex 32) ADMIN_PASSWORD=<初始密码> docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 源码构建(单文件,含前端)
|
||||||
|
|
||||||
|
```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,可交叉编译)
|
||||||
|
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o bin/oci-portal-server ./cmd/server
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:上述解压会覆盖仓库占位文件 `internal/webui/dist/index.html`,提交代码前勿把真实产物带入版本库。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
| 变量 | 必填 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `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 文档(生产建议按需临时开启) |
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```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)
|
||||||
@@ -3,10 +3,22 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// 纯 Go 时区数据(+~450KB):distroless/scratch 容器无 tzdata,
|
||||||
|
// 嵌入后 TZ 环境变量即可让 cron 按本地时区解释
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
// swag 生成的 OpenAPI spec(init 时注册,SWAGGER=1 的 /swagger 路由消费)
|
||||||
|
_ "oci-portal/docs"
|
||||||
"oci-portal/internal/api"
|
"oci-portal/internal/api"
|
||||||
"oci-portal/internal/config"
|
"oci-portal/internal/config"
|
||||||
"oci-portal/internal/crypto"
|
"oci-portal/internal/crypto"
|
||||||
@@ -15,19 +27,58 @@ import (
|
|||||||
"oci-portal/internal/service"
|
"oci-portal/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @title OCI Portal API
|
||||||
|
// @version 0.0.1
|
||||||
|
// @description 自托管 OCI 多租户管理面板 API。业务接口用 JWT(Bearer);AI 网关端点(/ai/v1/*)用独立网关密钥(Authorization: Bearer 或 x-api-key)。
|
||||||
|
// @BasePath /
|
||||||
|
// @securityDefinitions.apikey BearerAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @description 登录接口返回的 JWT,格式: Bearer <token>
|
||||||
func main() {
|
func main() {
|
||||||
|
healthcheck := flag.Bool("healthcheck", false, "探活本机服务后退出:健康 0,异常 1(容器 HEALTHCHECK 用)")
|
||||||
|
flag.Parse()
|
||||||
|
if *healthcheck {
|
||||||
|
os.Exit(runHealthcheck())
|
||||||
|
}
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runHealthcheck 请求本机 HTTP 端口:能响应且非 5xx 视为健康。
|
||||||
|
// distroless 镜像无 shell/wget,靠二进制自检 flag 支撑 HEALTHCHECK。
|
||||||
|
func runHealthcheck() int {
|
||||||
|
addr := os.Getenv("ADDR")
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":8080"
|
||||||
|
}
|
||||||
|
_, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "healthcheck: bad ADDR %q: %v\n", addr, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: 3 * time.Second}
|
||||||
|
resp, err := client.Get("http://127.0.0.1:" + port + "/")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "healthcheck: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= http.StatusInternalServerError {
|
||||||
|
fmt.Fprintf(os.Stderr, "healthcheck: status %d\n", resp.StatusCode)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// bootstrap 完成配置加载、数据库打开与加密组件构建。
|
// bootstrap 完成配置加载、数据库打开与加密组件构建。
|
||||||
func bootstrap() (*config.Config, *gorm.DB, *crypto.Cipher, error) {
|
func bootstrap() (*config.Config, *gorm.DB, *crypto.Cipher, error) {
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
db, err := database.Open(cfg.DBPath)
|
db, err := database.Open(cfg.DBDriver, cfg.DBDSN, cfg.DBPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
oci-portal:
|
||||||
|
image: ghcr.io/wangdefaa/oci-portal:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "18888:8080"
|
||||||
|
environment:
|
||||||
|
DB_PATH: /data/oci-portal.db
|
||||||
|
DATA_KEY: ${DATA_KEY:?DATA_KEY is required}
|
||||||
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- oci-portal-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
oci-portal-data:
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||||
|
<g transform="rotate(-8 24 24)">
|
||||||
|
<path d="M24 5 L27.9 9.5 L33.5 7.5 L34.6 13.4 L40.5 14.5 L38.5 20.1 L43 24 L38.5 27.9 L40.5 33.5 L34.6 34.6 L33.5 40.5 L27.9 38.5 L24 43 L20.1 38.5 L14.5 40.5 L13.4 34.6 L7.5 33.5 L9.5 27.9 L5 24 L9.5 20.1 L7.5 14.5 L13.4 13.4 L14.5 7.5 L20.1 9.5 Z" fill="#3A2418" transform="translate(1.9,2.4)"/>
|
||||||
|
<path d="M24 5 L27.9 9.5 L33.5 7.5 L34.6 13.4 L40.5 14.5 L38.5 20.1 L43 24 L38.5 27.9 L40.5 33.5 L34.6 34.6 L33.5 40.5 L27.9 38.5 L24 43 L20.1 38.5 L14.5 40.5 L13.4 34.6 L7.5 33.5 L9.5 27.9 L5 24 L9.5 20.1 L7.5 14.5 L13.4 13.4 L14.5 7.5 L20.1 9.5 Z" fill="#C96442" stroke="#FFFDF7" stroke-width="2.6" stroke-linejoin="round"/>
|
||||||
|
<text x="24" y="28.8" text-anchor="middle" font-family="Georgia,'Times New Roman',serif" font-size="13" font-weight="900" fill="#FAF3E3">O!</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 130 KiB |
@@ -1,63 +1,95 @@
|
|||||||
module oci-portal
|
module oci-portal
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/coreos/go-oidc/v3 v3.19.0
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/oracle/oci-go-sdk/v65 v65.119.0
|
github.com/oracle/oci-go-sdk/v65 v65.119.0
|
||||||
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.53.0
|
||||||
|
golang.org/x/net v0.56.0
|
||||||
|
golang.org/x/oauth2 v0.36.0
|
||||||
|
golang.org/x/time v0.15.0
|
||||||
|
gorm.io/driver/mysql v1.6.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.31.2
|
gorm.io/gorm v1.31.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/coreos/go-oidc/v3 v3.19.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gofrs/flock v0.10.0 // indirect
|
github.com/gofrs/flock v0.10.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pquerna/otp v1.5.0 // indirect
|
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/sony/gobreaker/v2 v2.4.0 // indirect
|
github.com/sony/gobreaker/v2 v2.4.0 // indirect
|
||||||
|
github.com/swaggo/files v1.0.1 // indirect
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 // indirect
|
||||||
|
github.com/swaggo/swag v1.16.6 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/net v0.56.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.38.0 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tool github.com/swaggo/swag/cmd/swag
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
@@ -10,6 +19,9 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
|
|||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
|
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
|
||||||
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -25,6 +37,18 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
|||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -33,6 +57,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
@@ -50,16 +76,34 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
@@ -69,6 +113,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/oracle/oci-go-sdk/v65 v65.119.0 h1:0u9ujtEACjk3Sr72bnbTyJlCquhJiiEceIyMDhN9zcs=
|
github.com/oracle/oci-go-sdk/v65 v65.119.0 h1:0u9ujtEACjk3Sr72bnbTyJlCquhJiiEceIyMDhN9zcs=
|
||||||
github.com/oracle/oci-go-sdk/v65 v65.119.0/go.mod h1:nv7HqsLpM/5aH66gu6JD1oqMftTxGfxo3ow1eYKPSmI=
|
github.com/oracle/oci-go-sdk/v65 v65.119.0/go.mod h1:nv7HqsLpM/5aH66gu6JD1oqMftTxGfxo3ow1eYKPSmI=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
@@ -79,13 +124,17 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
|||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
|
github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
|
||||||
github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
|
github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -94,47 +143,102 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||||
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||||
@@ -147,3 +251,5 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
|
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// about 返回构建与运行环境信息,「设置 · 关于」页展示。
|
// about 返回构建与运行环境信息,「设置 · 关于」页展示。
|
||||||
|
//
|
||||||
|
// @Summary 返回构建与运行环境信息,「设置 · 关于」页展示
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/about [get]
|
||||||
func about(c *gin.Context) {
|
func about(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"version": buildVersion,
|
"version": buildVersion,
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type aiAdminHandler struct {
|
|||||||
|
|
||||||
// ---- 密钥 ----
|
// ---- 密钥 ----
|
||||||
|
|
||||||
|
// @Summary ---- 密钥 ----
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-keys [get]
|
||||||
func (h *aiAdminHandler) listKeys(c *gin.Context) {
|
func (h *aiAdminHandler) listKeys(c *gin.Context) {
|
||||||
keys, err := h.gw.Keys(c.Request.Context())
|
keys, err := h.gw.Keys(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,6 +31,13 @@ func (h *aiAdminHandler) listKeys(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createKey 生成密钥;明文仅在本响应返回一次。
|
// createKey 生成密钥;明文仅在本响应返回一次。
|
||||||
|
//
|
||||||
|
// @Summary 生成密钥
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-keys [post]
|
||||||
func (h *aiAdminHandler) createKey(c *gin.Context) {
|
func (h *aiAdminHandler) createKey(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
@@ -44,6 +56,13 @@ func (h *aiAdminHandler) createKey(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{"key": plaintext, "item": key})
|
c.JSON(http.StatusCreated, gin.H{"key": plaintext, "item": key})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新 AI 网关密钥
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-keys/{id} [put]
|
||||||
func (h *aiAdminHandler) updateKey(c *gin.Context) {
|
func (h *aiAdminHandler) updateKey(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -65,6 +84,12 @@ func (h *aiAdminHandler) updateKey(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除 AI 网关密钥
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-keys/{id} [delete]
|
||||||
func (h *aiAdminHandler) deleteKey(c *gin.Context) {
|
func (h *aiAdminHandler) deleteKey(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -78,6 +103,14 @@ func (h *aiAdminHandler) deleteKey(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateKeyContentLog 设置密钥内容日志窗口(红线例外:显式限时开启,0 关闭)。
|
// updateKeyContentLog 设置密钥内容日志窗口(红线例外:显式限时开启,0 关闭)。
|
||||||
|
//
|
||||||
|
// @Summary 设置密钥内容日志窗口
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-keys/{id}/content-log [put]
|
||||||
func (h *aiAdminHandler) updateKeyContentLog(c *gin.Context) {
|
func (h *aiAdminHandler) updateKeyContentLog(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -99,6 +132,12 @@ func (h *aiAdminHandler) updateKeyContentLog(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listContentLogs 分页查询内容日志(可选 keyId / callLogId 过滤)。
|
// listContentLogs 分页查询内容日志(可选 keyId / callLogId 过滤)。
|
||||||
|
//
|
||||||
|
// @Summary 分页查询内容日志
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-content-logs [get]
|
||||||
func (h *aiAdminHandler) listContentLogs(c *gin.Context) {
|
func (h *aiAdminHandler) listContentLogs(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||||
@@ -114,6 +153,11 @@ func (h *aiAdminHandler) listContentLogs(c *gin.Context) {
|
|||||||
|
|
||||||
// ---- 渠道 ----
|
// ---- 渠道 ----
|
||||||
|
|
||||||
|
// @Summary ---- 渠道 ----
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels [get]
|
||||||
func (h *aiAdminHandler) listChannels(c *gin.Context) {
|
func (h *aiAdminHandler) listChannels(c *gin.Context) {
|
||||||
chs, err := h.gw.Channels(c.Request.Context())
|
chs, err := h.gw.Channels(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,6 +167,12 @@ func (h *aiAdminHandler) listChannels(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"items": chs})
|
c.JSON(http.StatusOK, gin.H{"items": chs})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建 AI 渠道
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param body body service.ChannelInput true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels [post]
|
||||||
func (h *aiAdminHandler) createChannel(c *gin.Context) {
|
func (h *aiAdminHandler) createChannel(c *gin.Context) {
|
||||||
var req service.ChannelInput
|
var req service.ChannelInput
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -137,6 +187,13 @@ func (h *aiAdminHandler) createChannel(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, ch)
|
c.JSON(http.StatusCreated, ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新 AI 渠道
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body service.ChannelInput true "请求体"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels/{id} [put]
|
||||||
func (h *aiAdminHandler) updateChannel(c *gin.Context) {
|
func (h *aiAdminHandler) updateChannel(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -154,6 +211,12 @@ func (h *aiAdminHandler) updateChannel(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除 AI 渠道
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels/{id} [delete]
|
||||||
func (h *aiAdminHandler) deleteChannel(c *gin.Context) {
|
func (h *aiAdminHandler) deleteChannel(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -167,6 +230,13 @@ func (h *aiAdminHandler) deleteChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// probeChannel 触发探测:服务可见性 + 模型同步 + 配额试调,返回更新后的渠道。
|
// probeChannel 触发探测:服务可见性 + 模型同步 + 配额试调,返回更新后的渠道。
|
||||||
|
//
|
||||||
|
// @Summary 触发探测:服务可见性 + 模型同步 + 配额试调,返回更新后的渠道
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels/{id}/probe [post]
|
||||||
func (h *aiAdminHandler) probeChannel(c *gin.Context) {
|
func (h *aiAdminHandler) probeChannel(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -180,6 +250,12 @@ func (h *aiAdminHandler) probeChannel(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, ch)
|
c.JSON(http.StatusOK, ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 同步渠道模型缓存
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-channels/{id}/sync-models [post]
|
||||||
func (h *aiAdminHandler) syncChannelModels(c *gin.Context) {
|
func (h *aiAdminHandler) syncChannelModels(c *gin.Context) {
|
||||||
id, ok := aiPathID(c)
|
id, ok := aiPathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -195,6 +271,11 @@ func (h *aiAdminHandler) syncChannelModels(c *gin.Context) {
|
|||||||
|
|
||||||
// ---- 聚合模型与调用日志 ----
|
// ---- 聚合模型与调用日志 ----
|
||||||
|
|
||||||
|
// @Summary ---- 聚合模型与调用日志 ----
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-models [get]
|
||||||
func (h *aiAdminHandler) gatewayModels(c *gin.Context) {
|
func (h *aiAdminHandler) gatewayModels(c *gin.Context) {
|
||||||
list, err := h.gw.GatewayModels(c.Request.Context(), "")
|
list, err := h.gw.GatewayModels(c.Request.Context(), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -204,6 +285,11 @@ func (h *aiAdminHandler) gatewayModels(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"items": list.Data})
|
c.JSON(http.StatusOK, gin.H{"items": list.Data})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary AI 调用日志列表
|
||||||
|
// @Tags AI 管理
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/ai-logs [get]
|
||||||
func (h *aiAdminHandler) listLogs(c *gin.Context) {
|
func (h *aiAdminHandler) listLogs(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
|
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
|
||||||
|
|||||||
@@ -148,6 +148,12 @@ func (h *aiGatewayHandler) logFailure(c *gin.Context, entry model.AiCallLog, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// chatCompletions 是 OpenAI /ai/v1/chat/completions 端点。
|
// chatCompletions 是 OpenAI /ai/v1/chat/completions 端点。
|
||||||
|
//
|
||||||
|
// @Summary OpenAI 兼容对话补全
|
||||||
|
// @Tags AI 网关
|
||||||
|
// @Param body body object true "OpenAI chat/completions 请求体(支持 stream)"
|
||||||
|
// @Success 200 {object} map[string]any "OpenAI 兼容响应(流式为 SSE)"
|
||||||
|
// @Router /ai/v1/chat/completions [post]
|
||||||
func (h *aiGatewayHandler) chatCompletions(c *gin.Context) {
|
func (h *aiGatewayHandler) chatCompletions(c *gin.Context) {
|
||||||
var ir aiwire.ChatRequest
|
var ir aiwire.ChatRequest
|
||||||
if err := c.ShouldBindJSON(&ir); err != nil {
|
if err := c.ShouldBindJSON(&ir); err != nil {
|
||||||
@@ -191,6 +197,12 @@ func fillUsage(entry *model.AiCallLog, u *aiwire.Usage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// embeddings 是 OpenAI /ai/v1/embeddings 端点(非流式)。
|
// embeddings 是 OpenAI /ai/v1/embeddings 端点(非流式)。
|
||||||
|
//
|
||||||
|
// @Summary OpenAI 兼容向量嵌入
|
||||||
|
// @Tags AI 网关
|
||||||
|
// @Param body body object true "OpenAI embeddings 请求体"
|
||||||
|
// @Success 200 {object} map[string]any "OpenAI 兼容响应"
|
||||||
|
// @Router /ai/v1/embeddings [post]
|
||||||
func (h *aiGatewayHandler) embeddings(c *gin.Context) {
|
func (h *aiGatewayHandler) embeddings(c *gin.Context) {
|
||||||
var req aiwire.EmbeddingsRequest
|
var req aiwire.EmbeddingsRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -280,6 +292,12 @@ func writeSSEData(c *gin.Context, v any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// messages 是 Anthropic /ai/v1/messages 端点。
|
// messages 是 Anthropic /ai/v1/messages 端点。
|
||||||
|
//
|
||||||
|
// @Summary Anthropic Messages 兼容端点
|
||||||
|
// @Tags AI 网关
|
||||||
|
// @Param body body object true "Anthropic messages 请求体(支持 stream)"
|
||||||
|
// @Success 200 {object} map[string]any "Anthropic 兼容响应(流式为 SSE)"
|
||||||
|
// @Router /ai/v1/messages [post]
|
||||||
func (h *aiGatewayHandler) messages(c *gin.Context) {
|
func (h *aiGatewayHandler) messages(c *gin.Context) {
|
||||||
var req aiwire.MessagesRequest
|
var req aiwire.MessagesRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -369,6 +387,11 @@ func writeAnthEvents(c *gin.Context, events []service.AnthEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listModels 是 /ai/v1/models 端点(OpenAI 格式,从启用渠道的模型缓存聚合)。
|
// listModels 是 /ai/v1/models 端点(OpenAI 格式,从启用渠道的模型缓存聚合)。
|
||||||
|
//
|
||||||
|
// @Summary 可用模型列表
|
||||||
|
// @Tags AI 网关
|
||||||
|
// @Success 200 {object} map[string]any "OpenAI 兼容 models 列表"
|
||||||
|
// @Router /ai/v1/models [get]
|
||||||
func (h *aiGatewayHandler) listModels(c *gin.Context) {
|
func (h *aiGatewayHandler) listModels(c *gin.Context) {
|
||||||
list, err := h.gw.GatewayModels(c.Request.Context(), keyGroup(c))
|
list, err := h.gw.GatewayModels(c.Request.Context(), keyGroup(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -379,6 +402,12 @@ func (h *aiGatewayHandler) listModels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// responses 是 OpenAI /ai/v1/responses 端点(无状态子集)。
|
// responses 是 OpenAI /ai/v1/responses 端点(无状态子集)。
|
||||||
|
//
|
||||||
|
// @Summary OpenAI Responses 兼容端点
|
||||||
|
// @Tags AI 网关
|
||||||
|
// @Param body body object true "OpenAI responses 请求体(支持 stream)"
|
||||||
|
// @Success 200 {object} map[string]any "OpenAI 兼容响应(流式为 SSE)"
|
||||||
|
// @Router /ai/v1/responses [post]
|
||||||
func (h *aiGatewayHandler) responses(c *gin.Context) {
|
func (h *aiGatewayHandler) responses(c *gin.Context) {
|
||||||
var req aiwire.RespRequest
|
var req aiwire.RespRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import (
|
|||||||
|
|
||||||
// ---- 实例 IP ----
|
// ---- 实例 IP ----
|
||||||
|
|
||||||
|
// @Summary ---- 实例 IP ----
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/change-public-ip [post]
|
||||||
func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -27,6 +35,14 @@ func (h *ociConfigHandler) changePublicIP(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"publicIp": ip})
|
c.JSON(http.StatusOK, gin.H{"publicIp": ip})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例添加 IPv6 地址
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/ipv6-addresses [post]
|
||||||
func (h *ociConfigHandler) addInstanceIpv6(c *gin.Context) {
|
func (h *ociConfigHandler) addInstanceIpv6(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -45,6 +61,13 @@ func (h *ociConfigHandler) addInstanceIpv6(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{"ipv6Address": address})
|
c.JSON(http.StatusCreated, gin.H{"ipv6Address": address})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 移除实例 IPv6 地址
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/ipv6-addresses [delete]
|
||||||
func (h *ociConfigHandler) deleteInstanceIpv6(c *gin.Context) {
|
func (h *ociConfigHandler) deleteInstanceIpv6(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -65,6 +88,13 @@ type attachVnicRequest struct {
|
|||||||
oci.AttachVnicInput
|
oci.AttachVnicInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例 VNIC 列表
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/vnics [get]
|
||||||
func (h *ociConfigHandler) listInstanceVnics(c *gin.Context) {
|
func (h *ociConfigHandler) listInstanceVnics(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -78,6 +108,14 @@ func (h *ociConfigHandler) listInstanceVnics(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, vnics)
|
c.JSON(http.StatusOK, vnics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 附加 VNIC
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body attachVnicRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/vnics [post]
|
||||||
func (h *ociConfigHandler) attachVnic(c *gin.Context) {
|
func (h *ociConfigHandler) attachVnic(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -96,6 +134,13 @@ func (h *ociConfigHandler) attachVnic(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, vnic)
|
c.JSON(http.StatusCreated, vnic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 分离 VNIC
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param attachmentId path string true "attachmentId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vnic-attachments/{attachmentId} [delete]
|
||||||
func (h *ociConfigHandler) detachVnic(c *gin.Context) {
|
func (h *ociConfigHandler) detachVnic(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -109,6 +154,15 @@ func (h *ociConfigHandler) detachVnic(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addVnicIpv6 为指定 VNIC 添加 IPv6;body.address 留空自动分配。
|
// addVnicIpv6 为指定 VNIC 添加 IPv6;body.address 留空自动分配。
|
||||||
|
//
|
||||||
|
// @Summary 为指定 VNIC 添加 IPv6
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param vnicId path string true "vnicId"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vnics/{vnicId}/ipv6-addresses [post]
|
||||||
func (h *ociConfigHandler) addVnicIpv6(c *gin.Context) {
|
func (h *ociConfigHandler) addVnicIpv6(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -137,6 +191,13 @@ type attachBootVolumeRequest struct {
|
|||||||
BootVolumeID string `json:"bootVolumeId" binding:"required"`
|
BootVolumeID string `json:"bootVolumeId" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 引导卷附件列表
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/boot-volume-attachments [get]
|
||||||
func (h *ociConfigHandler) listBootVolumeAttachments(c *gin.Context) {
|
func (h *ociConfigHandler) listBootVolumeAttachments(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -150,6 +211,14 @@ func (h *ociConfigHandler) listBootVolumeAttachments(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, atts)
|
c.JSON(http.StatusOK, atts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 附加引导卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body attachBootVolumeRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/boot-volume-attachments [post]
|
||||||
func (h *ociConfigHandler) attachBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) attachBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -168,6 +237,14 @@ func (h *ociConfigHandler) attachBootVolume(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, att)
|
c.JSON(http.StatusCreated, att)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更换实例引导卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body attachBootVolumeRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/replace-boot-volume [post]
|
||||||
func (h *ociConfigHandler) replaceBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) replaceBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -186,6 +263,13 @@ func (h *ociConfigHandler) replaceBootVolume(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, att)
|
c.JSON(http.StatusOK, att)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 分离引导卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param attachmentId path string true "attachmentId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/boot-volume-attachments/{attachmentId} [delete]
|
||||||
func (h *ociConfigHandler) detachBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) detachBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -206,6 +290,12 @@ type attachVolumeRequest struct {
|
|||||||
ReadOnly bool `json:"readOnly"`
|
ReadOnly bool `json:"readOnly"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 块存储卷列表
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/volumes [get]
|
||||||
func (h *ociConfigHandler) listBlockVolumes(c *gin.Context) {
|
func (h *ociConfigHandler) listBlockVolumes(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -219,6 +309,13 @@ func (h *ociConfigHandler) listBlockVolumes(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, volumes)
|
c.JSON(http.StatusOK, volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 卷附件列表
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/volume-attachments [get]
|
||||||
func (h *ociConfigHandler) listVolumeAttachments(c *gin.Context) {
|
func (h *ociConfigHandler) listVolumeAttachments(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -232,6 +329,14 @@ func (h *ociConfigHandler) listVolumeAttachments(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, atts)
|
c.JSON(http.StatusOK, atts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 附加块存储卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body attachVolumeRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/volume-attachments [post]
|
||||||
func (h *ociConfigHandler) attachVolume(c *gin.Context) {
|
func (h *ociConfigHandler) attachVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -250,6 +355,13 @@ func (h *ociConfigHandler) attachVolume(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, att)
|
c.JSON(http.StatusCreated, att)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 分离块存储卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param attachmentId path string true "attachmentId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/volume-attachments/{attachmentId} [delete]
|
||||||
func (h *ociConfigHandler) detachVolume(c *gin.Context) {
|
func (h *ociConfigHandler) detachVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ type loginRequest struct {
|
|||||||
TotpCode string `json:"totpCode"`
|
TotpCode string `json:"totpCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// login 校验用户名密码(与可选 TOTP)后签发 JWT。
|
||||||
|
//
|
||||||
|
// @Summary 登录
|
||||||
|
// @Tags 认证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body loginRequest true "登录凭据"
|
||||||
|
// @Success 200 {object} map[string]any "token 与 expiresAt"
|
||||||
|
// @Failure 401 {object} map[string]string "凭据错误"
|
||||||
|
// @Failure 428 {object} map[string]any "需要两步验证码(totpRequired=true)"
|
||||||
|
// @Router /api/v1/auth/login [post]
|
||||||
func (h *authHandler) login(c *gin.Context) {
|
func (h *authHandler) login(c *gin.Context) {
|
||||||
var req loginRequest
|
var req loginRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -61,6 +72,12 @@ func (h *authHandler) login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// logout 把当前令牌拉黑至自然过期(secured 组内,写请求由系统日志中间件留痕)。
|
// logout 把当前令牌拉黑至自然过期(secured 组内,写请求由系统日志中间件留痕)。
|
||||||
|
//
|
||||||
|
// @Summary 登出
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 204 "令牌已吊销"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/logout [post]
|
||||||
func (h *authHandler) logout(c *gin.Context) {
|
func (h *authHandler) logout(c *gin.Context) {
|
||||||
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||||
if ok && token != "" {
|
if ok && token != "" {
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ type authxHandler struct {
|
|||||||
// ---- TOTP(JWT 组内) ----
|
// ---- TOTP(JWT 组内) ----
|
||||||
|
|
||||||
// totpStatus 返回当前账号两步验证启用状态。
|
// totpStatus 返回当前账号两步验证启用状态。
|
||||||
|
//
|
||||||
|
// @Summary 两步验证状态
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} map[string]bool "enabled"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/totp [get]
|
||||||
func (h *authxHandler) totpStatus(c *gin.Context) {
|
func (h *authxHandler) totpStatus(c *gin.Context) {
|
||||||
enabled, err := h.auth.TotpStatus(c.Request.Context(), c.GetString(usernameKey))
|
enabled, err := h.auth.TotpStatus(c.Request.Context(), c.GetString(usernameKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30,6 +36,13 @@ func (h *authxHandler) totpStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// totpSetup 生成待激活密钥,返回手动输入码与 otpauth URI(前端渲染二维码)。
|
// totpSetup 生成待激活密钥,返回手动输入码与 otpauth URI(前端渲染二维码)。
|
||||||
|
//
|
||||||
|
// @Summary 发起两步验证设置
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} map[string]string "secret 与 otpauthUri"
|
||||||
|
// @Failure 409 {object} map[string]string "已启用"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/totp/setup [post]
|
||||||
func (h *authxHandler) totpSetup(c *gin.Context) {
|
func (h *authxHandler) totpSetup(c *gin.Context) {
|
||||||
secret, uri, err := h.auth.SetupTotp(c.Request.Context(), c.GetString(usernameKey))
|
secret, uri, err := h.auth.SetupTotp(c.Request.Context(), c.GetString(usernameKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,6 +57,13 @@ func (h *authxHandler) totpSetup(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// totpActivate 校验验证码并正式启用两步验证。
|
// totpActivate 校验验证码并正式启用两步验证。
|
||||||
|
//
|
||||||
|
// @Summary 激活两步验证
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param body body object true "{code: 6 位验证码}"
|
||||||
|
// @Success 204 "已启用"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/totp/activate [post]
|
||||||
func (h *authxHandler) totpActivate(c *gin.Context) {
|
func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Code string `json:"code" binding:"required"`
|
Code string `json:"code" binding:"required"`
|
||||||
@@ -65,6 +85,13 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
||||||
|
//
|
||||||
|
// @Summary 停用两步验证
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param body body object true "{password 或 code 任一确认}"
|
||||||
|
// @Success 204 "已停用"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/totp/disable [post]
|
||||||
func (h *authxHandler) totpDisable(c *gin.Context) {
|
func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@@ -89,6 +116,12 @@ func (h *authxHandler) totpDisable(c *gin.Context) {
|
|||||||
// ---- 登录凭据(JWT 组内) ----
|
// ---- 登录凭据(JWT 组内) ----
|
||||||
|
|
||||||
// getCredentials 返回登录凭据摘要:当前用户名与密码登录禁用态。
|
// getCredentials 返回登录凭据摘要:当前用户名与密码登录禁用态。
|
||||||
|
//
|
||||||
|
// @Summary 登录凭据摘要
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} map[string]any "username 与 passwordLoginDisabled"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/credentials [get]
|
||||||
func (h *authxHandler) getCredentials(c *gin.Context) {
|
func (h *authxHandler) getCredentials(c *gin.Context) {
|
||||||
disabled, err := h.auth.PasswordLoginDisabled(c.Request.Context())
|
disabled, err := h.auth.PasswordLoginDisabled(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,6 +135,14 @@ func (h *authxHandler) getCredentials(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateCredentials 修改用户名 / 密码;当前密码必验,改名后旧 JWT 随即失效(前端应重新登录)。
|
// updateCredentials 修改用户名 / 密码;当前密码必验,改名后旧 JWT 随即失效(前端应重新登录)。
|
||||||
|
//
|
||||||
|
// @Summary 修改登录凭据
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param body body service.UpdateCredentialsInput true "新凭据(当前密码必验)"
|
||||||
|
// @Success 204 "已更新,请重新登录"
|
||||||
|
// @Failure 401 {object} map[string]string "当前密码错误"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/credentials [put]
|
||||||
func (h *authxHandler) updateCredentials(c *gin.Context) {
|
func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||||
var req service.UpdateCredentialsInput
|
var req service.UpdateCredentialsInput
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -125,6 +166,14 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
||||||
|
//
|
||||||
|
// @Summary 密码登录开关
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param body body object true "{disabled: bool}"
|
||||||
|
// @Success 204 "已保存"
|
||||||
|
// @Failure 409 {object} map[string]string "未绑定外部身份"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/password-login [put]
|
||||||
func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Disabled *bool `json:"disabled" binding:"required"`
|
Disabled *bool `json:"disabled" binding:"required"`
|
||||||
@@ -150,6 +199,11 @@ func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
|||||||
// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
|
// oauthProviders 返回已配置的 provider 列表(公开,登录页展示按钮),
|
||||||
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
|
// 并附带密码登录禁用开关:开启且存在可用外部身份时,登录页隐藏密码表单;
|
||||||
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
|
// provider 清空时不下发禁用,保证界面始终留有登录入口(后端 Login 仍会拒绝)。
|
||||||
|
//
|
||||||
|
// @Summary 外部登录 provider 列表
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} map[string]any "providers 与 passwordLoginDisabled"
|
||||||
|
// @Router /api/v1/auth/oauth/providers [get]
|
||||||
func (h *authxHandler) oauthProviders(c *gin.Context) {
|
func (h *authxHandler) oauthProviders(c *gin.Context) {
|
||||||
providers := h.oauth.Providers(c.Request.Context())
|
providers := h.oauth.Providers(c.Request.Context())
|
||||||
disabled := false
|
disabled := false
|
||||||
@@ -160,6 +214,13 @@ func (h *authxHandler) oauthProviders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
|
// oauthAuthorize 返回授权跳转 URL;mode=bind 需有效 JWT(绑定到当前账号),login 公开。
|
||||||
|
//
|
||||||
|
// @Summary 外部登录授权跳转
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param provider path string true "oidc / github"
|
||||||
|
// @Param mode query string false "bind=绑定当前账号(需 Bearer),缺省登录"
|
||||||
|
// @Success 200 {object} map[string]string "url"
|
||||||
|
// @Router /api/v1/auth/oauth/{provider}/authorize [get]
|
||||||
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
func (h *authxHandler) oauthAuthorize(c *gin.Context) {
|
||||||
provider := c.Param("provider")
|
provider := c.Param("provider")
|
||||||
if provider != "oidc" && provider != "github" {
|
if provider != "oidc" && provider != "github" {
|
||||||
@@ -201,6 +262,14 @@ func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// oauthCallback 是 provider 回调落点(浏览器直达):完成登录或绑定后 302 回前端。
|
// oauthCallback 是 provider 回调落点(浏览器直达):完成登录或绑定后 302 回前端。
|
||||||
|
//
|
||||||
|
// @Summary 外部登录回调
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param provider path string true "oidc / github"
|
||||||
|
// @Param state query string true "授权流程 state"
|
||||||
|
// @Param code query string true "授权码"
|
||||||
|
// @Success 302 "登录 token 经 fragment 回前端,绑定回设置页"
|
||||||
|
// @Router /api/v1/auth/oauth/{provider}/callback [get]
|
||||||
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||||
provider := c.Param("provider")
|
provider := c.Param("provider")
|
||||||
token, _, mode, err := h.oauth.HandleCallback(
|
token, _, mode, err := h.oauth.HandleCallback(
|
||||||
@@ -233,6 +302,12 @@ func oauthErrText(err error) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identities 列出当前账号绑定的外部身份。
|
// identities 列出当前账号绑定的外部身份。
|
||||||
|
//
|
||||||
|
// @Summary 已绑定外部身份
|
||||||
|
// @Tags 认证
|
||||||
|
// @Success 200 {object} map[string]any "items"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/identities [get]
|
||||||
func (h *authxHandler) identities(c *gin.Context) {
|
func (h *authxHandler) identities(c *gin.Context) {
|
||||||
items, err := h.oauth.Identities(c.Request.Context(), c.GetString(usernameKey))
|
items, err := h.oauth.Identities(c.Request.Context(), c.GetString(usernameKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -243,6 +318,14 @@ func (h *authxHandler) identities(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// unbindIdentity 解绑外部身份;密码登录禁用期间不允许解绑最后一个身份。
|
// unbindIdentity 解绑外部身份;密码登录禁用期间不允许解绑最后一个身份。
|
||||||
|
//
|
||||||
|
// @Summary 解绑外部身份
|
||||||
|
// @Tags 认证
|
||||||
|
// @Param id path int true "身份 ID"
|
||||||
|
// @Success 204 "已解绑"
|
||||||
|
// @Failure 409 {object} map[string]string "最后一个身份不可解绑"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/auth/identities/{id} [delete]
|
||||||
func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -263,6 +346,12 @@ func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
|||||||
// ---- OAuth provider 设置(JWT 组内) ----
|
// ---- OAuth provider 设置(JWT 组内) ----
|
||||||
|
|
||||||
// getOAuthSettings 返回 provider 配置(secret 只回设置态)。
|
// getOAuthSettings 返回 provider 配置(secret 只回设置态)。
|
||||||
|
//
|
||||||
|
// @Summary OAuth provider 配置
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} service.OAuthProvidersView
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/oauth [get]
|
||||||
func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||||
view, err := settings.OAuthView(c.Request.Context())
|
view, err := settings.OAuthView(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -273,6 +362,13 @@ func (h *authxHandler) getOAuthSettings(c *gin.Context, settings *service.Settin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateOAuthSettings 保存 provider 配置;secret 缺省沿用、空串清除。
|
// updateOAuthSettings 保存 provider 配置;secret 缺省沿用、空串清除。
|
||||||
|
//
|
||||||
|
// @Summary 保存 OAuth provider 配置
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body service.UpdateOAuthInput true "provider 配置"
|
||||||
|
// @Success 200 {object} service.OAuthProvidersView
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/oauth [put]
|
||||||
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
func (h *authxHandler) updateOAuthSettings(c *gin.Context, settings *service.SettingService) {
|
||||||
var req service.UpdateOAuthInput
|
var req service.UpdateOAuthInput
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ type createConsoleConnectionRequest struct {
|
|||||||
SSHPublicKey string `json:"sshPublicKey" binding:"required"`
|
SSHPublicKey string `json:"sshPublicKey" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建实例控制台连接
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body createConsoleConnectionRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-connections [post]
|
||||||
func (h *ociConfigHandler) createConsoleConnection(c *gin.Context) {
|
func (h *ociConfigHandler) createConsoleConnection(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -31,6 +39,13 @@ func (h *ociConfigHandler) createConsoleConnection(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, conn)
|
c.JSON(http.StatusCreated, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例控制台连接列表
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-connections [get]
|
||||||
func (h *ociConfigHandler) listConsoleConnections(c *gin.Context) {
|
func (h *ociConfigHandler) listConsoleConnections(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -44,6 +59,13 @@ func (h *ociConfigHandler) listConsoleConnections(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, connections)
|
c.JSON(http.StatusOK, connections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除实例控制台连接
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param connectionId path string true "connectionId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/console-connections/{connectionId} [delete]
|
||||||
func (h *ociConfigHandler) deleteConsoleConnection(c *gin.Context) {
|
func (h *ociConfigHandler) deleteConsoleConnection(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ func boolOr(v *bool, def bool) bool {
|
|||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 身份提供商列表
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-providers [get]
|
||||||
func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -50,6 +56,13 @@ func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, idps)
|
c.JSON(http.StatusOK, idps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建身份提供商(SAML)
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createIdpRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-providers [post]
|
||||||
func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -81,6 +94,14 @@ func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, idp)
|
c.JSON(http.StatusCreated, idp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 激活身份提供商
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param idpId path string true "idpId"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-providers/{idpId}/activate [post]
|
||||||
func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -101,6 +122,13 @@ func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, idp)
|
c.JSON(http.StatusOK, idp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除身份提供商
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param idpId path string true "idpId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-providers/{idpId} [delete]
|
||||||
func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -113,6 +141,12 @@ func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 下载 SAML 元数据
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/saml-metadata [get]
|
||||||
func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -127,6 +161,12 @@ func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
|||||||
c.Data(http.StatusOK, "application/xml", metadata)
|
c.Data(http.StatusOK, "application/xml", metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 单点登录规则列表
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/sign-on-rules [get]
|
||||||
func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -140,6 +180,13 @@ func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, rules)
|
c.JSON(http.StatusOK, rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建 MFA 豁免规则
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/sign-on-exemptions [post]
|
||||||
func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -160,6 +207,13 @@ func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, rule)
|
c.JSON(http.StatusCreated, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除 MFA 豁免规则
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param ruleId path string true "ruleId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/sign-on-exemptions/{ruleId} [delete]
|
||||||
func (h *ociConfigHandler) deleteMfaExemption(c *gin.Context) {
|
func (h *ociConfigHandler) deleteMfaExemption(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"oci-portal/internal/oci"
|
"oci-portal/internal/oci"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @Summary 可用域列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/availability-domains [get]
|
||||||
func (h *ociConfigHandler) availabilityDomains(c *gin.Context) {
|
func (h *ociConfigHandler) availabilityDomains(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -58,6 +64,12 @@ type instanceActionRequest struct {
|
|||||||
Action string `json:"action" binding:"required"`
|
Action string `json:"action" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例列表
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances [get]
|
||||||
func (h *ociConfigHandler) listInstances(c *gin.Context) {
|
func (h *ociConfigHandler) listInstances(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -71,6 +83,13 @@ func (h *ociConfigHandler) listInstances(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, instances)
|
c.JSON(http.StatusOK, instances)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建实例
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createInstanceRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances [post]
|
||||||
func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -124,6 +143,13 @@ func (h *ociConfigHandler) createInstance(c *gin.Context) {
|
|||||||
c.JSON(status, gin.H{"instances": instances, "errors": failures})
|
c.JSON(status, gin.H{"instances": instances, "errors": failures})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例详情
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [get]
|
||||||
func (h *ociConfigHandler) getInstance(c *gin.Context) {
|
func (h *ociConfigHandler) getInstance(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -137,6 +163,14 @@ func (h *ociConfigHandler) getInstance(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, instance)
|
c.JSON(http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新实例
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body updateInstanceRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [put]
|
||||||
func (h *ociConfigHandler) updateInstance(c *gin.Context) {
|
func (h *ociConfigHandler) updateInstance(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -161,6 +195,13 @@ func (h *ociConfigHandler) updateInstance(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, instance)
|
c.JSON(http.StatusOK, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 终止实例
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId} [delete]
|
||||||
func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
|
func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -174,6 +215,14 @@ func (h *ociConfigHandler) terminateInstance(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 实例电源操作(启停/重启)
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Param body body instanceActionRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/action [post]
|
||||||
func (h *ociConfigHandler) instanceAction(c *gin.Context) {
|
func (h *ociConfigHandler) instanceAction(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -201,6 +250,12 @@ type updateBootVolumeRequest struct {
|
|||||||
VpusPerGB int64 `json:"vpusPerGB"`
|
VpusPerGB int64 `json:"vpusPerGB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 引导卷列表
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/boot-volumes [get]
|
||||||
func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
|
func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -214,6 +269,13 @@ func (h *ociConfigHandler) listBootVolumes(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, volumes)
|
c.JSON(http.StatusOK, volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 引导卷详情
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param bootVolumeId path string true "bootVolumeId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [get]
|
||||||
func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -227,6 +289,14 @@ func (h *ociConfigHandler) getBootVolume(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, volume)
|
c.JSON(http.StatusOK, volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新引导卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param bootVolumeId path string true "bootVolumeId"
|
||||||
|
// @Param body body updateBootVolumeRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [put]
|
||||||
func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -250,6 +320,13 @@ func (h *ociConfigHandler) updateBootVolume(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, volume)
|
c.JSON(http.StatusOK, volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除引导卷
|
||||||
|
// @Tags 存储
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param bootVolumeId path string true "bootVolumeId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/boot-volumes/{bootVolumeId} [delete]
|
||||||
func (h *ociConfigHandler) deleteBootVolume(c *gin.Context) {
|
func (h *ociConfigHandler) deleteBootVolume(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ type logEventHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getWebhook 查询配置的回调地址;未生成时 exists 为 false 且不含 secret。
|
// getWebhook 查询配置的回调地址;未生成时 exists 为 false 且不含 secret。
|
||||||
|
//
|
||||||
|
// @Summary 查询配置的回调地址
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-webhook [get]
|
||||||
func (h *logEventHandler) getWebhook(c *gin.Context) {
|
func (h *logEventHandler) getWebhook(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -34,6 +41,13 @@ func (h *logEventHandler) getWebhook(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensureWebhook 生成(或幂等返回)配置的回传 secret 与回调路径。
|
// ensureWebhook 生成(或幂等返回)配置的回传 secret 与回调路径。
|
||||||
|
//
|
||||||
|
// @Summary 生成
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-webhook [post]
|
||||||
func (h *logEventHandler) ensureWebhook(c *gin.Context) {
|
func (h *logEventHandler) ensureWebhook(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -48,6 +62,13 @@ func (h *logEventHandler) ensureWebhook(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// revokeWebhook 撤销配置的回传 secret,旧回调地址随即失效。
|
// revokeWebhook 撤销配置的回传 secret,旧回调地址随即失效。
|
||||||
|
//
|
||||||
|
// @Summary 撤销配置的回传 secret,旧回调地址随即失效
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-webhook [delete]
|
||||||
func (h *logEventHandler) revokeWebhook(c *gin.Context) {
|
func (h *logEventHandler) revokeWebhook(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -61,6 +82,14 @@ func (h *logEventHandler) revokeWebhook(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list 分页查询回传事件;cfgId 缺省为全部租户。
|
// list 分页查询回传事件;cfgId 缺省为全部租户。
|
||||||
|
//
|
||||||
|
// @Summary 回传日志事件列表
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param configId query int false "按配置过滤"
|
||||||
|
// @Param q query string false "关键字"
|
||||||
|
// @Success 200 {object} map[string]any "items 与 total"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/log-events [get]
|
||||||
func (h *logEventHandler) list(c *gin.Context) {
|
func (h *logEventHandler) list(c *gin.Context) {
|
||||||
cfgID, _ := strconv.ParseUint(c.Query("cfgId"), 10, 64)
|
cfgID, _ := strconv.ParseUint(c.Query("cfgId"), 10, 64)
|
||||||
page, _ := strconv.Atoi(c.Query("page"))
|
page, _ := strconv.Atoi(c.Query("page"))
|
||||||
@@ -78,6 +107,13 @@ func (h *logEventHandler) list(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getRelay 查询 OCI 侧链路状态与关键事件清单。
|
// getRelay 查询 OCI 侧链路状态与关键事件清单。
|
||||||
|
//
|
||||||
|
// @Summary 查询 OCI 侧链路状态与关键事件清单
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-relay [get]
|
||||||
func (h *logEventHandler) getRelay(c *gin.Context) {
|
func (h *logEventHandler) getRelay(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -92,6 +128,13 @@ func (h *logEventHandler) getRelay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setupRelay 一键建立回传链路(P1 引导创建);同步执行,前端以长超时等待。
|
// setupRelay 一键建立回传链路(P1 引导创建);同步执行,前端以长超时等待。
|
||||||
|
//
|
||||||
|
// @Summary 一键建立回传链路
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-relay [post]
|
||||||
func (h *logEventHandler) setupRelay(c *gin.Context) {
|
func (h *logEventHandler) setupRelay(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -106,6 +149,13 @@ func (h *logEventHandler) setupRelay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// teardownRelay 逆序销毁链路并撤销 secret。
|
// teardownRelay 逆序销毁链路并撤销 secret。
|
||||||
|
//
|
||||||
|
// @Summary 逆序销毁链路并撤销 secret
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/log-relay [delete]
|
||||||
func (h *logEventHandler) teardownRelay(c *gin.Context) {
|
func (h *logEventHandler) teardownRelay(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"oci-portal/internal/oci"
|
"oci-portal/internal/oci"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @Summary 实例形状列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/shapes [get]
|
||||||
func (h *ociConfigHandler) shapes(c *gin.Context) {
|
func (h *ociConfigHandler) shapes(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -21,6 +27,12 @@ func (h *ociConfigHandler) shapes(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, shapes)
|
c.JSON(http.StatusOK, shapes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 镜像列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/images [get]
|
||||||
func (h *ociConfigHandler) images(c *gin.Context) {
|
func (h *ociConfigHandler) images(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -38,6 +50,13 @@ func (h *ociConfigHandler) images(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, images)
|
c.JSON(http.StatusOK, images)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 镜像详情
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param imageId path string true "imageId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/images/{imageId} [get]
|
||||||
func (h *ociConfigHandler) getImage(c *gin.Context) {
|
func (h *ociConfigHandler) getImage(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -67,6 +86,12 @@ type renameRequest struct {
|
|||||||
DisplayName string `json:"displayName" binding:"required"`
|
DisplayName string `json:"displayName" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary VCN 列表
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns [get]
|
||||||
func (h *ociConfigHandler) listVCNs(c *gin.Context) {
|
func (h *ociConfigHandler) listVCNs(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -80,6 +105,13 @@ func (h *ociConfigHandler) listVCNs(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, vcns)
|
c.JSON(http.StatusOK, vcns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建 VCN
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createVCNRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns [post]
|
||||||
func (h *ociConfigHandler) createVCN(c *gin.Context) {
|
func (h *ociConfigHandler) createVCN(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -105,6 +137,13 @@ func (h *ociConfigHandler) createVCN(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, vcn)
|
c.JSON(http.StatusCreated, vcn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary VCN 详情
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param vcnId path string true "vcnId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [get]
|
||||||
func (h *ociConfigHandler) getVCN(c *gin.Context) {
|
func (h *ociConfigHandler) getVCN(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -118,6 +157,14 @@ func (h *ociConfigHandler) getVCN(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, vcn)
|
c.JSON(http.StatusOK, vcn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新 VCN
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param vcnId path string true "vcnId"
|
||||||
|
// @Param body body renameRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [put]
|
||||||
func (h *ociConfigHandler) updateVCN(c *gin.Context) {
|
func (h *ociConfigHandler) updateVCN(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -136,6 +183,13 @@ func (h *ociConfigHandler) updateVCN(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, vcn)
|
c.JSON(http.StatusOK, vcn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除 VCN
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param vcnId path string true "vcnId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId} [delete]
|
||||||
func (h *ociConfigHandler) deleteVCN(c *gin.Context) {
|
func (h *ociConfigHandler) deleteVCN(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -152,6 +206,14 @@ type enableIPv6Request struct {
|
|||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary VCN 启用 IPv6
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param vcnId path string true "vcnId"
|
||||||
|
// @Param body body enableIPv6Request true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/vcns/{vcnId}/enable-ipv6 [post]
|
||||||
func (h *ociConfigHandler) enableVCNIPv6(c *gin.Context) {
|
func (h *ociConfigHandler) enableVCNIPv6(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -182,6 +244,12 @@ type createSubnetRequest struct {
|
|||||||
ProhibitPublicIP bool `json:"prohibitPublicIp"`
|
ProhibitPublicIP bool `json:"prohibitPublicIp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 子网列表
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subnets [get]
|
||||||
func (h *ociConfigHandler) listSubnets(c *gin.Context) {
|
func (h *ociConfigHandler) listSubnets(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -195,6 +263,13 @@ func (h *ociConfigHandler) listSubnets(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, subnets)
|
c.JSON(http.StatusOK, subnets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建子网
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createSubnetRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subnets [post]
|
||||||
func (h *ociConfigHandler) createSubnet(c *gin.Context) {
|
func (h *ociConfigHandler) createSubnet(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -221,6 +296,13 @@ func (h *ociConfigHandler) createSubnet(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, subnet)
|
c.JSON(http.StatusCreated, subnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 子网详情
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param subnetId path string true "subnetId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [get]
|
||||||
func (h *ociConfigHandler) getSubnet(c *gin.Context) {
|
func (h *ociConfigHandler) getSubnet(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -234,6 +316,14 @@ func (h *ociConfigHandler) getSubnet(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, subnet)
|
c.JSON(http.StatusOK, subnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新子网
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param subnetId path string true "subnetId"
|
||||||
|
// @Param body body renameRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [put]
|
||||||
func (h *ociConfigHandler) updateSubnet(c *gin.Context) {
|
func (h *ociConfigHandler) updateSubnet(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -252,6 +342,13 @@ func (h *ociConfigHandler) updateSubnet(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, subnet)
|
c.JSON(http.StatusOK, subnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除子网
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param subnetId path string true "subnetId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subnets/{subnetId} [delete]
|
||||||
func (h *ociConfigHandler) deleteSubnet(c *gin.Context) {
|
func (h *ociConfigHandler) deleteSubnet(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -281,6 +378,12 @@ type updateSecurityListRequest struct {
|
|||||||
EgressRules *[]oci.SecurityRule `json:"egressRules"`
|
EgressRules *[]oci.SecurityRule `json:"egressRules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 安全列表清单
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/security-lists [get]
|
||||||
func (h *ociConfigHandler) listSecurityLists(c *gin.Context) {
|
func (h *ociConfigHandler) listSecurityLists(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -294,6 +397,13 @@ func (h *ociConfigHandler) listSecurityLists(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, lists)
|
c.JSON(http.StatusOK, lists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建安全列表
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createSecurityListRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/security-lists [post]
|
||||||
func (h *ociConfigHandler) createSecurityList(c *gin.Context) {
|
func (h *ociConfigHandler) createSecurityList(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -318,6 +428,13 @@ func (h *ociConfigHandler) createSecurityList(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, list)
|
c.JSON(http.StatusCreated, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 安全列表详情
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param securityListId path string true "securityListId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [get]
|
||||||
func (h *ociConfigHandler) getSecurityList(c *gin.Context) {
|
func (h *ociConfigHandler) getSecurityList(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -331,6 +448,14 @@ func (h *ociConfigHandler) getSecurityList(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, list)
|
c.JSON(http.StatusOK, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新安全列表
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param securityListId path string true "securityListId"
|
||||||
|
// @Param body body updateSecurityListRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [put]
|
||||||
func (h *ociConfigHandler) updateSecurityList(c *gin.Context) {
|
func (h *ociConfigHandler) updateSecurityList(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -354,6 +479,13 @@ func (h *ociConfigHandler) updateSecurityList(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, list)
|
c.JSON(http.StatusOK, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除安全列表
|
||||||
|
// @Tags 网络
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param securityListId path string true "securityListId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/security-lists/{securityListId} [delete]
|
||||||
func (h *ociConfigHandler) deleteSecurityList(c *gin.Context) {
|
func (h *ociConfigHandler) deleteSecurityList(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ type importRequest struct {
|
|||||||
ProxyID *uint `json:"proxyId"` // 关联出站代理,null 直连
|
ProxyID *uint `json:"proxyId"` // 关联出站代理,null 直连
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 导入租户配置
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param body body importRequest true "API Key 配置(私钥密文落库)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs [post]
|
||||||
func (h *ociConfigHandler) create(c *gin.Context) {
|
func (h *ociConfigHandler) create(c *gin.Context) {
|
||||||
var req importRequest
|
var req importRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -60,6 +66,11 @@ func (h *ociConfigHandler) create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, cfg)
|
c.JSON(http.StatusCreated, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 租户配置列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Success 200 {object} map[string]any "items"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs [get]
|
||||||
func (h *ociConfigHandler) list(c *gin.Context) {
|
func (h *ociConfigHandler) list(c *gin.Context) {
|
||||||
items, err := h.svc.List(c.Request.Context())
|
items, err := h.svc.List(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,6 +80,12 @@ func (h *ociConfigHandler) list(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, items)
|
c.JSON(http.StatusOK, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 租户配置详情
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id} [get]
|
||||||
func (h *ociConfigHandler) get(c *gin.Context) {
|
func (h *ociConfigHandler) get(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -82,6 +99,12 @@ func (h *ociConfigHandler) get(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, cfg)
|
c.JSON(http.StatusOK, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 验证配置连通性并刷新画像
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/verify [post]
|
||||||
func (h *ociConfigHandler) verify(c *gin.Context) {
|
func (h *ociConfigHandler) verify(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -109,6 +132,13 @@ type updateConfigRequest struct {
|
|||||||
ProxyID *uint `json:"proxyId"` // 非 null 切换代理:0 解除,>0 关联
|
ProxyID *uint `json:"proxyId"` // 非 null 切换代理:0 解除,>0 关联
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新租户配置
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "可更新字段(别名/分组/代理/多区域开关等)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id} [put]
|
||||||
func (h *ociConfigHandler) update(c *gin.Context) {
|
func (h *ociConfigHandler) update(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -137,6 +167,12 @@ func (h *ociConfigHandler) update(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"config": cfg, "changes": changes})
|
c.JSON(http.StatusOK, gin.H{"config": cfg, "changes": changes})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除租户配置
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id} [delete]
|
||||||
func (h *ociConfigHandler) remove(c *gin.Context) {
|
func (h *ociConfigHandler) remove(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -150,6 +186,13 @@ func (h *ociConfigHandler) remove(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compartments 列出租户下全部 ACTIVE compartment(不含租户根)。
|
// compartments 列出租户下全部 ACTIVE compartment(不含租户根)。
|
||||||
|
//
|
||||||
|
// @Summary 列出租户下全部 ACTIVE compartment
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/compartments [get]
|
||||||
func (h *ociConfigHandler) compartments(c *gin.Context) {
|
func (h *ociConfigHandler) compartments(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -165,6 +208,13 @@ func (h *ociConfigHandler) compartments(c *gin.Context) {
|
|||||||
|
|
||||||
// cachedRegions 返回筛选器用区域列表:未开多区域支持只含默认区域;
|
// cachedRegions 返回筛选器用区域列表:未开多区域支持只含默认区域;
|
||||||
// 缓存存在非 READY 时实时刷新。
|
// 缓存存在非 READY 时实时刷新。
|
||||||
|
//
|
||||||
|
// @Summary 返回筛选器用区域列表:未开多区域支持只含默认区域
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/cached-regions [get]
|
||||||
func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
|
func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -179,6 +229,13 @@ func (h *ociConfigHandler) cachedRegions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cachedCompartments 返回筛选器用区间列表:未开多区间支持返回空数组。
|
// cachedCompartments 返回筛选器用区间列表:未开多区间支持返回空数组。
|
||||||
|
//
|
||||||
|
// @Summary 返回筛选器用区间列表:未开多区间支持返回空数组
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/cached-compartments [get]
|
||||||
func (h *ociConfigHandler) cachedCompartments(c *gin.Context) {
|
func (h *ociConfigHandler) cachedCompartments(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// overview 返回总览页聚合数据(本地快照,不发云端请求)。
|
// overview 返回总览页聚合数据(本地快照,不发云端请求)。
|
||||||
|
//
|
||||||
|
// @Summary 返回总览页聚合数据
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/overview [get]
|
||||||
func (h *ociConfigHandler) overview(c *gin.Context) {
|
func (h *ociConfigHandler) overview(c *gin.Context) {
|
||||||
out, err := h.svc.Overview(c.Request.Context())
|
out, err := h.svc.Overview(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ type proxyHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list 返回全部代理(脱敏视图,含引用计数)。
|
// list 返回全部代理(脱敏视图,含引用计数)。
|
||||||
|
//
|
||||||
|
// @Summary 代理列表
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any "items"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies [get]
|
||||||
func (h *proxyHandler) list(c *gin.Context) {
|
func (h *proxyHandler) list(c *gin.Context) {
|
||||||
items, err := h.svc.List(c.Request.Context())
|
items, err := h.svc.List(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -25,6 +31,13 @@ func (h *proxyHandler) list(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create 新建代理。
|
// create 新建代理。
|
||||||
|
//
|
||||||
|
// @Summary 创建代理
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body service.ProxyInput true "代理配置(密码只写不回)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies [post]
|
||||||
func (h *proxyHandler) create(c *gin.Context) {
|
func (h *proxyHandler) create(c *gin.Context) {
|
||||||
var req service.ProxyInput
|
var req service.ProxyInput
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -40,6 +53,14 @@ func (h *proxyHandler) create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update 更新代理;密码缺省沿用、空串清除。
|
// update 更新代理;密码缺省沿用、空串清除。
|
||||||
|
//
|
||||||
|
// @Summary 更新代理
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param id path int true "代理 ID"
|
||||||
|
// @Param body body service.ProxyInput true "代理配置(密码缺省沿用)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies/{id} [put]
|
||||||
func (h *proxyHandler) update(c *gin.Context) {
|
func (h *proxyHandler) update(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -59,6 +80,13 @@ func (h *proxyHandler) update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove 删除代理;仍被租户引用时返回 409。
|
// remove 删除代理;仍被租户引用时返回 409。
|
||||||
|
//
|
||||||
|
// @Summary 删除代理
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param id path int true "代理 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies/{id} [delete]
|
||||||
func (h *proxyHandler) remove(c *gin.Context) {
|
func (h *proxyHandler) remove(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -72,6 +100,13 @@ func (h *proxyHandler) remove(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// importBatch 批量导入代理;逐行解析,返回成功与脱敏后的失败明细。
|
// importBatch 批量导入代理;逐行解析,返回成功与脱敏后的失败明细。
|
||||||
|
//
|
||||||
|
// @Summary 批量导入代理
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies/import [post]
|
||||||
func (h *proxyHandler) importBatch(c *gin.Context) {
|
func (h *proxyHandler) importBatch(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Text string `json:"text" binding:"required"`
|
Text string `json:"text" binding:"required"`
|
||||||
@@ -89,6 +124,13 @@ func (h *proxyHandler) importBatch(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// probe 手动重测代理出口地区,同步返回最新视图。
|
// probe 手动重测代理出口地区,同步返回最新视图。
|
||||||
|
//
|
||||||
|
// @Summary 手动重测代理出口地区,同步返回最新视图
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/proxies/{id}/probe [post]
|
||||||
func (h *proxyHandler) probe(c *gin.Context) {
|
func (h *proxyHandler) probe(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -22,8 +23,9 @@ type ipRateLimiter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ipEntry struct {
|
type ipEntry struct {
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
lastSeen time.Time
|
// lastSeen 是 UnixNano 时间戳;请求路径无锁写、回收循环无锁读
|
||||||
|
lastSeen atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIPRateLimiter() *ipRateLimiter {
|
func newIPRateLimiter() *ipRateLimiter {
|
||||||
@@ -33,6 +35,7 @@ func newIPRateLimiter() *ipRateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get 返回 ip 对应的令牌桶,参数与当前配置不一致时就地调整。
|
// get 返回 ip 对应的令牌桶,参数与当前配置不一致时就地调整。
|
||||||
|
// lastSeen 用原子时间戳:并发请求与回收循环两侧无锁读写,避免数据竞态。
|
||||||
func (l *ipRateLimiter) get(ip string, rps, burst int) *rate.Limiter {
|
func (l *ipRateLimiter) get(ip string, rps, burst int) *rate.Limiter {
|
||||||
l.mu.RLock()
|
l.mu.RLock()
|
||||||
entry, ok := l.limiters[ip]
|
entry, ok := l.limiters[ip]
|
||||||
@@ -45,7 +48,7 @@ func (l *ipRateLimiter) get(ip string, rps, burst int) *rate.Limiter {
|
|||||||
}
|
}
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
}
|
}
|
||||||
entry.lastSeen = time.Now()
|
entry.lastSeen.Store(time.Now().UnixNano())
|
||||||
if entry.limiter.Limit() != rate.Limit(rps) || entry.limiter.Burst() != burst {
|
if entry.limiter.Limit() != rate.Limit(rps) || entry.limiter.Burst() != burst {
|
||||||
entry.limiter.SetLimit(rate.Limit(rps))
|
entry.limiter.SetLimit(rate.Limit(rps))
|
||||||
entry.limiter.SetBurst(burst)
|
entry.limiter.SetBurst(burst)
|
||||||
@@ -58,9 +61,9 @@ func (l *ipRateLimiter) evictLoop() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
cutoff := time.Now().Add(-ipEvictInterval)
|
cutoff := time.Now().Add(-ipEvictInterval).UnixNano()
|
||||||
for ip, e := range l.limiters {
|
for ip, e := range l.limiters {
|
||||||
if e.lastSeen.Before(cutoff) {
|
if e.lastSeen.Load() < cutoff {
|
||||||
delete(l.limiters, ip)
|
delete(l.limiters, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// listRegions 返回本地维护的完整区域表(含友好名称),不发起云端请求。
|
// listRegions 返回本地维护的完整区域表(含友好名称),不发起云端请求。
|
||||||
|
//
|
||||||
|
// @Summary 返回本地维护的完整区域表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/regions [get]
|
||||||
func listRegions(c *gin.Context) {
|
func listRegions(c *gin.Context) {
|
||||||
regions, err := oci.AllRegions()
|
regions, err := oci.AllRegions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -19,6 +25,12 @@ func listRegions(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, regions)
|
c.JSON(http.StatusOK, regions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 区域订阅列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/region-subscriptions [get]
|
||||||
func (h *ociConfigHandler) regionSubscriptions(c *gin.Context) {
|
func (h *ociConfigHandler) regionSubscriptions(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -36,6 +48,13 @@ type subscribeRegionRequest struct {
|
|||||||
RegionKey string `json:"regionKey" binding:"required"`
|
RegionKey string `json:"regionKey" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 订阅新区域
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body subscribeRegionRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/region-subscriptions [post]
|
||||||
func (h *ociConfigHandler) subscribeRegion(c *gin.Context) {
|
func (h *ociConfigHandler) subscribeRegion(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -54,6 +73,12 @@ func (h *ociConfigHandler) subscribeRegion(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, subs)
|
c.JSON(http.StatusOK, subs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 服务限额查询
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/limits [get]
|
||||||
func (h *ociConfigHandler) limits(c *gin.Context) {
|
func (h *ociConfigHandler) limits(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -1,217 +1,52 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"oci-portal/internal/service"
|
"oci-portal/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRouter 组装全部 HTTP 路由:/auth/login 公开,业务路由要求 JWT。
|
// NewRouter 组装 HTTP 路由骨架:中间件链、公开/鉴权分组与各域注册;
|
||||||
|
// 具体路由按域拆在 routes_*.go。/auth/login 公开,业务路由要求 JWT。
|
||||||
func NewRouter(auth *service.AuthService, oauth *service.OAuthService, ociConfigs *service.OciConfigService, tasks *service.TaskService, console *service.ConsoleService, settings *service.SettingService, notifier *service.Notifier, systemLogs *service.SystemLogService, logEvents *service.LogEventService, proxies *service.ProxyService, aiGateway *service.AiGatewayService) *gin.Engine {
|
func NewRouter(auth *service.AuthService, oauth *service.OAuthService, ociConfigs *service.OciConfigService, tasks *service.TaskService, console *service.ConsoleService, settings *service.SettingService, notifier *service.Notifier, systemLogs *service.SystemLogService, logEvents *service.LogEventService, proxies *service.ProxyService, aiGateway *service.AiGatewayService) *gin.Engine {
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// 禁用 gin 内置代理信任:真实 IP 统一由 RealIPMiddleware 按安全设置解析
|
// 禁用 gin 内置代理信任:真实 IP 统一由 RealIPMiddleware 按安全设置解析
|
||||||
_ = r.SetTrustedProxies(nil)
|
_ = r.SetTrustedProxies(nil)
|
||||||
|
|
||||||
// webhook 注册在全局 Logger 之前:访问日志不落含 secret 的路径;
|
registerWebhook(r, settings, logEvents)
|
||||||
// 不挂系统日志中间件——投递高频且已在回传日志留痕,避免刷屏
|
|
||||||
|
// 访问日志不落 query string:webconsole WS 的 JWT 经 query 传递,默认 Logger 会打进 stdout/journal
|
||||||
|
accessLog := gin.LoggerWithConfig(gin.LoggerConfig{SkipQueryString: true})
|
||||||
|
r.Use(accessLog, RealIPMiddleware(settings), IPRateMiddleware(settings), gin.Recovery())
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
registerAuthPublic(v1, auth, oauth, systemLogs)
|
||||||
|
// AI 网关对外端点:独立密钥鉴权,挂在全局 Use 之后,仍受 IP 限速与 Recovery 保护
|
||||||
|
registerAiGateway(r, aiGateway)
|
||||||
|
|
||||||
|
// 系统日志中间件挂在鉴权之后,写请求自动留痕(webconsole ws 为 GET,天然跳过)
|
||||||
|
secured := v1.Group("", RequireAuth(auth), systemLogMiddleware(systemLogs))
|
||||||
|
registerAuthSecured(secured, auth, oauth, settings, systemLogs)
|
||||||
|
registerConsole(v1, secured, console, auth)
|
||||||
|
registerSettings(secured, settings, notifier, systemLogs, proxies)
|
||||||
|
registerTasksAndLogs(secured, tasks, logEvents)
|
||||||
|
registerOci(secured, ociConfigs)
|
||||||
|
registerAiAdmin(secured, aiGateway)
|
||||||
|
registerSwagger(r)
|
||||||
|
|
||||||
|
// 嵌入的前端产物兜底伺服;失败仅降级为纯 API 服务,不阻塞启动
|
||||||
|
if err := registerWebUI(r); err != nil {
|
||||||
|
log.Printf("[warn] webui disabled: %v", err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerWebhook 挂载 ONS 回传入口。必须注册在全局 Logger 之前:
|
||||||
|
// 访问日志不落含 secret 的路径;不挂系统日志中间件——投递高频且已在回传日志留痕。
|
||||||
|
func registerWebhook(r *gin.Engine, settings *service.SettingService, logEvents *service.LogEventService) {
|
||||||
wh := &webhookHandler{events: logEvents}
|
wh := &webhookHandler{events: logEvents}
|
||||||
r.POST("/api/v1/webhooks/oci-logs/:secret",
|
r.POST("/api/v1/webhooks/oci-logs/:secret",
|
||||||
gin.Recovery(), RealIPMiddleware(settings), wh.handle)
|
gin.Recovery(), RealIPMiddleware(settings), wh.handle)
|
||||||
|
|
||||||
r.Use(gin.Logger(), RealIPMiddleware(settings), IPRateMiddleware(settings), gin.Recovery())
|
|
||||||
|
|
||||||
v1 := r.Group("/api/v1")
|
|
||||||
ah := &authHandler{svc: auth, logs: systemLogs}
|
|
||||||
v1.POST("/auth/login", ah.login)
|
|
||||||
|
|
||||||
// 外部身份登录(公开):provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
|
||||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
|
||||||
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
|
||||||
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
|
||||||
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
|
||||||
|
|
||||||
// 控制台数据面:浏览器 WebSocket 无法带自定义头,token 经 query 在 handler 内校验
|
|
||||||
ch := &consoleHandler{svc: console, auth: auth}
|
|
||||||
v1.GET("/console-sessions/:sessionId/ws", ch.ws)
|
|
||||||
|
|
||||||
// AI 网关对外端点:独立密钥鉴权(Bearer / x-api-key),不挂 JWT 与系统日志
|
|
||||||
// (高频调用自有 AiCallLog);挂在全局 Use 之后,仍受 IP 限速与 Recovery 保护
|
|
||||||
aih := &aiGatewayHandler{gw: aiGateway}
|
|
||||||
ai := r.Group("/ai/v1", aih.auth)
|
|
||||||
ai.POST("/chat/completions", aih.chatCompletions)
|
|
||||||
ai.POST("/responses", aih.responses)
|
|
||||||
ai.POST("/messages", aih.messages)
|
|
||||||
ai.POST("/embeddings", aih.embeddings)
|
|
||||||
ai.GET("/models", aih.listModels)
|
|
||||||
|
|
||||||
// 系统日志中间件挂在鉴权之后,写请求自动留痕(webconsole ws 为 GET,天然跳过)
|
|
||||||
secured := v1.Group("", RequireAuth(auth), systemLogMiddleware(systemLogs))
|
|
||||||
secured.POST("/auth/logout", ah.logout)
|
|
||||||
secured.GET("/regions", listRegions)
|
|
||||||
secured.GET("/system-logs", (&systemLogHandler{svc: systemLogs}).list)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/console-sessions", ch.create)
|
|
||||||
secured.DELETE("/console-sessions/:sessionId", ch.remove)
|
|
||||||
|
|
||||||
st := &settingsHandler{svc: settings, notifier: notifier}
|
|
||||||
secured.GET("/about", about)
|
|
||||||
secured.GET("/settings/telegram", st.getTelegram)
|
|
||||||
secured.PUT("/settings/telegram", st.updateTelegram)
|
|
||||||
secured.POST("/settings/telegram/test", st.testTelegram)
|
|
||||||
secured.GET("/settings/notify-events", st.getNotifyEvents)
|
|
||||||
secured.PUT("/settings/notify-events", st.updateNotifyEvents)
|
|
||||||
secured.GET("/settings/notify-templates", st.listNotifyTemplates)
|
|
||||||
secured.PUT("/settings/notify-templates/:kind", st.updateNotifyTemplate)
|
|
||||||
secured.POST("/settings/notify-templates/:kind/test", st.testNotifyTemplate)
|
|
||||||
secured.GET("/settings/task", st.getTaskSettings)
|
|
||||||
secured.PUT("/settings/task", st.updateTaskSettings)
|
|
||||||
secured.GET("/settings/security", st.getSecurity)
|
|
||||||
secured.PUT("/settings/security", st.updateSecurity)
|
|
||||||
|
|
||||||
// 两步验证与外部身份管理(JWT 组内)
|
|
||||||
secured.GET("/auth/totp", ax.totpStatus)
|
|
||||||
secured.POST("/auth/totp/setup", ax.totpSetup)
|
|
||||||
secured.POST("/auth/totp/activate", ax.totpActivate)
|
|
||||||
secured.POST("/auth/totp/disable", ax.totpDisable)
|
|
||||||
secured.GET("/auth/credentials", ax.getCredentials)
|
|
||||||
secured.PUT("/auth/credentials", ax.updateCredentials)
|
|
||||||
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
|
||||||
secured.GET("/auth/identities", ax.identities)
|
|
||||||
secured.DELETE("/auth/identities/:id", ax.unbindIdentity)
|
|
||||||
secured.GET("/settings/oauth", func(c *gin.Context) { ax.getOAuthSettings(c, settings) })
|
|
||||||
secured.PUT("/settings/oauth", func(c *gin.Context) { ax.updateOAuthSettings(c, settings) })
|
|
||||||
|
|
||||||
px := &proxyHandler{svc: proxies}
|
|
||||||
secured.GET("/proxies", px.list)
|
|
||||||
secured.POST("/proxies", px.create)
|
|
||||||
secured.POST("/proxies/import", px.importBatch)
|
|
||||||
secured.PUT("/proxies/:id", px.update)
|
|
||||||
secured.DELETE("/proxies/:id", px.remove)
|
|
||||||
secured.POST("/proxies/:id/probe", px.probe)
|
|
||||||
|
|
||||||
le := &logEventHandler{svc: logEvents}
|
|
||||||
secured.GET("/log-events", le.list)
|
|
||||||
secured.GET("/oci-configs/:id/log-webhook", le.getWebhook)
|
|
||||||
secured.POST("/oci-configs/:id/log-webhook", le.ensureWebhook)
|
|
||||||
secured.DELETE("/oci-configs/:id/log-webhook", le.revokeWebhook)
|
|
||||||
secured.GET("/oci-configs/:id/log-relay", le.getRelay)
|
|
||||||
secured.POST("/oci-configs/:id/log-relay", le.setupRelay)
|
|
||||||
secured.DELETE("/oci-configs/:id/log-relay", le.teardownRelay)
|
|
||||||
|
|
||||||
t := &taskHandler{svc: tasks}
|
|
||||||
secured.POST("/tasks", t.create)
|
|
||||||
secured.GET("/tasks", t.list)
|
|
||||||
secured.GET("/tasks/:id", t.get)
|
|
||||||
secured.PUT("/tasks/:id", t.update)
|
|
||||||
secured.DELETE("/tasks/:id", t.remove)
|
|
||||||
secured.GET("/tasks/:id/logs", t.logs)
|
|
||||||
secured.POST("/tasks/:id/run", t.run)
|
|
||||||
|
|
||||||
h := &ociConfigHandler{svc: ociConfigs}
|
|
||||||
secured.GET("/overview", h.overview)
|
|
||||||
secured.POST("/oci-configs", h.create)
|
|
||||||
secured.GET("/oci-configs", h.list)
|
|
||||||
secured.GET("/oci-configs/:id", h.get)
|
|
||||||
secured.PUT("/oci-configs/:id", h.update)
|
|
||||||
secured.POST("/oci-configs/:id/verify", h.verify)
|
|
||||||
secured.DELETE("/oci-configs/:id", h.remove)
|
|
||||||
secured.GET("/oci-configs/:id/compartments", h.compartments)
|
|
||||||
secured.GET("/oci-configs/:id/cached-regions", h.cachedRegions)
|
|
||||||
secured.GET("/oci-configs/:id/cached-compartments", h.cachedCompartments)
|
|
||||||
secured.GET("/oci-configs/:id/region-subscriptions", h.regionSubscriptions)
|
|
||||||
secured.POST("/oci-configs/:id/region-subscriptions", h.subscribeRegion)
|
|
||||||
secured.GET("/oci-configs/:id/limits", h.limits)
|
|
||||||
secured.GET("/oci-configs/:id/limits/services", h.limitServices)
|
|
||||||
secured.GET("/oci-configs/:id/subscriptions", h.subscriptions)
|
|
||||||
secured.GET("/oci-configs/:id/subscriptions/:subscriptionId", h.subscriptionDetail)
|
|
||||||
secured.GET("/oci-configs/:id/shapes", h.shapes)
|
|
||||||
secured.GET("/oci-configs/:id/images", h.images)
|
|
||||||
secured.GET("/oci-configs/:id/images/:imageId", h.getImage)
|
|
||||||
secured.GET("/oci-configs/:id/vcns", h.listVCNs)
|
|
||||||
secured.POST("/oci-configs/:id/vcns", h.createVCN)
|
|
||||||
secured.GET("/oci-configs/:id/vcns/:vcnId", h.getVCN)
|
|
||||||
secured.PUT("/oci-configs/:id/vcns/:vcnId", h.updateVCN)
|
|
||||||
secured.DELETE("/oci-configs/:id/vcns/:vcnId", h.deleteVCN)
|
|
||||||
secured.POST("/oci-configs/:id/vcns/:vcnId/enable-ipv6", h.enableVCNIPv6)
|
|
||||||
secured.GET("/oci-configs/:id/subnets", h.listSubnets)
|
|
||||||
secured.POST("/oci-configs/:id/subnets", h.createSubnet)
|
|
||||||
secured.GET("/oci-configs/:id/subnets/:subnetId", h.getSubnet)
|
|
||||||
secured.PUT("/oci-configs/:id/subnets/:subnetId", h.updateSubnet)
|
|
||||||
secured.DELETE("/oci-configs/:id/subnets/:subnetId", h.deleteSubnet)
|
|
||||||
secured.GET("/oci-configs/:id/security-lists", h.listSecurityLists)
|
|
||||||
secured.POST("/oci-configs/:id/security-lists", h.createSecurityList)
|
|
||||||
secured.GET("/oci-configs/:id/security-lists/:securityListId", h.getSecurityList)
|
|
||||||
secured.PUT("/oci-configs/:id/security-lists/:securityListId", h.updateSecurityList)
|
|
||||||
secured.DELETE("/oci-configs/:id/security-lists/:securityListId", h.deleteSecurityList)
|
|
||||||
secured.GET("/oci-configs/:id/availability-domains", h.availabilityDomains)
|
|
||||||
secured.GET("/oci-configs/:id/instances", h.listInstances)
|
|
||||||
secured.POST("/oci-configs/:id/instances", h.createInstance)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId", h.getInstance)
|
|
||||||
secured.PUT("/oci-configs/:id/instances/:instanceId", h.updateInstance)
|
|
||||||
secured.DELETE("/oci-configs/:id/instances/:instanceId", h.terminateInstance)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/action", h.instanceAction)
|
|
||||||
secured.GET("/oci-configs/:id/boot-volumes", h.listBootVolumes)
|
|
||||||
secured.GET("/oci-configs/:id/boot-volumes/:bootVolumeId", h.getBootVolume)
|
|
||||||
secured.PUT("/oci-configs/:id/boot-volumes/:bootVolumeId", h.updateBootVolume)
|
|
||||||
secured.DELETE("/oci-configs/:id/boot-volumes/:bootVolumeId", h.deleteBootVolume)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/change-public-ip", h.changePublicIP)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.addInstanceIpv6)
|
|
||||||
secured.DELETE("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.deleteInstanceIpv6)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/console-connections", h.createConsoleConnection)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId/console-connections", h.listConsoleConnections)
|
|
||||||
secured.DELETE("/oci-configs/:id/console-connections/:connectionId", h.deleteConsoleConnection)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId/vnics", h.listInstanceVnics)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/vnics", h.attachVnic)
|
|
||||||
secured.DELETE("/oci-configs/:id/vnic-attachments/:attachmentId", h.detachVnic)
|
|
||||||
secured.POST("/oci-configs/:id/vnics/:vnicId/ipv6-addresses", h.addVnicIpv6)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.listBootVolumeAttachments)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.attachBootVolume)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/replace-boot-volume", h.replaceBootVolume)
|
|
||||||
secured.DELETE("/oci-configs/:id/boot-volume-attachments/:attachmentId", h.detachBootVolume)
|
|
||||||
secured.GET("/oci-configs/:id/volumes", h.listBlockVolumes)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId/volume-attachments", h.listVolumeAttachments)
|
|
||||||
secured.POST("/oci-configs/:id/instances/:instanceId/volume-attachments", h.attachVolume)
|
|
||||||
secured.DELETE("/oci-configs/:id/volume-attachments/:attachmentId", h.detachVolume)
|
|
||||||
secured.GET("/oci-configs/:id/instances/:instanceId/traffic", h.instanceTraffic)
|
|
||||||
secured.GET("/oci-configs/:id/costs", h.costs)
|
|
||||||
// AI 网关面板管理:密钥 / 渠道(号池)/ 聚合模型 / 调用日志
|
|
||||||
aiadmin := &aiAdminHandler{gw: aiGateway}
|
|
||||||
secured.GET("/ai-keys", aiadmin.listKeys)
|
|
||||||
secured.POST("/ai-keys", aiadmin.createKey)
|
|
||||||
secured.PUT("/ai-keys/:id", aiadmin.updateKey)
|
|
||||||
secured.PUT("/ai-keys/:id/content-log", aiadmin.updateKeyContentLog)
|
|
||||||
secured.DELETE("/ai-keys/:id", aiadmin.deleteKey)
|
|
||||||
secured.GET("/ai-channels", aiadmin.listChannels)
|
|
||||||
secured.POST("/ai-channels", aiadmin.createChannel)
|
|
||||||
secured.PUT("/ai-channels/:id", aiadmin.updateChannel)
|
|
||||||
secured.DELETE("/ai-channels/:id", aiadmin.deleteChannel)
|
|
||||||
secured.POST("/ai-channels/:id/probe", aiadmin.probeChannel)
|
|
||||||
secured.POST("/ai-channels/:id/sync-models", aiadmin.syncChannelModels)
|
|
||||||
secured.GET("/ai-models", aiadmin.gatewayModels)
|
|
||||||
secured.GET("/ai-logs", aiadmin.listLogs)
|
|
||||||
secured.GET("/ai-content-logs", aiadmin.listContentLogs)
|
|
||||||
secured.GET("/oci-configs/:id/audit-events", h.getAuditEvents)
|
|
||||||
secured.GET("/oci-configs/:id/audit-events/detail", h.getAuditEventDetail)
|
|
||||||
secured.GET("/oci-configs/:id/users", h.listTenantUsers)
|
|
||||||
secured.POST("/oci-configs/:id/users", h.createTenantUser)
|
|
||||||
secured.GET("/oci-configs/:id/users/:userId", h.getTenantUserDetail)
|
|
||||||
secured.PUT("/oci-configs/:id/users/:userId", h.updateTenantUser)
|
|
||||||
secured.DELETE("/oci-configs/:id/users/:userId", h.deleteTenantUser)
|
|
||||||
secured.POST("/oci-configs/:id/users/:userId/reset-password", h.resetTenantUserPassword)
|
|
||||||
secured.DELETE("/oci-configs/:id/users/:userId/mfa-devices", h.deleteTenantUserMfa)
|
|
||||||
secured.DELETE("/oci-configs/:id/users/:userId/api-keys", h.deleteTenantUserApiKeys)
|
|
||||||
secured.GET("/oci-configs/:id/notification-recipients", h.getNotificationRecipients)
|
|
||||||
secured.PUT("/oci-configs/:id/notification-recipients", h.updateNotificationRecipients)
|
|
||||||
secured.GET("/oci-configs/:id/password-policies", h.listPasswordPolicies)
|
|
||||||
secured.PUT("/oci-configs/:id/password-policies/:policyId", h.updatePasswordPolicy)
|
|
||||||
secured.GET("/oci-configs/:id/identity-settings", h.getIdentitySetting)
|
|
||||||
secured.PUT("/oci-configs/:id/identity-settings", h.updateIdentitySetting)
|
|
||||||
secured.GET("/oci-configs/:id/identity-providers", h.listIdentityProviders)
|
|
||||||
secured.POST("/oci-configs/:id/identity-providers", h.createIdentityProvider)
|
|
||||||
secured.POST("/oci-configs/:id/identity-providers/:idpId/activate", h.activateIdentityProvider)
|
|
||||||
secured.DELETE("/oci-configs/:id/identity-providers/:idpId", h.deleteIdentityProvider)
|
|
||||||
secured.GET("/oci-configs/:id/saml-metadata", h.downloadSamlMetadata)
|
|
||||||
secured.GET("/oci-configs/:id/sign-on-rules", h.listSignOnRules)
|
|
||||||
secured.POST("/oci-configs/:id/sign-on-exemptions", h.createMfaExemption)
|
|
||||||
secured.DELETE("/oci-configs/:id/sign-on-exemptions/:ruleId", h.deleteMfaExemption)
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerAiGateway 挂载 /ai/v1/* 对外网关端点(独立密钥鉴权,不挂 JWT 与系统日志)。
|
||||||
|
// 挂在全局中间件之后,仍受 IP 限速与 Recovery 保护;高频调用自有 AiCallLog。
|
||||||
|
func registerAiGateway(r *gin.Engine, aiGateway *service.AiGatewayService) {
|
||||||
|
aih := &aiGatewayHandler{gw: aiGateway}
|
||||||
|
ai := r.Group("/ai/v1", aih.auth)
|
||||||
|
ai.POST("/chat/completions", aih.chatCompletions)
|
||||||
|
ai.POST("/responses", aih.responses)
|
||||||
|
ai.POST("/messages", aih.messages)
|
||||||
|
ai.POST("/embeddings", aih.embeddings)
|
||||||
|
ai.GET("/models", aih.listModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerAiAdmin 挂载面板管理侧的 AI 密钥/渠道/模型/日志(JWT 组内)。
|
||||||
|
func registerAiAdmin(secured *gin.RouterGroup, aiGateway *service.AiGatewayService) {
|
||||||
|
aiadmin := &aiAdminHandler{gw: aiGateway}
|
||||||
|
secured.GET("/ai-keys", aiadmin.listKeys)
|
||||||
|
secured.POST("/ai-keys", aiadmin.createKey)
|
||||||
|
secured.PUT("/ai-keys/:id", aiadmin.updateKey)
|
||||||
|
secured.PUT("/ai-keys/:id/content-log", aiadmin.updateKeyContentLog)
|
||||||
|
secured.DELETE("/ai-keys/:id", aiadmin.deleteKey)
|
||||||
|
secured.GET("/ai-channels", aiadmin.listChannels)
|
||||||
|
secured.POST("/ai-channels", aiadmin.createChannel)
|
||||||
|
secured.PUT("/ai-channels/:id", aiadmin.updateChannel)
|
||||||
|
secured.DELETE("/ai-channels/:id", aiadmin.deleteChannel)
|
||||||
|
secured.POST("/ai-channels/:id/probe", aiadmin.probeChannel)
|
||||||
|
secured.POST("/ai-channels/:id/sync-models", aiadmin.syncChannelModels)
|
||||||
|
secured.GET("/ai-models", aiadmin.gatewayModels)
|
||||||
|
secured.GET("/ai-logs", aiadmin.listLogs)
|
||||||
|
secured.GET("/ai-content-logs", aiadmin.listContentLogs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerAuthPublic 公开登录/OAuth 路由(不经 JWT)。
|
||||||
|
func registerAuthPublic(v1 *gin.RouterGroup, auth *service.AuthService, oauth *service.OAuthService, systemLogs *service.SystemLogService) {
|
||||||
|
ah := &authHandler{svc: auth, logs: systemLogs}
|
||||||
|
v1.POST("/auth/login", ah.login)
|
||||||
|
|
||||||
|
// 外部身份登录:provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
||||||
|
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||||
|
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
||||||
|
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
||||||
|
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerAuthSecured 登出、两步验证、外部身份管理(JWT 组内)。
|
||||||
|
func registerAuthSecured(secured *gin.RouterGroup, auth *service.AuthService, oauth *service.OAuthService, settings *service.SettingService, systemLogs *service.SystemLogService) {
|
||||||
|
ah := &authHandler{svc: auth, logs: systemLogs}
|
||||||
|
secured.POST("/auth/logout", ah.logout)
|
||||||
|
|
||||||
|
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||||
|
secured.GET("/auth/totp", ax.totpStatus)
|
||||||
|
secured.POST("/auth/totp/setup", ax.totpSetup)
|
||||||
|
secured.POST("/auth/totp/activate", ax.totpActivate)
|
||||||
|
secured.POST("/auth/totp/disable", ax.totpDisable)
|
||||||
|
secured.GET("/auth/credentials", ax.getCredentials)
|
||||||
|
secured.PUT("/auth/credentials", ax.updateCredentials)
|
||||||
|
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
||||||
|
secured.GET("/auth/identities", ax.identities)
|
||||||
|
secured.DELETE("/auth/identities/:id", ax.unbindIdentity)
|
||||||
|
secured.GET("/settings/oauth", func(c *gin.Context) { ax.getOAuthSettings(c, settings) })
|
||||||
|
secured.PUT("/settings/oauth", func(c *gin.Context) { ax.updateOAuthSettings(c, settings) })
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerConsole 网页控制台:ws 数据面挂 v1 组(浏览器 WebSocket 无法带自定义头,
|
||||||
|
// token 经 query 在 handler 内校验),会话创建/删除走 JWT 组。
|
||||||
|
func registerConsole(v1, secured *gin.RouterGroup, console *service.ConsoleService, auth *service.AuthService) {
|
||||||
|
ch := &consoleHandler{svc: console, auth: auth}
|
||||||
|
v1.GET("/console-sessions/:sessionId/ws", ch.ws)
|
||||||
|
secured.POST("/oci-configs/:id/instances/:instanceId/console-sessions", ch.create)
|
||||||
|
secured.DELETE("/console-sessions/:sessionId", ch.remove)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerOci 挂载 OCI 租户配置及其资源的全部路由;共用 ociConfigHandler。
|
||||||
|
// 按资源域顺序分节注册,便于维护:配置元数据 → 计算 → 网络 → 存储 → IAM/审计。
|
||||||
|
func registerOci(secured *gin.RouterGroup, ociConfigs *service.OciConfigService) {
|
||||||
|
h := &ociConfigHandler{svc: ociConfigs}
|
||||||
|
secured.GET("/regions", listRegions)
|
||||||
|
secured.GET("/overview", h.overview)
|
||||||
|
registerOciConfig(secured, h)
|
||||||
|
registerOciCompute(secured, h)
|
||||||
|
registerOciNetwork(secured, h)
|
||||||
|
registerOciStorage(secured, h)
|
||||||
|
registerOciCost(secured, h)
|
||||||
|
registerOciTenantIAM(secured, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciConfig 配置本身 + 区域/区间/订阅/限额/形状/镜像。
|
||||||
|
func registerOciConfig(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.POST("/oci-configs", h.create)
|
||||||
|
g.GET("/oci-configs", h.list)
|
||||||
|
g.GET("/oci-configs/:id", h.get)
|
||||||
|
g.PUT("/oci-configs/:id", h.update)
|
||||||
|
g.POST("/oci-configs/:id/verify", h.verify)
|
||||||
|
g.DELETE("/oci-configs/:id", h.remove)
|
||||||
|
g.GET("/oci-configs/:id/compartments", h.compartments)
|
||||||
|
g.GET("/oci-configs/:id/cached-regions", h.cachedRegions)
|
||||||
|
g.GET("/oci-configs/:id/cached-compartments", h.cachedCompartments)
|
||||||
|
g.GET("/oci-configs/:id/region-subscriptions", h.regionSubscriptions)
|
||||||
|
g.POST("/oci-configs/:id/region-subscriptions", h.subscribeRegion)
|
||||||
|
g.GET("/oci-configs/:id/limits", h.limits)
|
||||||
|
g.GET("/oci-configs/:id/limits/services", h.limitServices)
|
||||||
|
g.GET("/oci-configs/:id/subscriptions", h.subscriptions)
|
||||||
|
g.GET("/oci-configs/:id/subscriptions/:subscriptionId", h.subscriptionDetail)
|
||||||
|
g.GET("/oci-configs/:id/shapes", h.shapes)
|
||||||
|
g.GET("/oci-configs/:id/images", h.images)
|
||||||
|
g.GET("/oci-configs/:id/images/:imageId", h.getImage)
|
||||||
|
g.GET("/oci-configs/:id/availability-domains", h.availabilityDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciCompute 实例、控制台连接、VNIC、流量。
|
||||||
|
func registerOciCompute(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.GET("/oci-configs/:id/instances", h.listInstances)
|
||||||
|
g.POST("/oci-configs/:id/instances", h.createInstance)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId", h.getInstance)
|
||||||
|
g.PUT("/oci-configs/:id/instances/:instanceId", h.updateInstance)
|
||||||
|
g.DELETE("/oci-configs/:id/instances/:instanceId", h.terminateInstance)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/action", h.instanceAction)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/change-public-ip", h.changePublicIP)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.addInstanceIpv6)
|
||||||
|
g.DELETE("/oci-configs/:id/instances/:instanceId/ipv6-addresses", h.deleteInstanceIpv6)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/console-connections", h.createConsoleConnection)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId/console-connections", h.listConsoleConnections)
|
||||||
|
g.DELETE("/oci-configs/:id/console-connections/:connectionId", h.deleteConsoleConnection)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId/vnics", h.listInstanceVnics)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/vnics", h.attachVnic)
|
||||||
|
g.DELETE("/oci-configs/:id/vnic-attachments/:attachmentId", h.detachVnic)
|
||||||
|
g.POST("/oci-configs/:id/vnics/:vnicId/ipv6-addresses", h.addVnicIpv6)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId/traffic", h.instanceTraffic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciNetwork VCN、子网、安全列表。
|
||||||
|
func registerOciNetwork(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.GET("/oci-configs/:id/vcns", h.listVCNs)
|
||||||
|
g.POST("/oci-configs/:id/vcns", h.createVCN)
|
||||||
|
g.GET("/oci-configs/:id/vcns/:vcnId", h.getVCN)
|
||||||
|
g.PUT("/oci-configs/:id/vcns/:vcnId", h.updateVCN)
|
||||||
|
g.DELETE("/oci-configs/:id/vcns/:vcnId", h.deleteVCN)
|
||||||
|
g.POST("/oci-configs/:id/vcns/:vcnId/enable-ipv6", h.enableVCNIPv6)
|
||||||
|
g.GET("/oci-configs/:id/subnets", h.listSubnets)
|
||||||
|
g.POST("/oci-configs/:id/subnets", h.createSubnet)
|
||||||
|
g.GET("/oci-configs/:id/subnets/:subnetId", h.getSubnet)
|
||||||
|
g.PUT("/oci-configs/:id/subnets/:subnetId", h.updateSubnet)
|
||||||
|
g.DELETE("/oci-configs/:id/subnets/:subnetId", h.deleteSubnet)
|
||||||
|
g.GET("/oci-configs/:id/security-lists", h.listSecurityLists)
|
||||||
|
g.POST("/oci-configs/:id/security-lists", h.createSecurityList)
|
||||||
|
g.GET("/oci-configs/:id/security-lists/:securityListId", h.getSecurityList)
|
||||||
|
g.PUT("/oci-configs/:id/security-lists/:securityListId", h.updateSecurityList)
|
||||||
|
g.DELETE("/oci-configs/:id/security-lists/:securityListId", h.deleteSecurityList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciStorage 引导卷、块存储、附加/替换。
|
||||||
|
func registerOciStorage(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.GET("/oci-configs/:id/boot-volumes", h.listBootVolumes)
|
||||||
|
g.GET("/oci-configs/:id/boot-volumes/:bootVolumeId", h.getBootVolume)
|
||||||
|
g.PUT("/oci-configs/:id/boot-volumes/:bootVolumeId", h.updateBootVolume)
|
||||||
|
g.DELETE("/oci-configs/:id/boot-volumes/:bootVolumeId", h.deleteBootVolume)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.listBootVolumeAttachments)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/boot-volume-attachments", h.attachBootVolume)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/replace-boot-volume", h.replaceBootVolume)
|
||||||
|
g.DELETE("/oci-configs/:id/boot-volume-attachments/:attachmentId", h.detachBootVolume)
|
||||||
|
g.GET("/oci-configs/:id/volumes", h.listBlockVolumes)
|
||||||
|
g.GET("/oci-configs/:id/instances/:instanceId/volume-attachments", h.listVolumeAttachments)
|
||||||
|
g.POST("/oci-configs/:id/instances/:instanceId/volume-attachments", h.attachVolume)
|
||||||
|
g.DELETE("/oci-configs/:id/volume-attachments/:attachmentId", h.detachVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciCost 成本快照。
|
||||||
|
func registerOciCost(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.GET("/oci-configs/:id/costs", h.costs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerOciTenantIAM 租户 IAM 治理:审计、用户、密码策略、身份提供商、单点登录规则。
|
||||||
|
func registerOciTenantIAM(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||||
|
g.GET("/oci-configs/:id/audit-events", h.getAuditEvents)
|
||||||
|
g.GET("/oci-configs/:id/audit-events/detail", h.getAuditEventDetail)
|
||||||
|
g.GET("/oci-configs/:id/users", h.listTenantUsers)
|
||||||
|
g.POST("/oci-configs/:id/users", h.createTenantUser)
|
||||||
|
g.GET("/oci-configs/:id/users/:userId", h.getTenantUserDetail)
|
||||||
|
g.PUT("/oci-configs/:id/users/:userId", h.updateTenantUser)
|
||||||
|
g.DELETE("/oci-configs/:id/users/:userId", h.deleteTenantUser)
|
||||||
|
g.POST("/oci-configs/:id/users/:userId/reset-password", h.resetTenantUserPassword)
|
||||||
|
g.DELETE("/oci-configs/:id/users/:userId/mfa-devices", h.deleteTenantUserMfa)
|
||||||
|
g.DELETE("/oci-configs/:id/users/:userId/api-keys", h.deleteTenantUserApiKeys)
|
||||||
|
g.GET("/oci-configs/:id/notification-recipients", h.getNotificationRecipients)
|
||||||
|
g.PUT("/oci-configs/:id/notification-recipients", h.updateNotificationRecipients)
|
||||||
|
g.GET("/oci-configs/:id/password-policies", h.listPasswordPolicies)
|
||||||
|
g.PUT("/oci-configs/:id/password-policies/:policyId", h.updatePasswordPolicy)
|
||||||
|
g.GET("/oci-configs/:id/identity-settings", h.getIdentitySetting)
|
||||||
|
g.PUT("/oci-configs/:id/identity-settings", h.updateIdentitySetting)
|
||||||
|
g.GET("/oci-configs/:id/identity-providers", h.listIdentityProviders)
|
||||||
|
g.POST("/oci-configs/:id/identity-providers", h.createIdentityProvider)
|
||||||
|
g.POST("/oci-configs/:id/identity-providers/:idpId/activate", h.activateIdentityProvider)
|
||||||
|
g.DELETE("/oci-configs/:id/identity-providers/:idpId", h.deleteIdentityProvider)
|
||||||
|
g.GET("/oci-configs/:id/saml-metadata", h.downloadSamlMetadata)
|
||||||
|
g.GET("/oci-configs/:id/sign-on-rules", h.listSignOnRules)
|
||||||
|
g.POST("/oci-configs/:id/sign-on-exemptions", h.createMfaExemption)
|
||||||
|
g.DELETE("/oci-configs/:id/sign-on-exemptions/:ruleId", h.deleteMfaExemption)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerSettings 挂载 /settings/* 与 /about,以及系统日志与代理管理。
|
||||||
|
func registerSettings(secured *gin.RouterGroup, settings *service.SettingService, notifier *service.Notifier, systemLogs *service.SystemLogService, proxies *service.ProxyService) {
|
||||||
|
secured.GET("/about", about)
|
||||||
|
secured.GET("/system-logs", (&systemLogHandler{svc: systemLogs}).list)
|
||||||
|
|
||||||
|
st := &settingsHandler{svc: settings, notifier: notifier}
|
||||||
|
secured.GET("/settings/telegram", st.getTelegram)
|
||||||
|
secured.PUT("/settings/telegram", st.updateTelegram)
|
||||||
|
secured.POST("/settings/telegram/test", st.testTelegram)
|
||||||
|
secured.GET("/settings/notify-events", st.getNotifyEvents)
|
||||||
|
secured.PUT("/settings/notify-events", st.updateNotifyEvents)
|
||||||
|
secured.GET("/settings/notify-templates", st.listNotifyTemplates)
|
||||||
|
secured.PUT("/settings/notify-templates/:kind", st.updateNotifyTemplate)
|
||||||
|
secured.POST("/settings/notify-templates/:kind/test", st.testNotifyTemplate)
|
||||||
|
secured.GET("/settings/task", st.getTaskSettings)
|
||||||
|
secured.PUT("/settings/task", st.updateTaskSettings)
|
||||||
|
secured.GET("/settings/security", st.getSecurity)
|
||||||
|
secured.PUT("/settings/security", st.updateSecurity)
|
||||||
|
|
||||||
|
px := &proxyHandler{svc: proxies}
|
||||||
|
secured.GET("/proxies", px.list)
|
||||||
|
secured.POST("/proxies", px.create)
|
||||||
|
secured.POST("/proxies/import", px.importBatch)
|
||||||
|
secured.PUT("/proxies/:id", px.update)
|
||||||
|
secured.DELETE("/proxies/:id", px.remove)
|
||||||
|
secured.POST("/proxies/:id/probe", px.probe)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerSwagger 挂载 swagger UI 与 spec(/swagger/index.html)。
|
||||||
|
// 默认关闭,环境变量 SWAGGER=1 时开启——面板管理接口结构不宜默认对外,
|
||||||
|
// 生产按需临时打开;spec 由 `go tool swag init` 生成进 docs/ 包。
|
||||||
|
func registerSwagger(r *gin.Engine) {
|
||||||
|
if os.Getenv("SWAGGER") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerTasksAndLogs 挂载后台任务与 OCI 日志回传相关路由。
|
||||||
|
func registerTasksAndLogs(secured *gin.RouterGroup, tasks *service.TaskService, logEvents *service.LogEventService) {
|
||||||
|
t := &taskHandler{svc: tasks}
|
||||||
|
secured.POST("/tasks", t.create)
|
||||||
|
secured.GET("/tasks", t.list)
|
||||||
|
secured.GET("/tasks/:id", t.get)
|
||||||
|
secured.PUT("/tasks/:id", t.update)
|
||||||
|
secured.DELETE("/tasks/:id", t.remove)
|
||||||
|
secured.GET("/tasks/:id/logs", t.logs)
|
||||||
|
secured.POST("/tasks/:id/run", t.run)
|
||||||
|
|
||||||
|
le := &logEventHandler{svc: logEvents}
|
||||||
|
secured.GET("/log-events", le.list)
|
||||||
|
secured.GET("/oci-configs/:id/log-webhook", le.getWebhook)
|
||||||
|
secured.POST("/oci-configs/:id/log-webhook", le.ensureWebhook)
|
||||||
|
secured.DELETE("/oci-configs/:id/log-webhook", le.revokeWebhook)
|
||||||
|
secured.GET("/oci-configs/:id/log-relay", le.getRelay)
|
||||||
|
secured.POST("/oci-configs/:id/log-relay", le.setupRelay)
|
||||||
|
secured.DELETE("/oci-configs/:id/log-relay", le.teardownRelay)
|
||||||
|
}
|
||||||
@@ -16,6 +16,12 @@ type settingsHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getTelegram 返回脱敏后的 Telegram 配置,绝不回 token 明文。
|
// getTelegram 返回脱敏后的 Telegram 配置,绝不回 token 明文。
|
||||||
|
//
|
||||||
|
// @Summary 返回脱敏后的 Telegram 配置,绝不回 token 明文
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/telegram [get]
|
||||||
func (h *settingsHandler) getTelegram(c *gin.Context) {
|
func (h *settingsHandler) getTelegram(c *gin.Context) {
|
||||||
view, err := h.svc.TelegramView(c.Request.Context())
|
view, err := h.svc.TelegramView(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,6 +40,13 @@ type updateTelegramRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateTelegram 保存配置并返回最新脱敏视图。
|
// updateTelegram 保存配置并返回最新脱敏视图。
|
||||||
|
//
|
||||||
|
// @Summary 保存配置并返回最新脱敏视图
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body updateTelegramRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/telegram [put]
|
||||||
func (h *settingsHandler) updateTelegram(c *gin.Context) {
|
func (h *settingsHandler) updateTelegram(c *gin.Context) {
|
||||||
var req updateTelegramRequest
|
var req updateTelegramRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -53,6 +66,12 @@ func (h *settingsHandler) updateTelegram(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testTelegram 用当前已保存配置同步发送一条测试消息。
|
// testTelegram 用当前已保存配置同步发送一条测试消息。
|
||||||
|
//
|
||||||
|
// @Summary 用当前已保存配置同步发送一条测试消息
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/telegram/test [post]
|
||||||
func (h *settingsHandler) testTelegram(c *gin.Context) {
|
func (h *settingsHandler) testTelegram(c *gin.Context) {
|
||||||
if err := h.notifier.Test(c.Request.Context()); err != nil {
|
if err := h.notifier.Test(c.Request.Context()); err != nil {
|
||||||
respondError(c, err)
|
respondError(c, err)
|
||||||
@@ -62,6 +81,12 @@ func (h *settingsHandler) testTelegram(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getNotifyEvents 返回全部通知事件开关;键缺省(存量部署)视为开启。
|
// getNotifyEvents 返回全部通知事件开关;键缺省(存量部署)视为开启。
|
||||||
|
//
|
||||||
|
// @Summary 返回全部通知事件开关
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/notify-events [get]
|
||||||
func (h *settingsHandler) getNotifyEvents(c *gin.Context) {
|
func (h *settingsHandler) getNotifyEvents(c *gin.Context) {
|
||||||
view, err := h.svc.NotifyEvents(c.Request.Context())
|
view, err := h.svc.NotifyEvents(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,6 +98,13 @@ func (h *settingsHandler) getNotifyEvents(c *gin.Context) {
|
|||||||
|
|
||||||
// updateNotifyEvents 全量保存五个事件开关并返回最新值;
|
// updateNotifyEvents 全量保存五个事件开关并返回最新值;
|
||||||
// 请求体为全量语义,缺字段按 JSON 零值 false 处理。
|
// 请求体为全量语义,缺字段按 JSON 零值 false 处理。
|
||||||
|
//
|
||||||
|
// @Summary 全量保存五个事件开关并返回最新值
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body service.NotifyEventsView true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/notify-events [put]
|
||||||
func (h *settingsHandler) updateNotifyEvents(c *gin.Context) {
|
func (h *settingsHandler) updateNotifyEvents(c *gin.Context) {
|
||||||
var req service.NotifyEventsView
|
var req service.NotifyEventsView
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -87,6 +119,12 @@ func (h *settingsHandler) updateNotifyEvents(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listNotifyTemplates 返回全部通知模板(默认模板 + 自定义现值 + 可用变量)。
|
// listNotifyTemplates 返回全部通知模板(默认模板 + 自定义现值 + 可用变量)。
|
||||||
|
//
|
||||||
|
// @Summary 返回全部通知模板
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/notify-templates [get]
|
||||||
func (h *settingsHandler) listNotifyTemplates(c *gin.Context) {
|
func (h *settingsHandler) listNotifyTemplates(c *gin.Context) {
|
||||||
items, err := h.svc.NotifyTemplates(c.Request.Context())
|
items, err := h.svc.NotifyTemplates(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,6 +135,14 @@ func (h *settingsHandler) listNotifyTemplates(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateNotifyTemplate 保存单个通知模板;空模板清除自定义(恢复默认)。
|
// updateNotifyTemplate 保存单个通知模板;空模板清除自定义(恢复默认)。
|
||||||
|
//
|
||||||
|
// @Summary 保存单个通知模板
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param kind path string true "kind"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/notify-templates/{kind} [put]
|
||||||
func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) {
|
func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
@@ -114,6 +160,14 @@ func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) {
|
|||||||
|
|
||||||
// testNotifyTemplate 用示例变量渲染模板并同步发送测试消息;
|
// testNotifyTemplate 用示例变量渲染模板并同步发送测试消息;
|
||||||
// template 非空时按编辑中内容渲染(不落库)。
|
// template 非空时按编辑中内容渲染(不落库)。
|
||||||
|
//
|
||||||
|
// @Summary 用示例变量渲染模板并同步发送测试消息
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param kind path string true "kind"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/notify-templates/{kind}/test [post]
|
||||||
func (h *settingsHandler) testNotifyTemplate(c *gin.Context) {
|
func (h *settingsHandler) testNotifyTemplate(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
@@ -130,6 +184,12 @@ func (h *settingsHandler) testNotifyTemplate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getTaskSettings 返回任务行为设置(抢机熔断阈值),阈值未保存过时为默认值。
|
// getTaskSettings 返回任务行为设置(抢机熔断阈值),阈值未保存过时为默认值。
|
||||||
|
//
|
||||||
|
// @Summary 返回任务行为设置
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/task [get]
|
||||||
func (h *settingsHandler) getTaskSettings(c *gin.Context) {
|
func (h *settingsHandler) getTaskSettings(c *gin.Context) {
|
||||||
view, err := h.svc.TaskSettings(c.Request.Context())
|
view, err := h.svc.TaskSettings(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,6 +200,13 @@ func (h *settingsHandler) getTaskSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateTaskSettings 保存任务行为设置并返回最新值;阈值越界返回 400。
|
// updateTaskSettings 保存任务行为设置并返回最新值;阈值越界返回 400。
|
||||||
|
//
|
||||||
|
// @Summary 保存任务行为设置并返回最新值
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body service.TaskSettingsView true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/task [put]
|
||||||
func (h *settingsHandler) updateTaskSettings(c *gin.Context) {
|
func (h *settingsHandler) updateTaskSettings(c *gin.Context) {
|
||||||
var req service.TaskSettingsView
|
var req service.TaskSettingsView
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -158,6 +225,12 @@ func (h *settingsHandler) updateTaskSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getSecurity 返回安全设置(WAF 参数 / 真实IP请求头 / 面板地址),未保存过时为默认值。
|
// getSecurity 返回安全设置(WAF 参数 / 真实IP请求头 / 面板地址),未保存过时为默认值。
|
||||||
|
//
|
||||||
|
// @Summary 返回安全设置
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/security [get]
|
||||||
func (h *settingsHandler) getSecurity(c *gin.Context) {
|
func (h *settingsHandler) getSecurity(c *gin.Context) {
|
||||||
view, err := h.svc.Security(c.Request.Context())
|
view, err := h.svc.Security(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,6 +241,13 @@ func (h *settingsHandler) getSecurity(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateSecurity 保存安全设置并返回最新值;越界或非法返回 400,保存后立即生效。
|
// updateSecurity 保存安全设置并返回最新值;越界或非法返回 400,保存后立即生效。
|
||||||
|
//
|
||||||
|
// @Summary 保存安全设置并返回最新值
|
||||||
|
// @Tags 设置
|
||||||
|
// @Param body body service.SecuritySettings true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/settings/security [put]
|
||||||
func (h *settingsHandler) updateSecurity(c *gin.Context) {
|
func (h *settingsHandler) updateSecurity(c *gin.Context) {
|
||||||
var req service.SecuritySettings
|
var req service.SecuritySettings
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"oci-portal/internal/webui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerWebUI 挂载嵌入的前端产物。dev 模式走 vite devServer,不经此路径。
|
||||||
|
func registerWebUI(r *gin.Engine) error {
|
||||||
|
sub, err := webui.Dist()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mountSPA(r, sub)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountSPA 注册 NoRoute 兜底:命中真实文件直出,API 前缀保持 JSON 404,
|
||||||
|
// 其余路径回 index.html 交给 SPA 路由。
|
||||||
|
func mountSPA(r *gin.Engine, sub fs.FS) {
|
||||||
|
fileServer := http.FileServer(http.FS(sub))
|
||||||
|
r.NoRoute(func(c *gin.Context) {
|
||||||
|
p := c.Request.URL.Path
|
||||||
|
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ai/") {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name := strings.TrimPrefix(path.Clean(p), "/"); name != "" {
|
||||||
|
if _, err := fs.Stat(sub, name); err == nil {
|
||||||
|
setStaticCacheHeader(c, p)
|
||||||
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SPA 路由统一回 index.html,禁缓存保证发版后入口即时更新
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Request.URL.Path = "/"
|
||||||
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// setStaticCacheHeader 按路径设缓存策略:vite 产物带内容 hash,/assets/* 可永久缓存;
|
||||||
|
// 其余真实文件(favicon 等)不带 hash,禁强缓存。
|
||||||
|
func setStaticCacheHeader(c *gin.Context, p string) {
|
||||||
|
if strings.HasPrefix(p, "/assets/") {
|
||||||
|
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMountSPA(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
dist := fstest.MapFS{
|
||||||
|
"index.html": {Data: []byte("<html>spa-entry</html>")},
|
||||||
|
"favicon.svg": {Data: []byte("<svg/>")},
|
||||||
|
"assets/app.js": {Data: []byte("console.log(1)")},
|
||||||
|
}
|
||||||
|
r := gin.New()
|
||||||
|
mountSPA(r, dist)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
wantStatus int
|
||||||
|
wantBody string // 包含匹配;空跳过
|
||||||
|
wantCache string // Cache-Control 精确匹配;空跳过
|
||||||
|
wantJSON bool
|
||||||
|
}{
|
||||||
|
{name: "api 未知路径保持 JSON 404", path: "/api/v1/nope", wantStatus: 404, wantBody: "not found", wantJSON: true},
|
||||||
|
{name: "ai 未知路径保持 JSON 404", path: "/ai/v1/nope", wantStatus: 404, wantBody: "not found", wantJSON: true},
|
||||||
|
{name: "根路径出 index 且禁缓存", path: "/", wantStatus: 200, wantBody: "spa-entry", wantCache: "no-cache"},
|
||||||
|
{name: "SPA 路由 fallback 到 index", path: "/ai-gateway/keys", wantStatus: 200, wantBody: "spa-entry", wantCache: "no-cache"},
|
||||||
|
{name: "assets 命中带 immutable 长缓存", path: "/assets/app.js", wantStatus: 200, wantBody: "console.log", wantCache: "public, max-age=31536000, immutable"},
|
||||||
|
{name: "非 assets 真实文件禁强缓存", path: "/favicon.svg", wantStatus: 200, wantCache: "no-cache"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != tt.wantStatus {
|
||||||
|
t.Fatalf("status = %d, want %d", w.Code, tt.wantStatus)
|
||||||
|
}
|
||||||
|
if tt.wantBody != "" && !strings.Contains(w.Body.String(), tt.wantBody) {
|
||||||
|
t.Fatalf("body %q does not contain %q", w.Body.String(), tt.wantBody)
|
||||||
|
}
|
||||||
|
if tt.wantCache != "" && w.Header().Get("Cache-Control") != tt.wantCache {
|
||||||
|
t.Fatalf("cache-control = %q, want %q", w.Header().Get("Cache-Control"), tt.wantCache)
|
||||||
|
}
|
||||||
|
if tt.wantJSON && !strings.HasPrefix(w.Header().Get("Content-Type"), "application/json") {
|
||||||
|
t.Fatalf("content-type = %q, want json", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterWebUIPlaceholder 验证嵌入占位产物可挂载(裸 clone 后 go build/test 即过)。
|
||||||
|
func TestRegisterWebUIPlaceholder(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
if err := registerWebUI(r); err != nil {
|
||||||
|
t.Fatalf("registerWebUI: %v", err)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,12 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @Summary 订阅列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subscriptions [get]
|
||||||
func (h *ociConfigHandler) subscriptions(c *gin.Context) {
|
func (h *ociConfigHandler) subscriptions(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -19,6 +25,13 @@ func (h *ociConfigHandler) subscriptions(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, subs)
|
c.JSON(http.StatusOK, subs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 订阅详情
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param subscriptionId path string true "subscriptionId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/subscriptions/{subscriptionId} [get]
|
||||||
func (h *ociConfigHandler) subscriptionDetail(c *gin.Context) {
|
func (h *ociConfigHandler) subscriptionDetail(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -32,6 +45,12 @@ func (h *ociConfigHandler) subscriptionDetail(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, detail)
|
c.JSON(http.StatusOK, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 限额服务列表
|
||||||
|
// @Tags 租户配置
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/limits/services [get]
|
||||||
func (h *ociConfigHandler) limitServices(c *gin.Context) {
|
func (h *ociConfigHandler) limitServices(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ type systemLogHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list 分页倒序返回系统日志,支持路径 / 用户名关键字模糊过滤。
|
// list 分页倒序返回系统日志,支持路径 / 用户名关键字模糊过滤。
|
||||||
|
//
|
||||||
|
// @Summary 系统操作日志列表
|
||||||
|
// @Tags 设置
|
||||||
|
// @Success 200 {object} map[string]any "items 与 total"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/system-logs [get]
|
||||||
func (h *systemLogHandler) list(c *gin.Context) {
|
func (h *systemLogHandler) list(c *gin.Context) {
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ type updateTaskRequest struct {
|
|||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建任务
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param body body createTaskRequest true "任务定义(类型/cron/payload)"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks [post]
|
||||||
func (h *taskHandler) create(c *gin.Context) {
|
func (h *taskHandler) create(c *gin.Context) {
|
||||||
var req createTaskRequest
|
var req createTaskRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -48,6 +54,11 @@ func (h *taskHandler) create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, task)
|
c.JSON(http.StatusCreated, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 任务列表
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Success 200 {array} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks [get]
|
||||||
func (h *taskHandler) list(c *gin.Context) {
|
func (h *taskHandler) list(c *gin.Context) {
|
||||||
tasks, err := h.svc.ListTasks(c.Request.Context())
|
tasks, err := h.svc.ListTasks(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,6 +68,12 @@ func (h *taskHandler) list(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, tasks)
|
c.JSON(http.StatusOK, tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 任务详情
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "任务 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks/{id} [get]
|
||||||
func (h *taskHandler) get(c *gin.Context) {
|
func (h *taskHandler) get(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -70,6 +87,13 @@ func (h *taskHandler) get(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, task)
|
c.JSON(http.StatusOK, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新任务
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "任务 ID"
|
||||||
|
// @Param body body updateTaskRequest true "可更新字段"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks/{id} [put]
|
||||||
func (h *taskHandler) update(c *gin.Context) {
|
func (h *taskHandler) update(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -93,6 +117,12 @@ func (h *taskHandler) update(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, task)
|
c.JSON(http.StatusOK, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除任务
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "任务 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks/{id} [delete]
|
||||||
func (h *taskHandler) remove(c *gin.Context) {
|
func (h *taskHandler) remove(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -105,6 +135,12 @@ func (h *taskHandler) remove(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 任务执行日志
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks/{id}/logs [get]
|
||||||
func (h *taskHandler) logs(c *gin.Context) {
|
func (h *taskHandler) logs(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -119,6 +155,12 @@ func (h *taskHandler) logs(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, logs)
|
c.JSON(http.StatusOK, logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 立即执行任务
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/tasks/{id}/run [post]
|
||||||
func (h *taskHandler) run(c *gin.Context) {
|
func (h *taskHandler) run(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import (
|
|||||||
|
|
||||||
// ---- 流量与成本 ----
|
// ---- 流量与成本 ----
|
||||||
|
|
||||||
|
// @Summary ---- 流量与成本 ----
|
||||||
|
// @Tags 计算
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "instanceId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/traffic [get]
|
||||||
func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -28,6 +35,12 @@ func (h *ociConfigHandler) instanceTraffic(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, traffic)
|
c.JSON(http.StatusOK, traffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 配置成本快照
|
||||||
|
// @Tags 成本
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/costs [get]
|
||||||
func (h *ociConfigHandler) costs(c *gin.Context) {
|
func (h *ociConfigHandler) costs(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -56,6 +69,13 @@ func (h *ociConfigHandler) costs(c *gin.Context) {
|
|||||||
|
|
||||||
// getAuditEvents 实时查询租户 OCI 审计事件:hours 首查(缺省 24);
|
// getAuditEvents 实时查询租户 OCI 审计事件:hours 首查(缺省 24);
|
||||||
// start/end/page 为截断后的同窗续查;窗口越界或非法返回 400。
|
// start/end/page 为截断后的同窗续查;窗口越界或非法返回 400。
|
||||||
|
//
|
||||||
|
// @Summary 实时查询租户 OCI 审计事件:hours 首查
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/audit-events [get]
|
||||||
func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -80,6 +100,13 @@ func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
|||||||
|
|
||||||
// getAuditEventDetail 按 eventId 取回原始事件 JSON:缓存命中秒开,
|
// getAuditEventDetail 按 eventId 取回原始事件 JSON:缓存命中秒开,
|
||||||
// 过期走事件时间小窗重查;仍不可得返回 404,前端降级展示精简字段。
|
// 过期走事件时间小窗重查;仍不可得返回 404,前端降级展示精简字段。
|
||||||
|
//
|
||||||
|
// @Summary 按 eventId 取回原始事件 JSON:缓存命中秒开,
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/audit-events/detail [get]
|
||||||
func (h *ociConfigHandler) getAuditEventDetail(c *gin.Context) {
|
func (h *ociConfigHandler) getAuditEventDetail(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -116,6 +143,12 @@ type createTenantUserRequest struct {
|
|||||||
AddToAdminGroup bool `json:"addToAdminGroup"`
|
AddToAdminGroup bool `json:"addToAdminGroup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 租户 IAM 用户列表
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users [get]
|
||||||
func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -129,6 +162,13 @@ func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, users)
|
c.JSON(http.StatusOK, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 租户用户详情
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId} [get]
|
||||||
func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -142,6 +182,13 @@ func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, detail)
|
c.JSON(http.StatusOK, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 创建租户 IAM 用户
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body createTenantUserRequest true "请求体"
|
||||||
|
// @Success 201 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users [post]
|
||||||
func (h *ociConfigHandler) createTenantUser(c *gin.Context) {
|
func (h *ociConfigHandler) createTenantUser(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -179,6 +226,14 @@ type updateTenantUserRequest struct {
|
|||||||
AddToAdminGroup *bool `json:"addToAdminGroup"`
|
AddToAdminGroup *bool `json:"addToAdminGroup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新租户 IAM 用户
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Param body body updateTenantUserRequest true "请求体"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId} [put]
|
||||||
func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -204,6 +259,13 @@ func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, user)
|
c.JSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 删除租户 IAM 用户
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId} [delete]
|
||||||
func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -216,6 +278,13 @@ func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 重置租户用户密码
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId}/reset-password [post]
|
||||||
func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -229,6 +298,13 @@ func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"password": password})
|
c.JSON(http.StatusOK, gin.H{"password": password})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 清除用户 MFA 设备
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId}/mfa-devices [delete]
|
||||||
func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -242,6 +318,13 @@ func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"deletedTotpDevices": deleted})
|
c.JSON(http.StatusOK, gin.H{"deletedTotpDevices": deleted})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 清除用户全部 API Key
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param userId path string true "userId"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/users/{userId}/api-keys [delete]
|
||||||
func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -258,6 +341,12 @@ func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
|||||||
|
|
||||||
// ---- 域通知收件人与密码策略 ----
|
// ---- 域通知收件人与密码策略 ----
|
||||||
|
|
||||||
|
// @Summary ---- 域通知收件人与密码策略 ----
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/notification-recipients [get]
|
||||||
func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -271,6 +360,13 @@ func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, recipients)
|
c.JSON(http.StatusOK, recipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新通知收件人
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/notification-recipients [put]
|
||||||
func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -291,6 +387,12 @@ func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, recipients)
|
c.JSON(http.StatusOK, recipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 密码策略列表
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/password-policies [get]
|
||||||
func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -304,6 +406,14 @@ func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, policies)
|
c.JSON(http.StatusOK, policies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新密码策略
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param policyId path string true "policyId"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/password-policies/{policyId} [put]
|
||||||
func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -330,6 +440,12 @@ func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, policy)
|
c.JSON(http.StatusOK, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 身份域设置
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-settings [get]
|
||||||
func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -343,6 +459,13 @@ func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, setting)
|
c.JSON(http.StatusOK, setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary 更新身份域设置
|
||||||
|
// @Tags 租户 IAM
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param body body object true "请求体(见接口说明)"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/identity-settings [put]
|
||||||
func (h *ociConfigHandler) updateIdentitySetting(c *gin.Context) {
|
func (h *ociConfigHandler) updateIdentitySetting(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ type createConsoleSessionRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create 创建控制台会话(内部清理残留连接并创建 OCI 控制台连接,约 30-90 秒)。
|
// create 创建控制台会话(内部清理残留连接并创建 OCI 控制台连接,约 30-90 秒)。
|
||||||
|
//
|
||||||
|
// @Summary 创建网页控制台会话
|
||||||
|
// @Tags 网页控制台
|
||||||
|
// @Param id path int true "配置 ID"
|
||||||
|
// @Param instanceId path string true "实例 OCID"
|
||||||
|
// @Param body body object true "{type: serial|vnc}"
|
||||||
|
// @Success 200 {object} map[string]any "sessionId 与 wsPath"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/oci-configs/{id}/instances/{instanceId}/console-sessions [post]
|
||||||
func (h *consoleHandler) create(c *gin.Context) {
|
func (h *consoleHandler) create(c *gin.Context) {
|
||||||
id, ok := pathID(c)
|
id, ok := pathID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -42,6 +51,13 @@ func (h *consoleHandler) create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove 结束会话并删除云端控制台连接。
|
// remove 结束会话并删除云端控制台连接。
|
||||||
|
//
|
||||||
|
// @Summary 关闭网页控制台会话
|
||||||
|
// @Tags 网页控制台
|
||||||
|
// @Param sessionId path string true "会话 ID"
|
||||||
|
// @Success 204 "无内容"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/console-sessions/{sessionId} [delete]
|
||||||
func (h *consoleHandler) remove(c *gin.Context) {
|
func (h *consoleHandler) remove(c *gin.Context) {
|
||||||
h.svc.CloseSession(c.Param("sessionId"))
|
h.svc.CloseSession(c.Param("sessionId"))
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
@@ -55,6 +71,13 @@ var wsUpgrader = websocket.Upgrader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ws 是控制台数据面:校验 token 后建立两跳 SSH 隧道,按会话类型分派数据泵。
|
// ws 是控制台数据面:校验 token 后建立两跳 SSH 隧道,按会话类型分派数据泵。
|
||||||
|
//
|
||||||
|
// @Summary 控制台 WebSocket 数据面
|
||||||
|
// @Tags 网页控制台
|
||||||
|
// @Param sessionId path string true "会话 ID"
|
||||||
|
// @Param token query string true "JWT(浏览器 WebSocket 无法带头,经 query 传递)"
|
||||||
|
// @Success 101 "升级为 WebSocket"
|
||||||
|
// @Router /api/v1/console-sessions/{sessionId}/ws [get]
|
||||||
func (h *consoleHandler) ws(c *gin.Context) {
|
func (h *consoleHandler) ws(c *gin.Context) {
|
||||||
if _, err := h.auth.ParseToken(c.Query("token")); err != nil {
|
if _, err := h.auth.ParseToken(c.Query("token")); err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,10 +16,12 @@ import (
|
|||||||
|
|
||||||
// ONS 消息头(方案A §2.2/§2.3,官方 X-OCI-NS-* 字段)。
|
// ONS 消息头(方案A §2.2/§2.3,官方 X-OCI-NS-* 字段)。
|
||||||
const (
|
const (
|
||||||
onsHeaderMessageType = "X-OCI-NS-MessageType"
|
onsHeaderMessageType = "X-OCI-NS-MessageType"
|
||||||
onsHeaderMessageID = "X-OCI-NS-MessageId"
|
onsHeaderMessageID = "X-OCI-NS-MessageId"
|
||||||
onsHeaderTimestamp = "X-OCI-NS-Timestamp"
|
onsHeaderTimestamp = "X-OCI-NS-Timestamp"
|
||||||
onsHeaderConfirmURL = "X-OCI-NS-ConfirmationURL"
|
onsHeaderConfirmURL = "X-OCI-NS-ConfirmationURL"
|
||||||
|
onsHeaderSigningCertURL = "X-OCI-NS-SigningCertURL"
|
||||||
|
onsHeaderSignature = "X-OCI-NS-Signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// webhook 同步路径的防御参数:body 上限高于 ONS 链路 128KB 留余量,
|
// webhook 同步路径的防御参数:body 上限高于 ONS 链路 128KB 留余量,
|
||||||
@@ -44,6 +47,15 @@ type webhookHandler struct {
|
|||||||
|
|
||||||
// handle 是 webhook 入口:secret 反查租户后按消息类型分派;
|
// handle 是 webhook 入口:secret 反查租户后按消息类型分派;
|
||||||
// 同步路径只做 secret→分派→读body→幂等入库,5 秒投递预算内返回(方案A §2.4)。
|
// 同步路径只做 secret→分派→读body→幂等入库,5 秒投递预算内返回(方案A §2.4)。
|
||||||
|
//
|
||||||
|
// @Summary OCI 日志回传入口(ONS HTTPS 订阅投递)
|
||||||
|
// @Tags 任务与日志回传
|
||||||
|
// @Param secret path string true "面板生成的回传密钥(鉴权凭据)"
|
||||||
|
// @Param body body object true "ONS 消息原文(SubscriptionConfirmation / Notification)"
|
||||||
|
// @Success 200 "已受理"
|
||||||
|
// @Failure 403 {object} map[string]string "时间戳超窗或证书源非 Oracle 域"
|
||||||
|
// @Failure 404 "secret 无效(不暴露端点存在性)"
|
||||||
|
// @Router /api/v1/webhooks/oci-logs/{secret} [post]
|
||||||
func (h *webhookHandler) handle(c *gin.Context) {
|
func (h *webhookHandler) handle(c *gin.Context) {
|
||||||
cfgID, ok := h.events.ResolveSecret(c.Request.Context(), c.Param("secret"))
|
cfgID, ok := h.events.ResolveSecret(c.Request.Context(), c.Param("secret"))
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -141,8 +153,25 @@ func parseOnsTime(value string) (time.Time, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verifySignature 校验 ONS 消息签名。
|
// verifySignature 校验 ONS 消息签名。
|
||||||
// TODO(方案A §2.3 spike):官方未公开签名 canonical form,待 test-server 抓取
|
//
|
||||||
// 真实消息确认输入格式后实现;当前按方案约定以 secret+时间戳+幂等先行。
|
// 现状:官方未公开 canonical form。归档任务 07-06-log-relay-plan-a 与 07-07-relay-impl
|
||||||
func verifySignature(http.Header) error {
|
// 用真实抓包样本(body+签名+证书公钥)对 15 种常见拼接 × 2 种 PSS salt 长度全部未命中,
|
||||||
|
// 逆向成本高。P0 决策以「secret 路径段 + HTTPS + 时间戳偏差 ≤5min + MessageId 幂等」上线。
|
||||||
|
//
|
||||||
|
// 本函数做能做的**证书源头硬化**:即便未来接入 canonical form + 公钥验签,
|
||||||
|
// SigningCertURL 若允许任意域名,攻击者可自签证书构造 body+签名+cert URL 三元组绕过
|
||||||
|
// (AWS SNS 曾出现同类 GHSA-8jgf-23q5-x7xx)。这里预先把 cert URL 域名锁到 Oracle 云,
|
||||||
|
// 让未来加签名校验成为「相加」而非「返修」。
|
||||||
|
func verifySignature(h http.Header) error {
|
||||||
|
certURL := h.Get(onsHeaderSigningCertURL)
|
||||||
|
if certURL == "" {
|
||||||
|
return nil // 头缺失回退到 secret+时间戳+幂等三重防护,不误杀历史 layout
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(certURL)
|
||||||
|
if err != nil || parsed.Scheme != "https" || !service.IsOracleHost(parsed.Host) {
|
||||||
|
return errors.New("ons: signing cert url not in oracle domain")
|
||||||
|
}
|
||||||
|
// TODO(canonical form 公开后):按 SigningCertURL 拉证书(含 host 复核 + 缓存),
|
||||||
|
// 用公钥对 signing string 做 SHA256withRSA/PSS 验签,失败返回 error。
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,36 @@ func TestWebhookDispatch(t *testing.T) {
|
|||||||
headers: map[string]string{onsHeaderTimestamp: now},
|
headers: map[string]string{onsHeaderTimestamp: now},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "SigningCertURL 非 Oracle 域名拒绝", secret: "",
|
||||||
|
headers: map[string]string{
|
||||||
|
onsHeaderMessageID: "m-evil-cert",
|
||||||
|
onsHeaderTimestamp: now,
|
||||||
|
onsHeaderSigningCertURL: "https://evil.example.com/cert.crt",
|
||||||
|
},
|
||||||
|
body: `{"eventType":"t"}`,
|
||||||
|
wantStatus: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SigningCertURL 非 https 拒绝", secret: "",
|
||||||
|
headers: map[string]string{
|
||||||
|
onsHeaderMessageID: "m-http-cert",
|
||||||
|
onsHeaderTimestamp: now,
|
||||||
|
onsHeaderSigningCertURL: "http://objectstorage.eu-paris-1.oraclecloud.com/cert.crt",
|
||||||
|
},
|
||||||
|
body: `{"eventType":"t"}`,
|
||||||
|
wantStatus: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SigningCertURL 白名单内放行(canonical form 未接入,不做签名校验)", secret: "",
|
||||||
|
headers: map[string]string{
|
||||||
|
onsHeaderMessageID: "m-good-cert",
|
||||||
|
onsHeaderTimestamp: now,
|
||||||
|
onsHeaderSigningCertURL: "https://objectstorage.eu-paris-1.oraclecloud.com/p/x/n/bmc-ons-prod/b/ons-ds/o/sig.crt",
|
||||||
|
},
|
||||||
|
body: `{"eventType":"t"}`,
|
||||||
|
wantStatus: http.StatusOK, wantIngest: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "缺时间戳头放行入库", secret: "",
|
name: "缺时间戳头放行入库", secret: "",
|
||||||
headers: map[string]string{onsHeaderMessageID: "m2"},
|
headers: map[string]string{onsHeaderMessageID: "m2"},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
// Config 保存进程运行所需的全部配置。
|
// Config 保存进程运行所需的全部配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Addr string // HTTP 监听地址
|
Addr string // HTTP 监听地址
|
||||||
|
DBDriver string // 数据库驱动:sqlite(默认)/ mysql / postgres
|
||||||
|
DBDSN string // mysql / postgres 的连接串;sqlite 不使用
|
||||||
DBPath string // SQLite 文件路径
|
DBPath string // SQLite 文件路径
|
||||||
DataKey string // 敏感字段加密主密钥
|
DataKey string // 敏感字段加密主密钥
|
||||||
JWTSecret string // JWT 签名密钥
|
JWTSecret string // JWT 签名密钥
|
||||||
@@ -26,8 +28,14 @@ func Load() (*Config, error) {
|
|||||||
if jwtSecret == "" {
|
if jwtSecret == "" {
|
||||||
return nil, fmt.Errorf("load config: JWT_SECRET is required")
|
return nil, fmt.Errorf("load config: JWT_SECRET is required")
|
||||||
}
|
}
|
||||||
|
driver, dsn, err := dbFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &Config{
|
return &Config{
|
||||||
Addr: envOr("ADDR", ":8080"),
|
Addr: envOr("ADDR", ":8080"),
|
||||||
|
DBDriver: driver,
|
||||||
|
DBDSN: dsn,
|
||||||
DBPath: envOr("DB_PATH", "oci-portal.db"),
|
DBPath: envOr("DB_PATH", "oci-portal.db"),
|
||||||
DataKey: dataKey,
|
DataKey: dataKey,
|
||||||
JWTSecret: jwtSecret,
|
JWTSecret: jwtSecret,
|
||||||
@@ -37,6 +45,22 @@ func Load() (*Config, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dbFromEnv 读取并校验数据库驱动配置;外部库缺 DSN 视为配置错误,启动即失败。
|
||||||
|
func dbFromEnv() (driver, dsn string, err error) {
|
||||||
|
driver = envOr("DB_DRIVER", "sqlite")
|
||||||
|
dsn = os.Getenv("DB_DSN")
|
||||||
|
switch driver {
|
||||||
|
case "sqlite":
|
||||||
|
case "mysql", "postgres":
|
||||||
|
if dsn == "" {
|
||||||
|
return "", "", fmt.Errorf("load config: DB_DSN is required when DB_DRIVER=%s", driver)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("load config: unsupported DB_DRIVER %q (sqlite/mysql/postgres)", driver)
|
||||||
|
}
|
||||||
|
return driver, dsn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func envOr(name, def string) string {
|
func envOr(name, def string) string {
|
||||||
if v := os.Getenv(name); v != "" {
|
if v := os.Getenv(name); v != "" {
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDBFromEnv(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
driver string
|
||||||
|
dsn string
|
||||||
|
wantDriver string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "缺省走 sqlite", wantDriver: "sqlite"},
|
||||||
|
{name: "postgres 带 DSN", driver: "postgres", dsn: "host=x", wantDriver: "postgres"},
|
||||||
|
{name: "mysql 缺 DSN 报错", driver: "mysql", wantErr: "DB_DSN is required"},
|
||||||
|
{name: "postgres 缺 DSN 报错", driver: "postgres", wantErr: "DB_DSN is required"},
|
||||||
|
{name: "非法驱动报错", driver: "oracle", wantErr: "unsupported DB_DRIVER"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Setenv("DB_DRIVER", tt.driver)
|
||||||
|
t.Setenv("DB_DSN", tt.dsn)
|
||||||
|
driver, dsn, err := dbFromEnv()
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("err = %v, want contains %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dbFromEnv: %v", err)
|
||||||
|
}
|
||||||
|
if driver != tt.wantDriver || dsn != tt.dsn {
|
||||||
|
t.Fatalf("got (%q,%q), want (%q,%q)", driver, dsn, tt.wantDriver, tt.dsn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,29 +4,56 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
"oci-portal/internal/model"
|
"oci-portal/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open 打开 SQLite 数据库并自动迁移全部模型。
|
// Open 按驱动打开数据库并自动迁移全部模型。
|
||||||
func Open(path string) (*gorm.DB, error) {
|
// driver 取值 sqlite(默认)/mysql/postgres;sqlite 用 path,其余用 dsn。
|
||||||
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
|
func Open(driver, dsn, path string) (*gorm.DB, error) {
|
||||||
|
dialector, err := buildDialector(driver, dsn, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db, err := gorm.Open(dialector, &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Warn),
|
Logger: logger.Default.LogMode(logger.Warn),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open sqlite %s: %w", path, err)
|
return nil, fmt.Errorf("open %s database: %w", dialector.Name(), err)
|
||||||
}
|
}
|
||||||
if err := db.AutoMigrate(
|
if err := autoMigrate(db); err != nil {
|
||||||
|
return nil, fmt.Errorf("auto migrate: %w", err)
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDialector 把驱动名映射为 GORM dialector;三个驱动均为纯 Go,不引入 cgo。
|
||||||
|
// MySQL 设 DefaultStringSize=512:未标 size 的 string 建为 varchar(512),
|
||||||
|
// 避免 OCID / URL 类字段被默认 varchar(191) 截断(SQLite/PG 的 string 天然无长度上限)。
|
||||||
|
func buildDialector(driver, dsn, path string) (gorm.Dialector, error) {
|
||||||
|
switch driver {
|
||||||
|
case "", "sqlite":
|
||||||
|
return sqlite.Open(path), nil
|
||||||
|
case "mysql":
|
||||||
|
return mysql.New(mysql.Config{DSN: dsn, DefaultStringSize: 512}), nil
|
||||||
|
case "postgres":
|
||||||
|
return postgres.Open(dsn), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported DB_DRIVER %q (sqlite/mysql/postgres)", driver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoMigrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
&model.User{}, &model.UserIdentity{}, &model.OciConfig{}, &model.Task{}, &model.TaskLog{},
|
&model.User{}, &model.UserIdentity{}, &model.OciConfig{}, &model.Task{}, &model.TaskLog{},
|
||||||
&model.CheckSnapshot{}, &model.CostSnapshot{},
|
&model.CheckSnapshot{}, &model.CostSnapshot{},
|
||||||
&model.RegionCache{}, &model.CompartmentCache{}, &model.Setting{},
|
&model.RegionCache{}, &model.CompartmentCache{}, &model.Setting{},
|
||||||
&model.SystemLog{}, &model.LogEvent{}, &model.Proxy{},
|
&model.SystemLog{}, &model.LogEvent{}, &model.Proxy{},
|
||||||
&model.AiKey{}, &model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{},
|
&model.AiKey{}, &model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{},
|
||||||
&model.AiContentLog{},
|
&model.AiContentLog{},
|
||||||
); err != nil {
|
)
|
||||||
return nil, fmt.Errorf("auto migrate: %w", err)
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildDialector(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
driver string
|
||||||
|
wantName string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "默认空驱动走 sqlite", driver: "", wantName: "sqlite"},
|
||||||
|
{name: "显式 sqlite", driver: "sqlite", wantName: "sqlite"},
|
||||||
|
{name: "mysql", driver: "mysql", wantName: "mysql"},
|
||||||
|
{name: "postgres", driver: "postgres", wantName: "postgres"},
|
||||||
|
{name: "未知驱动报错", driver: "oracle", wantErr: "unsupported DB_DRIVER"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
d, err := buildDialector(tt.driver, "user:pass@tcp(127.0.0.1)/db", "test.db")
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("err = %v, want contains %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildDialector: %v", err)
|
||||||
|
}
|
||||||
|
if got := d.Name(); got != tt.wantName {
|
||||||
|
t.Fatalf("dialector name = %q, want %q", got, tt.wantName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenSQLiteMigrates(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "test.db")
|
||||||
|
db, err := Open("sqlite", "", path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
for _, table := range []string{"users", "oci_configs", "ai_channels", "log_events"} {
|
||||||
|
if !db.Migrator().HasTable(table) {
|
||||||
|
t.Fatalf("table %s not migrated", table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ const (
|
|||||||
// Setting 是系统级键值配置;敏感值(如 telegram_bot_token)以 AES-GCM 密文存储。
|
// Setting 是系统级键值配置;敏感值(如 telegram_bot_token)以 AES-GCM 密文存储。
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Key string `gorm:"primaryKey;size:64" json:"key"`
|
Key string `gorm:"primaryKey;size:64" json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `gorm:"type:text" json:"value"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,10 +106,10 @@ type Task struct {
|
|||||||
Name string `gorm:"size:128" json:"name"`
|
Name string `gorm:"size:128" json:"name"`
|
||||||
Type string `gorm:"size:32;index" json:"type"`
|
Type string `gorm:"size:32;index" json:"type"`
|
||||||
CronExpr string `gorm:"size:64" json:"cronExpr"`
|
CronExpr string `gorm:"size:64" json:"cronExpr"`
|
||||||
Payload string `json:"payload"`
|
Payload string `gorm:"type:text" json:"payload"`
|
||||||
Status string `gorm:"size:16;index" json:"status"`
|
Status string `gorm:"size:16;index" json:"status"`
|
||||||
LastRunAt *time.Time `json:"lastRunAt"`
|
LastRunAt *time.Time `json:"lastRunAt"`
|
||||||
LastError string `json:"lastError"`
|
LastError string `gorm:"type:text" json:"lastError"`
|
||||||
RunCount int `json:"runCount"`
|
RunCount int `json:"runCount"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
@@ -120,7 +120,7 @@ type TaskLog struct {
|
|||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
TaskID uint `gorm:"index" json:"taskId"`
|
TaskID uint `gorm:"index" json:"taskId"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `gorm:"type:text" json:"message"`
|
||||||
DurationMs int64 `json:"durationMs"`
|
DurationMs int64 `json:"durationMs"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
@@ -162,8 +162,9 @@ type OciConfig struct {
|
|||||||
Fingerprint string `json:"fingerprint"`
|
Fingerprint string `json:"fingerprint"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
|
|
||||||
PrivateKeyEnc string `json:"-"`
|
// 私钥 PEM 密文约 4KB,必须 text;passphrase 密文同档处理
|
||||||
PassphraseEnc string `json:"-"`
|
PrivateKeyEnc string `gorm:"type:text" json:"-"`
|
||||||
|
PassphraseEnc string `gorm:"type:text" json:"-"`
|
||||||
|
|
||||||
TenancyName string `json:"tenancyName"`
|
TenancyName string `json:"tenancyName"`
|
||||||
HomeRegionKey string `json:"homeRegionKey"`
|
HomeRegionKey string `json:"homeRegionKey"`
|
||||||
@@ -177,7 +178,7 @@ type OciConfig struct {
|
|||||||
PromotionExpires *time.Time `json:"promotionExpires"`
|
PromotionExpires *time.Time `json:"promotionExpires"`
|
||||||
|
|
||||||
AliveStatus string `json:"aliveStatus"`
|
AliveStatus string `json:"aliveStatus"`
|
||||||
LastError string `json:"lastError"`
|
LastError string `gorm:"type:text" json:"lastError"`
|
||||||
LastVerifiedAt *time.Time `json:"lastVerifiedAt"`
|
LastVerifiedAt *time.Time `json:"lastVerifiedAt"`
|
||||||
|
|
||||||
// 多区域 / 多区间支持:开启后订阅区域与 compartment 列表入库缓存,
|
// 多区域 / 多区间支持:开启后订阅区域与 compartment 列表入库缓存,
|
||||||
@@ -199,10 +200,11 @@ type LogEvent struct {
|
|||||||
Source string `gorm:"size:64" json:"source"`
|
Source string `gorm:"size:64" json:"source"`
|
||||||
SourceIP string `gorm:"size:64" json:"sourceIp"` // 异步解析回填,Audit 事件的发起方 IP
|
SourceIP string `gorm:"size:64" json:"sourceIp"` // 异步解析回填,Audit 事件的发起方 IP
|
||||||
EventTime *time.Time `gorm:"index" json:"eventTime"`
|
EventTime *time.Time `gorm:"index" json:"eventTime"`
|
||||||
Payload string `json:"payload"`
|
// 防御上限 256KB 超 MySQL TEXT(64KB),size 上探一档 → MEDIUMTEXT;PG/SQLite 仍为 text
|
||||||
Truncated bool `json:"truncated"`
|
Payload string `gorm:"size:16777216" json:"payload"`
|
||||||
Processed bool `gorm:"index" json:"processed"`
|
Truncated bool `json:"truncated"`
|
||||||
ReceivedAt time.Time `json:"receivedAt"`
|
Processed bool `gorm:"index" json:"processed"`
|
||||||
|
ReceivedAt time.Time `json:"receivedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegionCache 是开启多区域支持的配置缓存的订阅区域(每配置多行,整组覆盖)。
|
// RegionCache 是开启多区域支持的配置缓存的订阅区域(每配置多行,整组覆盖)。
|
||||||
@@ -316,8 +318,9 @@ type AiContentLog struct {
|
|||||||
Endpoint string `gorm:"size:16" json:"endpoint"`
|
Endpoint string `gorm:"size:16" json:"endpoint"`
|
||||||
Model string `gorm:"size:96" json:"model"`
|
Model string `gorm:"size:96" json:"model"`
|
||||||
Stream bool `json:"stream"`
|
Stream bool `json:"stream"`
|
||||||
// RequestBody / ResponseBody 是截断后的正文 JSON
|
// RequestBody / ResponseBody 是截断后的正文 JSON;
|
||||||
RequestBody string `json:"requestBody"`
|
// 64KB 截断恰在 MySQL TEXT 上限(65535B)边界,size 上探一档 → MEDIUMTEXT
|
||||||
ResponseBody string `json:"responseBody"`
|
RequestBody string `gorm:"size:16777216" json:"requestBody"`
|
||||||
|
ResponseBody string `gorm:"size:16777216" json:"responseBody"`
|
||||||
CreatedAt time.Time `gorm:"index" json:"createdAt"`
|
CreatedAt time.Time `gorm:"index" json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>OCI Portal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>前端产物未嵌入:请从前端仓库 Release 下载 dist.zip 解压至此目录(或本地构建前端后拷入),此文件为仓库占位。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package webui 承载前端构建产物的 go:embed 嵌入。
|
||||||
|
// dist 由构建脚本从 oci-portal-dash/dist 拷入覆盖;
|
||||||
|
// 仓库只提交占位 index.html,保证裸 clone 后 go build 可过。
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// all: 前缀连同 _ / . 开头文件一并嵌入(vite 产物含下划线开头的 chunk)。
|
||||||
|
//
|
||||||
|
//go:embed all:dist
|
||||||
|
var distFS embed.FS
|
||||||
|
|
||||||
|
// Dist 返回以 dist 为根的只读文件系统,供 HTTP 层直接伺服。
|
||||||
|
func Dist() (fs.FS, error) {
|
||||||
|
return fs.Sub(distFS, "dist")
|
||||||
|
}
|
||||||