Add Gitea CI, align Docker for multi-arch, and drop legacy YAML config.
CI / docker (push) Failing after 1m35s

Introduce Gitea Actions workflow and CI-aligned Dockerfile; remove file-based
config in favor of SQLite settings; refresh branding and tighten gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
renjue
2026-06-23 21:10:38 +08:00
parent aca63d7526
commit a831012c48
27 changed files with 850 additions and 313 deletions
+24 -2
View File
@@ -1,12 +1,34 @@
# VCS / CI
.git .git
.gitignore .gitignore
.gitea
# Docs (not needed in image)
*.md
web/docs
# Local runtime / secrets
data/ data/
.env
.env.*
# Build artifacts
luminary luminary
luminary-recover
.docker-bin/
*.db *.db
*.test
coverage/
# Frontend (rebuilt in Dockerfile)
web/node_modules web/node_modules
web/dist web/dist
# IDE / editor
.idea .idea
.vscode .vscode
bifrost
.cursor .cursor
*.md
# OS
.DS_Store
Thumbs.db
+182
View File
@@ -0,0 +1,182 @@
# Gitea Actions CI 模板(Go + Vue
复制整个 `.gitea/` 目录到新仓库即可启用 CI:**可选 go test** + **Docker 构建/推送**Go 编译与 Vue 构建在 Dockerfile 内完成)。
## 项目约定
| 路径 | 说明 |
|------|------|
| `go.mod` | 仓库根目录 |
| `Dockerfile` | 仓库根目录,多阶段构建(含前端 `web/` |
| `web/` | 可选;由 Dockerfile 内 `npm run build` 处理,CI 不再单独构建 |
## Dockerfile 要求
工作流的 **Build image** 步骤在仓库根目录(或 Variable `DOCKERFILE` 指定路径)执行 `docker buildx build`**不会**在 CI 里单独装 Node / 跑 `npm`。因此 Dockerfile 必须能独立完成构建与(运行时)启动。
### 基本要求
| 项 | 要求 |
|----|------|
| 位置 | 默认仓库根目录 `Dockerfile`;其他路径用 Variable `DOCKERFILE` |
| 构建上下文 | 仓库根目录 `.`(整个仓库会被 `COPY` 进镜像) |
| Go 版本 | `go.mod``go` 行与 `golang` 基础镜像大版本一致 |
| 多架构 | `go build` 须使用 `GOARCH="$TARGETARCH"`buildx 注入);推荐 `CGO_ENABLED=0` |
| Go 模块代理 | **必须**在镜像内显式设置 `GOPROXY` / `GOSUMDB`(见下);容器内不会自动读取 `go.env` |
| 基础镜像加速 | 支持构建参数 `IMAGE_PREFIX`CI 默认 `docker.1panel.live/library/` |
| 前端(可选) | 存在 `web/` 时在 Dockerfile 内完成 `npm ci` + `npm run build` |
### CI 自动传入的构建参数
| 参数 | 默认 | 说明 |
|------|------|------|
| `IMAGE_PREFIX` | `docker.1panel.live/library/` | 基础镜像前缀;`hub` 表示 Docker Hub |
| `GOPROXY` | `https://goproxy.cn,direct` | 与 workflow / Variable 一致 |
| `GOSUMDB` | `sum.golang.google.cn` | checksum 数据库 |
buildx 还会注入 `TARGETARCH``BUILDPLATFORM` 等,无需在 workflow 里写。
### 推荐:Go + Vue 多阶段模板
```dockerfile
# syntax=docker/dockerfile:1
ARG IMAGE_PREFIX=
# 1) 前端(无 web/ 时可删整个 stage,并去掉 go-builder 里 COPY dist
FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder
WORKDIR /src/web
COPY web/package.json web/package-lock.json web/.npmrc ./
RUN npm ci
COPY web/ ./
RUN npm run build
# 2) Go 编译
FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.25-alpine AS go-builder
ARG TARGETARCH
ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn
ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
# 若静态资源嵌入 GoCOPY --from=web-builder /src/web/dist/ ./path/to/static/
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" \
go build -ldflags="-w -s" -o /app ./cmd/yourapp
# 3) 运行镜像
FROM ${IMAGE_PREFIX}alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=go-builder /app /usr/local/bin/yourapp
ENTRYPOINT ["/usr/local/bin/yourapp"]
```
按项目调整:`./cmd/yourapp`、静态资源路径、运行用户、`EXPOSE` / `VOLUME` 等。
### 仅 Go(无前端)
删除 `web-builder` stage`go-builder` 中不要 `COPY` 前端产物;其余 `GOPROXY` / `TARGETARCH` / `IMAGE_PREFIX` 要求相同。
### 前端 npm 源(国内)
`web/.npmrc` 配置 registry,并在 Dockerfile 里与 `package.json` 一并 `COPY`
```ini
registry=https://registry.npmmirror.com
```
### 本地验证(与 CI 一致)
```bash
docker buildx build --platform linux/amd64 \
--build-arg IMAGE_PREFIX=docker.1panel.live/library/ \
--build-arg GOPROXY=https://goproxy.cn,direct \
-f Dockerfile -t myapp:test .
```
### 常见 Dockerfile 构建错误
| 报错 | 原因 | 处理 |
|------|------|------|
| `proxy.golang.org` timeout | 镜像内未设 `ENV GOPROXY` | go-builder 阶段加 `ARG`/`ENV GOPROXY` |
| `node` / Vite 版本不符 | 基础镜像 Node 过旧 | 使用 `node:20-alpine` 及以上 |
| 某架构 build 失败 | 未使用 `TARGETARCH` | `GOARCH="$TARGETARCH"` |
| 拉基础镜像 timeout | Hub 不可达 | `--build-arg IMAGE_PREFIX=docker.1panel.live/library/` 或 1Panel 配镜像加速 |
## 一次性配置
1. **Runner**:部署 act_runner,标签含 `ubuntu-latest`,并挂载 `docker.sock`(见 [act-runner/README.md](act-runner/README.md))。
2. **Secret**:仓库 Settings → Actions → Secrets,添加 `REGISTRY_TOKEN`PAT`write:package` 权限)。
3. **Variable(推荐)**:若 runner 与 Gitea 同机、或 `gitea.server_url` 为内网地址,必须设置 `REGISTRY=你的公网域名`(如 `git.example.com`**不要**填 `172.17.0.1:13827`)。
4. **Gitea Registry**:服务端 `[packages] ENABLED = true``ROOT_URL` 正确;穿透场景建议 `PUBLIC_URL_DETECTION = never`Gitea 1.26+)。
`REGISTRY` 未设置时,会从 `GITEA_ROOT_URL``gitea.server_url` 推断;均为内网地址时 workflow 会提前失败并提示。
## 可选 Variables
仓库 Settings → Actions → Variables(留空则用默认值):
| 变量 | 默认 | 说明 |
|------|------|------|
| `REGISTRY` | 见下方推断顺序 | **公网** Registry 主机名,如 `git.example.com`(勿用内网 IP:13827 |
| `GITEA_ROOT_URL` | (空) | 当 `gitea.server_url` 为内网时,可设 `https://git.example.com/` |
| `IMAGE_NAME` | 仓库名小写 | 镜像名,非 owner/repo 全路径 |
| `DOCKERFILE` | `Dockerfile` | Dockerfile 路径 |
| `DOCKER_PLATFORMS` | `linux/amd64,linux/arm64` | push 时 buildx 平台 |
| `GO_TEST_SCOPE` | `./...` | `go test` 包路径 |
| `RUN_GO_TEST` | (空,即运行) | 设 `false` 跳过 go test,仅 Docker 构建 |
| `DOCKER_IMAGE_PREFIX` | `1panel` | 基础镜像前缀;可选 `hub` / `daocloud` |
| `GOPROXY` | `https://goproxy.cn,direct` | Go 模块代理 |
| `GOSUMDB` | `sum.golang.google.cn` | Go checksum 数据库 |
## 触发与镜像 tag
- **pull_request**go test(可关)+ 单架构 `docker build` 验证(不推送)
- **push main/master**go test + 多架构构建并推送 `:latest``:sha-xxxxxxx`
- **push tag v\***:额外推送 `:v1.2.3`
示例(仓库 `rose_cat707/Prism`Gitea 在 `git.example.com`):
```text
git.example.com/rose_cat707/prism:latest
git.example.com/rose_cat707/prism:sha-35b3b48
```
## 目录结构
```text
.gitea/
├── README.md # 本文件
├── workflows/
│ └── ci.yml # 主工作流
└── act-runner/ # runner 部署参考(可选)
├── README.md
├── config.yaml
├── docker-compose.yml
└── .env.example
```
## 本地验证 Registry
```bash
host=$(echo "https://你的-gitea-地址/" | sed -e 's|^https://||' -e 's|/.*||')
curl -s -D - "https://${host}/v2/" -o /dev/null | grep -i www-authenticate
docker pull "${host}/owner/image:latest" # 公开 Registry 无需 login
```
推送镜像(CI)仍需仓库 Secret `REGISTRY_TOKEN``write:package`)。仅拉取公开包不需要登录。
`realm` 应指向公网 Gitea 域名,而非 `127.0.0.1`
## 常见 Registry 错误
| 报错 | 原因 | 处理 |
|------|------|------|
| `Get "https://172.17.0.1:13827/v2/"` HTTP/HTTPS | Variable `REGISTRY``gitea.server_url` 为内网地址 | 设 `REGISTRY=git.example.com` |
| token 指向 `127.0.0.1` | Gitea `realm` 配置错误 | `PUBLIC_URL_DETECTION=never` + 正确 `ROOT_URL` |
+3
View File
@@ -0,0 +1,3 @@
GITEA_INSTANCE_URL=https://git.example.com
GITEA_RUNNER_REGISTRATION_TOKEN=从仓库_Settings_Actions_Runners_复制
GITEA_RUNNER_NAME=go-vue-ci-runner
+177
View File
@@ -0,0 +1,177 @@
# act_runner 配置参考
工作流 `runs-on: ubuntu-latest``docker` job 额外使用 `catthehacker/ubuntu:act-22.04` 容器(含 Docker CLI)。
## 构建镜像必须:挂载 Docker Socket
`docker` job 需要访问宿主机 Docker 引擎。在 **runner 所在机器**`config.yaml` 中配置:
```yaml
container:
options: -v /var/run/docker.sock:/var/run/docker.sock
valid_volumes:
- /var/run/docker.sock
```
推荐 job 镜像(含 Node + Docker CLI):
```yaml
runner:
labels:
- "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04"
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
```
修改后 **重启 runner**(例如 `docker restart <runner容器>` 或重启 Gitea Runner 服务)。
## 1Panel 宿主机:配置 Docker 镜像加速(推荐)
Runner 通过 `docker.sock` 使用宿主机 Docker。在 **1Panel** 中配置加速器后,普通 `docker pull` 会走加速;**buildx 多架构构建**仍建议配合 workflow 内的 `IMAGE_PREFIX`(默认 `docker.1panel.live`)。
1. 登录 1Panel → **容器****配置**
2. **镜像加速地址** 填入:
```text
https://docker.1panel.live
```
3. 保存并 **重启 Docker**
验证(在 runner 宿主机):
```bash
docker info | grep -A5 'Registry Mirrors'
docker pull alpine:3.20
```
等价 `daemon.json`
```json
{
"registry-mirrors": ["https://docker.1panel.live"]
}
```
CI 工作流默认 `IMAGE_PREFIX=docker.1panel.live/library/`buildkit 拉基础镜像)。若 1Panel 加速不可用,可在仓库 Variables 设 `DOCKER_IMAGE_PREFIX=daocloud` 或 `hub`。
参考:[1Panel 容器配置文档](https://1panel.cn/docs/user_manual/containers/setting)
## 验证
在 runner 宿主机执行:
```bash
docker info
ls -l /var/run/docker.sock
```
## 从零部署 runner(可选)
```bash
cd .gitea/act-runner
cp .env.example .env # 填入 Registration Token
docker compose up -d
```
## GitHub Actions 镜像(替代 ghfast
日志里出现 `git clone 'https://ghfast.top/https://github.com/actions/checkout'` 说明 **runner 宿主机** 的 `config.yaml` 配置了 `github_mirror`,与仓库 workflow 无关。
在 runner 的 `config.yaml` 中修改(修改后重启 runner):
```yaml
runner:
github_mirror: 'https://gitea.com' # 推荐
```
常用替代方案:
| `github_mirror` 值 | 说明 |
|------------------|------|
| `''`(留空) | 直连 `github.com`,网络可达时最简单 |
| `https://gitea.com` | Gitea 官方 actions 镜像,国内较稳 |
| `https://gitclone.com/github.com` | 第三方 GitHub 克隆镜像 |
| `https://ghfast.top/https://github.com` | 部分环境需代理认证,易报 `Proxy Authentication Required` |
前提:Gitea `app.ini` 中 `[actions] DEFAULT_ACTIONS_URL = github`(默认)。
**不依赖 runner 镜像** 的写法(workflow 内写绝对 URL,仅改写 `github.com` 的请求):
```yaml
uses: https://gitea.com/actions/checkout@v4
```
本模板 `ci.yml` 已采用此写法。若其他 workflow 仍写 `uses: actions/checkout@v4`,仍需配置 `github_mirror` 或改为绝对 URL。
修改 `github_mirror` 后建议清理 runner 缓存目录(如 `/root/.cache/act`),否则旧缓存可能仍指向 ghfast。
## 常见错误
| 报错 | 原因 | 处理 |
|------|------|------|
| `registry-1.docker.io` i/o timeout | Docker Hub 不可达 | 1Panel 配 `docker.1panel.live`Variable `DOCKER_IMAGE_PREFIX=1panel` |
| `ghfast.top` Proxy Authentication Required | runner `github_mirror` 指向 ghfast 且需代理认证 | 改 `github_mirror` 或删掉;见上文「GitHub Actions 镜像」 |
| `docker: command not found` | job 容器无 Docker CLI | 工作流已指定 act 镜像;或 runner 改用 catthehacker/ubuntu |
| `Cannot connect to Docker daemon` | 未挂载 docker.sock | 按上文修改 config.yaml 并重启 runner |
| `node not in PATH` | job 镜像无 Node | 标签映射改用 catthehacker/ubuntu:act-22.04 |
| `http: server gave HTTP response to HTTPS client` 且 token 指向 `127.0.0.1` | **Gitea Registry 配置/反代错误** | 见下文「Registry 登录失败」 |
## Registry 登录失败(127.0.0.1 / HTTP vs HTTPS
若 `docker login git.rc707blog.top` 报错类似:
```text
Get "https://127.0.0.1:xxxxx/v2/token?...": http: server gave HTTP response to HTTPS client
```
说明 Gitea 把 **Docker 认证 token 地址** 配成了本机内网地址,CI runner 访问不到。需在 **Gitea 服务器** 修复,而非改 workflow。
### 1. 检查 `app.ini`
```ini
[server]
ROOT_URL = https://git.example.com/
LOCAL_ROOT_URL = http://127.0.0.1:3000/
; 内网穿透 / 错误 Host 时(Gitea 1.26+):
; PUBLIC_URL_DETECTION = never
[packages]
ENABLED = true
```
`ROOT_URL` 必须与浏览器访问 Gitea 的 **HTTPS 外网地址** 完全一致(含末尾 `/`)。
### 2. 反向代理必须转发 `/v2` 并带上头
Container Registry 固定使用根路径 `/v2`。Nginx 示例:
```nginx
location / {
client_max_body_size 0;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
关键:`X-Forwarded-Proto: https` 和正确的 `Host`(与 Gitea `ROOT_URL` 域名一致)。
### 3. 在 runner 宿主机验证(修复后)
```bash
GITEA_HOST=git.example.com # 改成你的 Gitea 域名
curl -s -D - "https://${GITEA_HOST}/v2/" -o /dev/null | grep -i www-authenticate
echo "$REGISTRY_TOKEN" | docker login "${GITEA_HOST}" -u 你的用户名 --password-stdin
```
应返回 `401 Unauthorized`(正常,表示 registry 可达)且 `docker login` 显示 **Login Succeeded**。
参考:[Gitea 反向代理文档](https://docs.gitea.com/administration/reverse-proxies)
## Gitea Runner v0.6.x(个人 runner
1. 找到 runner 的配置文件或环境(安装目录 / docker compose
2. 确保 runner 进程能访问宿主机 `/var/run/docker.sock`
3. Runners 页标签含 `ubuntu-latest` 且状态 **空闲/在线**
若使用 Gitea 网页注册的个人 runnerdocker 模式),通常需在 runner 启动参数或 `config.yaml` 里加入 socket 挂载,具体路径取决于你的安装方式。
+34
View File
@@ -0,0 +1,34 @@
# act_runner 配置(可选参考部署)
# 工作流 runs-on: ubuntu-latest
log:
level: info
runner:
file: .runner
capacity: 2
timeout: 3h
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
# 拉取 uses: actions/checkout@v4 等 GitHub Action 时的镜像(替换 https://github.com
# 需 Gitea app.ini 中 [actions] DEFAULT_ACTIONS_URL = github
# 留空则直连 github.com;第三方镜像不稳定时可改用 workflow 绝对 URL(见 .gitea/README.md
#
# github_mirror: '' # 直连 GitHub
# github_mirror: 'https://gitea.com' # 推荐:Gitea 官方 actions 镜像
# github_mirror: 'https://gitclone.com/github.com' # 第三方 GitHub 克隆镜像
# github_mirror: 'https://ghfast.top/https://github.com' # 需代理认证时易失败,不推荐
labels:
- "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04"
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
cache:
enabled: false
container:
network: bridge
privileged: false
options: -v /var/run/docker.sock:/var/run/docker.sock
valid_volumes:
- /var/run/docker.sock
+22
View File
@@ -0,0 +1,22 @@
# Gitea act_runner — 可选参考部署(标签 default,与工作流一致)
#
# 若已在 Gitea 注册个人/仓库 runner 且标签为 default,无需使用本目录。
services:
act-runner:
image: docker.io/gitea/act_runner:0.2.12
container_name: prism-act-runner
restart: unless-stopped
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:-https://git.rc707blog.top}
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-prism-ci-runner}
volumes:
- ./config.yaml:/config.yaml:ro
- act-runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
working_dir: /data
volumes:
act-runner-data:
+236
View File
@@ -0,0 +1,236 @@
# 通用 Go + Vue 项目 CIGitea Actions
#
# 单 job:可选 go test + Docker 多架构构建/推送
# 约定:go.mod、Dockerfile(详见 .gitea/README.md「Dockerfile 要求」);前端(可选)在 web/
#
# 前置:act_runnerubuntu-latest + docker.sock)、Secret REGISTRY_TOKEN
# VariablesREGISTRY、IMAGE_NAME、DOCKER_IMAGE_PREFIX、RUN_GO_TEST 等
name: CI
on:
push:
branches: [main, master]
tags: ['v*']
pull_request:
env:
REGISTRY: ${{ vars.REGISTRY }}
GITEA_ROOT_URL: ${{ vars.GITEA_ROOT_URL }}
IMAGE_NAME: ${{ vars.IMAGE_NAME }}
DOCKERFILE: ${{ vars.DOCKERFILE }}
DOCKER_PLATFORMS: ${{ vars.DOCKER_PLATFORMS }}
GO_TEST_SCOPE: ${{ vars.GO_TEST_SCOPE }}
RUN_GO_TEST: ${{ vars.RUN_GO_TEST }}
DOCKER_IMAGE_PREFIX: ${{ vars.DOCKER_IMAGE_PREFIX }}
GOPROXY: ${{ vars.GOPROXY }}
GOSUMDB: ${{ vars.GOSUMDB }}
jobs:
docker:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-22.04
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Run Go tests
if: vars.RUN_GO_TEST != 'false'
shell: bash
run: |
set -euo pipefail
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export GOSUMDB="${GOSUMDB:-sum.golang.google.cn}"
GO_VERSION=$(grep '^go ' go.mod | awk '{print $2}')
ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
TAR="go${GO_VERSION}.linux-${ARCH}.tar.gz"
if ! command -v go >/dev/null 2>&1 || ! go version | grep -q "go${GO_VERSION} "; then
downloaded=0
for url in \
"https://mirrors.aliyun.com/golang/${TAR}" \
"https://golang.google.cn/dl/${TAR}" \
"https://go.dev/dl/${TAR}"; do
echo "trying ${url}"
if curl -fsSL --connect-timeout 20 --retry 3 --retry-delay 5 --max-time 600 \
"$url" -o /tmp/go.tgz; then
downloaded=1
break
fi
done
[ "$downloaded" -eq 1 ] || { echo "failed to download Go ${GO_VERSION}" >&2; exit 1; }
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tgz
export PATH="/usr/local/go/bin:${PATH}"
fi
go version
go test "${GO_TEST_SCOPE:-./...}"
- name: Setup Docker
run: |
set -eux
if ! command -v docker >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io
fi
if [ ! -S /var/run/docker.sock ]; then
echo "ERROR: /var/run/docker.sock 未挂载到 job 容器。" >&2
echo "请在 act_runner config.yaml 中配置:" >&2
echo " container.options: -v /var/run/docker.sock:/var/run/docker.sock" >&2
echo " container.valid_volumes: [/var/run/docker.sock]" >&2
exit 1
fi
docker info
- name: Resolve registry
id: reg
if: gitea.event_name == 'push'
shell: bash
env:
GITEA_SERVER_URL: ${{ gitea.server_url }}
run: |
set -euo pipefail
url_to_host() {
echo "$1" | sed -e 's|^https://||' -e 's|^http://||' -e 's|/.*||'
}
is_internal_host() {
local host="${1%%:*}"
case "$host" in
localhost|127.*|10.*|192.168.*) return 0 ;;
172.*)
local second
second=$(echo "$host" | cut -d. -f2)
[ "$second" -ge 16 ] && [ "$second" -le 31 ]
return
;;
*) return 1 ;;
esac
}
registry="${REGISTRY:-}"
if [ -z "$registry" ] && [ -n "${GITEA_ROOT_URL:-}" ]; then
registry=$(url_to_host "${GITEA_ROOT_URL}")
fi
if [ -z "$registry" ]; then
registry=$(url_to_host "${GITEA_SERVER_URL}")
fi
if is_internal_host "$registry"; then
echo "ERROR: Registry 主机 '${registry}' 是内网地址。" >&2
echo "请设置 Variable REGISTRY=公网 Gitea 域名(如 git.example.com)。" >&2
exit 1
fi
echo "registry host: ${registry}"
auth_header=""
if auth_header=$(curl -fsS -D - "https://${registry}/v2/" -o /dev/null 2>&1 | grep -i '^www-authenticate:'); then
if echo "$auth_header" | grep -qiE '127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|localhost|:13827'; then
echo "ERROR: Registry token realm 指向内网地址:${auth_header}" >&2
exit 1
fi
fi
echo "host=${registry}" >> "${GITHUB_OUTPUT}"
- name: Log in to Container Registry
if: gitea.event_name == 'push'
shell: bash
run: |
set -euo pipefail
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login "${{ steps.reg.outputs.host }}" -u "${{ gitea.actor }}" --password-stdin
- name: Build image
shell: bash
env:
GITEA_SHA: ${{ gitea.sha }}
GITEA_REF: ${{ gitea.ref }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }}
GITEA_EVENT_NAME: ${{ gitea.event_name }}
REGISTRY_HOST: ${{ steps.reg.outputs.host }}
run: |
set -euo pipefail
dockerfile="${DOCKERFILE:-Dockerfile}"
if [ "${DOCKER_IMAGE_PREFIX:-}" = "hub" ] || [ "${DOCKER_IMAGE_PREFIX:-}" = "docker.io" ]; then
image_prefix=""
elif [ -z "${DOCKER_IMAGE_PREFIX:-}" ] || [ "${DOCKER_IMAGE_PREFIX}" = "1panel" ]; then
image_prefix="docker.1panel.live/library/"
elif [ "${DOCKER_IMAGE_PREFIX}" = "daocloud" ]; then
image_prefix="docker.m.daocloud.io/library/"
else
image_prefix="${DOCKER_IMAGE_PREFIX}"
[[ "${image_prefix}" == */ ]] || image_prefix="${image_prefix}/"
fi
echo "using IMAGE_PREFIX=${image_prefix:-<docker.io>}"
build_args=(
--build-arg "IMAGE_PREFIX=${image_prefix}"
--build-arg "GOPROXY=${GOPROXY:-https://goproxy.cn,direct}"
--build-arg "GOSUMDB=${GOSUMDB:-sum.golang.google.cn}"
)
builder_id="gitea-buildx-${GITEA_REPOSITORY//\//-}"
if [ "${GITEA_EVENT_NAME}" = "push" ]; then
install_binfmt() {
for img in \
"docker.1panel.live/tonistiigi/binfmt:latest" \
"docker.m.daocloud.io/tonistiigi/binfmt:latest" \
"tonistiigi/binfmt:latest"; do
echo "trying binfmt image: ${img}"
if docker run --privileged --rm "${img}" --install all; then
return 0
fi
done
return 1
}
install_binfmt || true
registry="${REGISTRY_HOST}"
image_name="${IMAGE_NAME:-$(echo "${GITEA_REPOSITORY##*/}" | tr '[:upper:]' '[:lower:]')}"
platforms="${DOCKER_PLATFORMS:-linux/amd64,linux/arm64}"
image="${registry}/${GITEA_REPOSITORY_OWNER}/${image_name}"
tags="${image}:sha-${GITEA_SHA:0:7}"
case "${GITEA_REF}" in
refs/heads/main|refs/heads/master)
tags="${tags},${image}:latest"
;;
refs/tags/v*)
tags="${tags},${image}:${GITEA_REF#refs/tags/}"
;;
esac
docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}"
docker buildx inspect --bootstrap
tag_args=()
IFS=',' read -r -a tag_list <<< "$tags"
for t in "${tag_list[@]}"; do
tag_args+=(-t "$t")
done
docker buildx build \
--platform "${platforms}" \
-f "${dockerfile}" \
"${build_args[@]}" \
"${tag_args[@]}" \
--push \
.
else
# PR:仅单架构构建验证 Dockerfile,不推送
docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}"
docker buildx inspect --bootstrap
docker buildx build \
--platform linux/amd64 \
-f "${dockerfile}" \
"${build_args[@]}" \
--load \
.
fi
+25 -24
View File
@@ -1,41 +1,42 @@
# ---> Go # Binaries
# If you prefer the allow list template instead of the deny list, see community template: /luminary
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore /luminary-recover
#
# Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
*.dylib *.dylib
# Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE # Go
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work go.work
go.work.sum go.work.sum
*.out
coverage/
*.coverprofile
# env file # Luminary runtime data
.env data/
# Luminary runtime
/data/ /data/
/luminary
.docker-bin/
*.db *.db
# Node # Docker local prebuilt
web/node_modules/ .docker-bin/
web/dist/assets/
# IDE # Frontend build output
web/dist/
web/node_modules/
# Environment
.env
.env.*
!.env.example
# IDE / editor
.idea/ .idea/
.vscode/ .vscode/
.cursor/
# OS
.DS_Store
Thumbs.db
+51 -26
View File
@@ -1,43 +1,68 @@
# Multi-stage: build frontend + Go binaries at image build time; runtime only executes. # Luminary AI GatewayGo + Vue 管理台)
ARG NODE_MAJOR=20 #
ARG DOCKER_REGISTRY=docker.1panel.live/library/ # 构建:
ARG APK_MIRROR=mirrors.aliyun.com # docker build -t luminary:latest .
#
# 运行:
# docker run -d --name luminary \
# -p 8293:8293 \
# -v luminary-data:/data \
# luminary:latest
#
# 生产镜像由 Gitea Actions 构建,见 README 与 .gitea/README.md
# syntax=docker/dockerfile:1
# 基础镜像默认 Docker HubCI 默认 1Panel 加速(docker.1panel.live),本地可传:
# docker build --build-arg IMAGE_PREFIX=docker.1panel.live/library/ -t luminary:latest .
ARG IMAGE_PREFIX=
FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder
FROM ${DOCKER_REGISTRY}node:${NODE_MAJOR}-alpine AS web
WORKDIR /src/web WORKDIR /src/web
ARG NPM_REGISTRY=https://registry.npmmirror.com COPY web/package.json web/package-lock.json web/.npmrc ./
COPY web/package.json web/package-lock.json ./ RUN npm ci
RUN npm ci --registry="${NPM_REGISTRY}" COPY web/src web/src
COPY web/ ./ COPY web/public web/public
COPY web/index.html web/tsconfig.json web/vite.config.ts ./
RUN npm run build RUN npm run build
ARG DOCKER_REGISTRY=docker.1panel.live/library/ FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.25-alpine AS go-builder
FROM ${DOCKER_REGISTRY}golang:1.25-alpine AS gobuild
WORKDIR /src ARG TARGETARCH=amd64
ARG GOPROXY=https://goproxy.cn,direct ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn ARG GOSUMDB=sum.golang.google.cn
ENV CGO_ENABLED=0 \ ENV CGO_ENABLED=0 GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
GOPROXY=${GOPROXY} \
GOSUMDB=${GOSUMDB}
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=web /src/web/dist ./web/dist
RUN go build -trimpath -ldflags="-s -w" -o /out/luminary ./cmd/luminary \
&& go build -trimpath -ldflags="-s -w" -o /out/luminary-recover ./cmd/luminary-recover
ARG DOCKER_REGISTRY=docker.1panel.live/library/ WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
COPY web/static_embed.go ./web/
COPY --from=web-builder /src/web/dist ./web/dist
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOOS=linux GOARCH="$TARGETARCH" \
go build -trimpath -ldflags="-w -s" -o /luminary ./cmd/luminary \
&& go build -trimpath -ldflags="-w -s" -o /luminary-recover ./cmd/luminary-recover
ARG IMAGE_PREFIX=
ARG APK_MIRROR=mirrors.aliyun.com ARG APK_MIRROR=mirrors.aliyun.com
FROM ${DOCKER_REGISTRY}alpine:3.20 AS runtime FROM ${IMAGE_PREFIX}alpine:3.20
ARG APK_MIRROR ARG APK_MIRROR
RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \ RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \
&& apk add --no-cache ca-certificates tini && apk add --no-cache ca-certificates tzdata tini
ENV LUMINARY_DATA=/data \ ENV LUMINARY_DATA=/data \
LUMINARY_ADDR=:8293 LUMINARY_ADDR=:8293
COPY --from=gobuild /out/luminary /usr/local/bin/luminary COPY --from=go-builder /luminary /usr/local/bin/luminary
COPY --from=gobuild /out/luminary-recover /usr/local/bin/luminary-recover COPY --from=go-builder /luminary-recover /usr/local/bin/luminary-recover
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY scripts/luminary-recover.sh /usr/local/bin/luminary-recover.sh COPY scripts/luminary-recover.sh /usr/local/bin/luminary-recover.sh
RUN chmod +x /entrypoint.sh /usr/local/bin/luminary-recover.sh RUN chmod +x /entrypoint.sh /usr/local/bin/luminary-recover.sh
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Luminary Contributors
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.
+4 -4
View File
@@ -3,7 +3,7 @@
export GOPROXY ?= https://goproxy.cn,direct export GOPROXY ?= https://goproxy.cn,direct
export GOSUMDB ?= sum.golang.google.cn export GOSUMDB ?= sum.golang.google.cn
export GOPRIVATE ?= git.rc707blog.top export GOPRIVATE ?= git.rc707blog.top
export DOCKER_REGISTRY ?= docker.1panel.live/library/ export IMAGE_PREFIX ?= docker.1panel.live/library/
export APK_MIRROR ?= mirrors.aliyun.com export APK_MIRROR ?= mirrors.aliyun.com
export DOCKER_PLATFORM ?= linux/amd64 export DOCKER_PLATFORM ?= linux/amd64
export LUMINARY_IMAGE ?= registry.rc707blog.top/rose_cat707/luminary:latest export LUMINARY_IMAGE ?= registry.rc707blog.top/rose_cat707/luminary:latest
@@ -33,7 +33,7 @@ test:
docker-build: docker-build:
docker buildx build --platform $(DOCKER_PLATFORM) \ docker buildx build --platform $(DOCKER_PLATFORM) \
-t $(LUMINARY_IMAGE) \ -t $(LUMINARY_IMAGE) \
--build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \
--build-arg APK_MIRROR=$(APK_MIRROR) \ --build-arg APK_MIRROR=$(APK_MIRROR) \
--build-arg GOPROXY=$(GOPROXY) \ --build-arg GOPROXY=$(GOPROXY) \
--build-arg GOSUMDB=$(GOSUMDB) \ --build-arg GOSUMDB=$(GOSUMDB) \
@@ -43,7 +43,7 @@ docker-build:
docker-push: docker-push:
docker buildx build --platform $(DOCKER_PLATFORM) \ docker buildx build --platform $(DOCKER_PLATFORM) \
-t $(LUMINARY_IMAGE) \ -t $(LUMINARY_IMAGE) \
--build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \
--build-arg APK_MIRROR=$(APK_MIRROR) \ --build-arg APK_MIRROR=$(APK_MIRROR) \
--build-arg GOPROXY=$(GOPROXY) \ --build-arg GOPROXY=$(GOPROXY) \
--build-arg GOSUMDB=$(GOSUMDB) \ --build-arg GOSUMDB=$(GOSUMDB) \
@@ -57,7 +57,7 @@ docker-push-prebuilt: build-web
docker buildx build --platform $(DOCKER_PLATFORM) \ docker buildx build --platform $(DOCKER_PLATFORM) \
-f docker/Dockerfile.prebuilt \ -f docker/Dockerfile.prebuilt \
-t $(LUMINARY_IMAGE) \ -t $(LUMINARY_IMAGE) \
--build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \
--build-arg APK_MIRROR=$(APK_MIRROR) \ --build-arg APK_MIRROR=$(APK_MIRROR) \
--push \ --push \
. .
+30 -4
View File
@@ -133,7 +133,7 @@ cd web && npm install && npm run dev
| `-data-dir` / `LUMINARY_DATA` | SQLite 数据目录 | `./data` | | `-data-dir` / `LUMINARY_DATA` | SQLite 数据目录 | `./data` |
| `-addr` / `LUMINARY_ADDR` | HTTP 监听地址 | `:8293` | | `-addr` / `LUMINARY_ADDR` | HTTP 监听地址 | `:8293` |
> 若存在旧版 `configs/config.yaml` 或 `/data/config.yaml`**仅在首次初始化系统设置时**会尝试导入其中的加密密钥等项(导入失败不阻塞启动)。 可选环境变量 `LUMINARY_ENCRYPTION_KEY`:仅在首次初始化系统设置时写入加密密钥(一般无需设置,管理台可改)。
## Docker 部署 ## Docker 部署
@@ -152,7 +152,34 @@ LUMINARY_IMAGE=registry.example.com/luminary:v1.0.0 make docker-push
docker compose up -d docker compose up -d
``` ```
`docker-compose.yml` 通过环境变量 `LUMINARY_IMAGE` 指定镜像名(默认 `luminary:latest`)。更新版本需重新 `docker build` / `docker push` 后重启容器。 `docker-compose.yml` 通过环境变量 `LUMINARY_IMAGE` 指定镜像名(默认 `registry.rc707blog.top/rose_cat707/luminary:latest`)。更新版本需重新 `docker build` / `docker push` 后重启容器。
### Gitea Actions CI
生产镜像由 Gitea Actions 自动构建并推送到 Container Registry,工作流见 [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml),详细配置见 [`.gitea/README.md`](.gitea/README.md)。
**一次性配置**(仓库 Settings → Actions):
| 类型 | 名称 | 说明 |
|------|------|------|
| Secret | `REGISTRY_TOKEN` | Gitea PAT,需 `write:package` 权限 |
| Variable | `REGISTRY` | 公网 Gitea 域名,如 `git.rc707blog.top`(勿用内网 IP |
**触发与镜像 tag**
- `pull_request`go test + 单架构 Docker 构建验证(不推送)
- `push main/master`:多架构构建并推送 `:latest``:sha-xxxxxxx`
- `push tag v*`:额外推送 `:v1.2.3`
示例镜像地址(仓库 `rose_cat707/luminary`Gitea 在 `git.rc707blog.top`):
```text
git.rc707blog.top/rose_cat707/luminary:latest
git.rc707blog.top/rose_cat707/luminary:sha-35b3b48
git.rc707blog.top/rose_cat707/luminary:v1.0.0
```
部署时设置 `LUMINARY_IMAGE` 指向 CI 推送的镜像即可。
## 应急恢复(容器内) ## 应急恢复(容器内)
@@ -189,7 +216,7 @@ go run ./cmd/luminary-recover --all
| `--clear-ip` | 删除全部 IP 规则 | | `--clear-ip` | 删除全部 IP 规则 |
| `--clear-sessions` | 删除全部管理员 Session | | `--clear-sessions` | 删除全部管理员 Session |
| `--restart` | 向 PID 1 发送 SIGTERM,由容器重启策略拉起新进程(清除登录冷却) | | `--restart` | 向 PID 1 发送 SIGTERM,由容器重启策略拉起新进程(清除登录冷却) |
| `--password` | 新密码(默认 `admin123`;若存在旧版 config 则尝试读取其中 `admin.password` | | `--password` | 新密码(默认 `admin123` |
| `--username` | 管理员用户名(默认 `admin` | | `--username` | 管理员用户名(默认 `admin` |
| `-data-dir` / `LUMINARY_DATA` | 数据目录(默认 `./data`,容器内为 `/data` | | `-data-dir` / `LUMINARY_DATA` | 数据目录(默认 `./data`,容器内为 `/data` |
@@ -201,7 +228,6 @@ cmd/luminary-recover/ 应急恢复 CLI(容器内 docker exec 使用)
scripts/ 运维脚本(含 luminary-recover.sh scripts/ 运维脚本(含 luminary-recover.sh
internal/ Go 后端(gateway、store、handler... internal/ Go 后端(gateway、store、handler...
web/ Vue 3 前端(构建产物嵌入二进制) web/ Vue 3 前端(构建产物嵌入二进制)
configs/ 旧版配置示例(可选,仅用于首次迁移导入)
``` ```
## License ## License
-31
View File
@@ -1,31 +0,0 @@
# 已废弃:Luminary 不再依赖配置文件启动,请在管理台「系统设置」维护参数。
# 本文件仅用于从旧版部署首次迁移时自动导入(若存在且数据库尚无 system_settings 记录)。
server:
addr: ":8293"
database:
path: "./data/luminary.db"
admin:
username: "admin"
password: "admin123"
cache:
usage_flush_interval: 5s
health_check:
interval: 30s
security:
# 生产环境必须修改;容器首次启动会自动生成随机密钥
# 也可通过环境变量覆盖:encryption_key: "env.LUMINARY_ENCRYPTION_KEY"
encryption_key: "change-me-in-production-32bytes!!"
session_ttl: 24h
login_max_attempts: 5
login_lockout: 15m
# 仅当直连来源为以下 IP/CIDR 时才信任 X-Forwarded-For / X-Real-IP(反代部署时配置)
trusted_proxies: []
gateway:
max_retries: 3
retry_backoff_ms: 200
+2 -2
View File
@@ -4,10 +4,10 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
DOCKER_REGISTRY: docker.1panel.live/library/ IMAGE_PREFIX: docker.1panel.live/library/
APK_MIRROR: mirrors.aliyun.com APK_MIRROR: mirrors.aliyun.com
NPM_REGISTRY: https://registry.npmmirror.com
GOPROXY: https://goproxy.cn,direct GOPROXY: https://goproxy.cn,direct
GOSUMDB: sum.golang.google.cn
image: ${LUMINARY_IMAGE:-registry.rc707blog.top/rose_cat707/luminary:latest} image: ${LUMINARY_IMAGE:-registry.rc707blog.top/rose_cat707/luminary:latest}
container_name: luminary container_name: luminary
restart: unless-stopped restart: unless-stopped
+2 -2
View File
@@ -1,7 +1,7 @@
# Runtime image using locally cross-compiled linux/amd64 binaries (avoids QEMU segfault in buildx). # Runtime image using locally cross-compiled linux/amd64 binaries (avoids QEMU segfault in buildx).
ARG DOCKER_REGISTRY=docker.1panel.live/library/ ARG IMAGE_PREFIX=docker.1panel.live/library/
ARG APK_MIRROR=mirrors.aliyun.com ARG APK_MIRROR=mirrors.aliyun.com
FROM ${DOCKER_REGISTRY}alpine:3.20 AS runtime FROM ${IMAGE_PREFIX}alpine:3.20 AS runtime
ARG APK_MIRROR ARG APK_MIRROR
RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \ RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \
&& apk add --no-cache ca-certificates tini && apk add --no-cache ca-certificates tini
+2 -3
View File
@@ -5,8 +5,9 @@ go 1.25.0
require ( require (
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/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.3
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.53.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
@@ -24,8 +25,6 @@ require (
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.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/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // 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/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
-8
View File
@@ -50,10 +50,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -74,8 +70,6 @@ github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRC
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
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=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -109,8 +103,6 @@ golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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.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=
-113
View File
@@ -1,113 +0,0 @@
package config
import (
"fmt"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Admin AdminConfig `yaml:"admin"`
Cache CacheConfig `yaml:"cache"`
HealthCheck HealthCheckConfig `yaml:"health_check"`
Security SecurityConfig `yaml:"security"`
Gateway GatewayConfig `yaml:"gateway"`
}
type GatewayConfig struct {
MaxRetries int `yaml:"max_retries"`
RetryBackoffMs int `yaml:"retry_backoff_ms"`
}
type ServerConfig struct {
Addr string `yaml:"addr"`
}
type DatabaseConfig struct {
Path string `yaml:"path"`
}
type AdminConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type CacheConfig struct {
UsageFlushInterval time.Duration `yaml:"usage_flush_interval"`
}
type HealthCheckConfig struct {
Interval time.Duration `yaml:"interval"`
}
type SecurityConfig struct {
EncryptionKey string `yaml:"encryption_key"`
SessionTTL time.Duration `yaml:"session_ttl"`
IPAllowlist []string `yaml:"ip_allowlist"`
TrustedProxies []string `yaml:"trusted_proxies"`
LoginMaxAttempts int `yaml:"login_max_attempts"`
LoginLockout time.Duration `yaml:"login_lockout"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
cfg := Default()
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
resolveEnv(cfg)
return cfg, nil
}
func Default() *Config {
return &Config{
Server: ServerConfig{Addr: ":8080"},
Database: DatabaseConfig{
Path: "./data/luminary.db",
},
Admin: AdminConfig{
Username: "admin",
Password: "admin123",
},
Cache: CacheConfig{
UsageFlushInterval: 5 * time.Second,
},
HealthCheck: HealthCheckConfig{
Interval: 30 * time.Second,
},
Security: SecurityConfig{
EncryptionKey: "change-me-in-production-32bytes!!",
SessionTTL: 24 * time.Hour,
LoginMaxAttempts: 5,
LoginLockout: 15 * time.Minute,
},
Gateway: GatewayConfig{
MaxRetries: 3,
RetryBackoffMs: 200,
},
}
}
func resolveEnv(cfg *Config) {
cfg.Admin.Username = resolveEnvValue(cfg.Admin.Username)
cfg.Admin.Password = resolveEnvValue(cfg.Admin.Password)
cfg.Security.EncryptionKey = resolveEnvValue(cfg.Security.EncryptionKey)
}
func resolveEnvValue(v string) string {
if strings.HasPrefix(v, "env.") {
key := strings.TrimPrefix(v, "env.")
if val := os.Getenv(key); val != "" {
return val
}
}
return v
}
-8
View File
@@ -6,7 +6,6 @@ import (
"strings" "strings"
"github.com/rose_cat707/luminary/internal/auth" "github.com/rose_cat707/luminary/internal/auth"
"github.com/rose_cat707/luminary/internal/config"
"github.com/rose_cat707/luminary/internal/model" "github.com/rose_cat707/luminary/internal/model"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -25,13 +24,6 @@ func encryptionKeyCandidates() []string {
} }
add(os.Getenv("LUMINARY_ENCRYPTION_KEY")) add(os.Getenv("LUMINARY_ENCRYPTION_KEY"))
for _, path := range legacyConfigPaths() {
cfg, err := config.Load(path)
if err != nil {
continue
}
add(cfg.Security.EncryptionKey)
}
add(legacyDefaultEncryptionKey) add(legacyDefaultEncryptionKey)
return out return out
} }
+2 -63
View File
@@ -1,83 +1,22 @@
package settings package settings
import ( import (
"encoding/json"
"os" "os"
"strings" "strings"
"github.com/rose_cat707/luminary/internal/config"
"github.com/rose_cat707/luminary/internal/model" "github.com/rose_cat707/luminary/internal/model"
) )
const legacyDefaultEncryptionKey = "change-me-in-production-32bytes!!" const legacyDefaultEncryptionKey = "change-me-in-production-32bytes!!"
func legacyConfigPaths() []string {
var paths []string
if p := os.Getenv("LUMINARY_CONFIG"); p != "" {
paths = append(paths, p)
}
paths = append(paths, "configs/config.yaml", "/data/config.yaml")
return paths
}
func mergeLegacySettings(s model.SystemSettings) model.SystemSettings { func mergeLegacySettings(s model.SystemSettings) model.SystemSettings {
for _, path := range legacyConfigPaths() {
cfg, err := config.Load(path)
if err != nil {
continue
}
key := strings.TrimSpace(cfg.Security.EncryptionKey)
if key != "" {
s.EncryptionKey = key
}
if cfg.Security.SessionTTL > 0 {
s.SessionTTLSeconds = int64(cfg.Security.SessionTTL.Seconds())
}
if cfg.Security.LoginMaxAttempts > 0 {
s.LoginMaxAttempts = cfg.Security.LoginMaxAttempts
}
if cfg.Security.LoginLockout > 0 {
s.LoginLockoutSeconds = int64(cfg.Security.LoginLockout.Seconds())
}
if len(cfg.Security.TrustedProxies) > 0 {
b, _ := json.Marshal(cfg.Security.TrustedProxies)
s.TrustedProxiesJSON = string(b)
}
if cfg.Cache.UsageFlushInterval > 0 {
s.UsageFlushSeconds = int64(cfg.Cache.UsageFlushInterval.Seconds())
}
if cfg.HealthCheck.Interval > 0 {
s.HealthCheckSeconds = int64(cfg.HealthCheck.Interval.Seconds())
}
if cfg.Gateway.MaxRetries > 0 {
s.GatewayMaxRetries = cfg.Gateway.MaxRetries
}
if cfg.Gateway.RetryBackoffMs >= 0 {
s.GatewayRetryBackoffMs = cfg.Gateway.RetryBackoffMs
}
return s
}
if key := strings.TrimSpace(os.Getenv("LUMINARY_ENCRYPTION_KEY")); key != "" { if key := strings.TrimSpace(os.Getenv("LUMINARY_ENCRYPTION_KEY")); key != "" {
s.EncryptionKey = key s.EncryptionKey = key
} }
return s return s
} }
// LegacyAdminCredentials returns admin bootstrap credentials from an optional legacy config file. // LegacyAdminCredentials returns default bootstrap credentials for first admin.
func LegacyAdminCredentials() (username, password string) { func LegacyAdminCredentials() (username, password string) {
username, password = "admin", "admin123" return "admin", "admin123"
for _, path := range legacyConfigPaths() {
cfg, err := config.Load(path)
if err != nil {
continue
}
if u := strings.TrimSpace(cfg.Admin.Username); u != "" {
username = u
}
if p := strings.TrimSpace(cfg.Admin.Password); p != "" {
password = p
}
return username, password
}
return username, password
} }
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Luminary AI Gateway</title>
<script type="module" crossorigin src="/assets/index-DZZ57wgd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CDyLXzvy.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Luminary AI Gateway</title> <title>Luminary AI Gateway</title>
</head> </head>
<body> <body>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#0f7a62"/>
<circle cx="16" cy="16" r="2" fill="#fafafa"/>
<path d="M16 8v4.5" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
<path d="M16 19.5V24" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
<path d="M8 16h4.5" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
<path d="M19.5 16H24" stroke="#fafafa" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

+20
View File
@@ -0,0 +1,20 @@
<template>
<svg
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<circle cx="12" cy="12" r="2" fill="currentColor" />
<path d="M12 4v4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M12 15.5V20" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M4 12h4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M15.5 12H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</template>
<script setup lang="ts">
withDefaults(defineProps<{ size?: number | string }>(), { size: 20 })
</script>
+2 -5
View File
@@ -3,11 +3,7 @@
<aside class="sidebar custom-scrollbar"> <aside class="sidebar custom-scrollbar">
<div class="sidebar-brand"> <div class="sidebar-brand">
<div class="brand-icon"> <div class="brand-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <BrandIcon :size="20" />
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div> </div>
<span class="brand-name">{{ t('common.brand') }}</span> <span class="brand-name">{{ t('common.brand') }}</span>
</div> </div>
@@ -57,6 +53,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Connection, Document, Key, Lock, Odometer, Setting } from '@element-plus/icons-vue' import { Connection, Document, Key, Lock, Odometer, Setting } from '@element-plus/icons-vue'
import api from '@/api/client' import api from '@/api/client'
import BrandIcon from '@/components/BrandIcon.vue'
import LocaleSwitcher from '@/components/LocaleSwitcher.vue' import LocaleSwitcher from '@/components/LocaleSwitcher.vue'
const route = useRoute() const route = useRoute()
+2 -5
View File
@@ -2,11 +2,7 @@
<div class="login-page"> <div class="login-page">
<div class="login-wrap"> <div class="login-wrap">
<div class="login-brand-icon"> <div class="login-brand-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <BrandIcon :size="24" />
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div> </div>
<h1 class="page-title">{{ t('common.brand') }}</h1> <h1 class="page-title">{{ t('common.brand') }}</h1>
<p class="page-desc">{{ t('login.subtitle') }}</p> <p class="page-desc">{{ t('login.subtitle') }}</p>
@@ -38,6 +34,7 @@ import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import api from '@/api/client' import api from '@/api/client'
import BrandIcon from '@/components/BrandIcon.vue'
import LocaleSwitcher from '@/components/LocaleSwitcher.vue' import LocaleSwitcher from '@/components/LocaleSwitcher.vue'
const router = useRouter() const router = useRouter()