移除本地 Docker 推送脚本,Dockerfile 对接通用 Gitea Actions 工作流;补充 LICENSE、优化 gitignore,并从版本库移除前端构建产物。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-2
@@ -1,4 +1,3 @@
|
||||
nps/
|
||||
.git/
|
||||
bin/
|
||||
release/
|
||||
@@ -6,6 +5,6 @@ data/
|
||||
**/.DS_Store
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
agent-transcripts/
|
||||
.gitea/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
@@ -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/
|
||||
# 若静态资源嵌入 Go:COPY --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` |
|
||||
@@ -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
|
||||
@@ -0,0 +1,144 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 报错 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| `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 |
|
||||
| `registry-1.docker.io` i/o timeout | Docker Hub 不可达 | 1Panel 配 `docker.1panel.live`;Variable `DOCKER_IMAGE_PREFIX=1panel` |
|
||||
| `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 网页注册的个人 runner(docker 模式),通常需在 runner 启动参数或 `config.yaml` 里加入 socket 挂载,具体路径取决于你的安装方式。
|
||||
@@ -0,0 +1,26 @@
|
||||
# act_runner 配置(可选参考部署)
|
||||
# 工作流 runs-on: ubuntu-latest
|
||||
|
||||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
file: .runner
|
||||
capacity: 2
|
||||
timeout: 3h
|
||||
insecure: false
|
||||
fetch_timeout: 5s
|
||||
fetch_interval: 2s
|
||||
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
|
||||
@@ -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:
|
||||
@@ -0,0 +1,236 @@
|
||||
# 通用 Go + Vue 项目 CI(Gitea Actions)
|
||||
#
|
||||
# 单 job:可选 go test + Docker 多架构构建/推送
|
||||
# 约定:go.mod、Dockerfile(详见 .gitea/README.md「Dockerfile 要求」);前端(可选)在 web/
|
||||
#
|
||||
# 前置:act_runner(ubuntu-latest + docker.sock)、Secret REGISTRY_TOKEN
|
||||
# Variables:REGISTRY、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: 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
|
||||
+33
-18
@@ -1,32 +1,47 @@
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
# Go
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Wormhole
|
||||
bin/
|
||||
coverage.out
|
||||
coverage.html
|
||||
*.coverprofile
|
||||
|
||||
# Dependencies
|
||||
vendor/
|
||||
|
||||
# Environment & secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Runtime data
|
||||
data/
|
||||
release/
|
||||
|
||||
# Frontend build artifacts
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temp
|
||||
tmp/
|
||||
dist/
|
||||
|
||||
+18
-11
@@ -1,7 +1,10 @@
|
||||
# Wormhole 统一镜像(server / agent 同一二进制,启动命令区分角色)
|
||||
#
|
||||
# 构建:
|
||||
# docker build -t wormhole:latest .
|
||||
# 本地构建(与 CI 一致):
|
||||
# docker buildx build --platform linux/amd64 \
|
||||
# --build-arg IMAGE_PREFIX=docker.1panel.live/library/ \
|
||||
# --build-arg GOPROXY=https://goproxy.cn,direct \
|
||||
# -f Dockerfile -t wormhole:latest .
|
||||
#
|
||||
# 服务端:
|
||||
# docker run -d --name wormhole \
|
||||
@@ -10,22 +13,27 @@
|
||||
# wormhole:latest
|
||||
#
|
||||
# Agent:
|
||||
# docker run -d --name wormhole-agent --network host \
|
||||
# docker run -d --name wormhole-agent --restart=unless-stopped --network host \
|
||||
# wormhole:latest agent -server <服务端IP>:8528 -key <verify_key>
|
||||
|
||||
# syntax=docker/dockerfile:1
|
||||
ARG IMAGE_PREFIX=
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS web-builder
|
||||
FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder
|
||||
|
||||
WORKDIR /src/web
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci --registry=https://registry.npmmirror.com
|
||||
COPY web/package.json web/package-lock.json web/.npmrc ./
|
||||
RUN npm ci
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS go-builder
|
||||
FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.24-alpine AS go-builder
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG GOPROXY=https://goproxy.cn,direct
|
||||
ARG GOSUMDB=sum.golang.google.cn
|
||||
ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev curl tar \
|
||||
&& if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
curl -fsSL https://musl.cc/x86_64-linux-musl-cross.tgz -o /tmp/cross.tgz \
|
||||
@@ -35,8 +43,7 @@ RUN apk add --no-cache gcc musl-dev curl tar \
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum go.env ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
GOPROXY=https://goproxy.cn,direct go mod download
|
||||
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
@@ -46,13 +53,13 @@ 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 \
|
||||
set -eu; \
|
||||
export CGO_ENABLED=1 GOOS=linux GOARCH="$TARGETARCH" GOPROXY=https://goproxy.cn,direct; \
|
||||
export CGO_ENABLED=1 GOOS=linux GOARCH="$TARGETARCH"; \
|
||||
if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
export CC=/usr/local/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc; \
|
||||
fi; \
|
||||
go build -ldflags="-w -s" -o /wormhole ./cmd/wormhole
|
||||
|
||||
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
|
||||
FROM ${IMAGE_PREFIX}alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S wormhole \
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Wormhole 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
# 运行参考(配置已迁移至 Web 管理台 → 系统设置,无需配置文件)
|
||||
|
||||
.PHONY: tidy build build-all dev-web web install dev run run-agent docker-push-amd64 docker-push-multiarch
|
||||
.PHONY: tidy build build-all dev-web web install dev run run-agent
|
||||
|
||||
export GOENV := $(CURDIR)/go.env
|
||||
export GOPROXY ?= https://goproxy.cn,direct
|
||||
@@ -28,11 +28,3 @@ run-agent:
|
||||
|
||||
install:
|
||||
cd web && npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
docker-push-amd64:
|
||||
chmod +x scripts/docker-build-push-amd64.sh
|
||||
./scripts/docker-build-push-amd64.sh
|
||||
|
||||
docker-push-multiarch:
|
||||
chmod +x scripts/docker-build-push-multiarch.sh
|
||||
./scripts/docker-build-push-multiarch.sh
|
||||
|
||||
@@ -30,23 +30,37 @@ make run
|
||||
|
||||
管理界面:http://127.0.0.1:8529(首次启动默认端口,可在系统设置中修改)
|
||||
|
||||
## Docker(单一镜像)
|
||||
## Docker
|
||||
|
||||
镜像由 [Gitea Actions](.gitea/workflows/ci.yml) 在 push 到 `main`/`master` 或打 `v*` 标签时自动构建并推送到 Gitea Container Registry。详见 [.gitea/README.md](.gitea/README.md)。
|
||||
|
||||
```bash
|
||||
# 将 <gitea-host>、<owner> 替换为你的 Gitea 域名与仓库所有者
|
||||
IMAGE=<gitea-host>/<owner>/wormhole:latest
|
||||
|
||||
# 服务端
|
||||
docker run -d --name wormhole \
|
||||
-p 8529:8529 -p 8528:8528 -p 8081:8081 -p 8443:8443 \
|
||||
-v wormhole-data:/app/data \
|
||||
registry.rc707blog.top/rose_cat707/wormhole:latest
|
||||
"$IMAGE"
|
||||
|
||||
# Agent(同一镜像;务必加 --restart,服务端重启后 Agent 会自动重连)
|
||||
docker run -d --name wormhole-agent \
|
||||
--restart=unless-stopped \
|
||||
--network host \
|
||||
registry.rc707blog.top/rose_cat707/wormhole:latest \
|
||||
"$IMAGE" \
|
||||
agent -server <服务端IP>:8528 -key <verify_key>
|
||||
```
|
||||
|
||||
本地自行构建(与 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 wormhole:local .
|
||||
```
|
||||
|
||||
> **注意**:Agent 容器必须与 Server 分开部署。更新 Server 镜像时不要用同一 compose 重建 Agent;Agent 需 `--restart=unless-stopped`,否则收到 `SIGTERM` 后容器会保持 `Exited` 状态。
|
||||
|
||||
## 命令
|
||||
@@ -73,3 +87,15 @@ docker run -d --name wormhole-agent \
|
||||
|
||||
- 用户名:`admin`
|
||||
- 密码:`admin`
|
||||
|
||||
## 开发与 CI
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) | Go 测试 + Docker 多架构构建/推送 |
|
||||
| [`.gitea/README.md`](.gitea/README.md) | Runner、Registry、Variables 配置说明 |
|
||||
| [`.gitea/act-runner/`](.gitea/act-runner/) | act_runner 部署参考(可选) |
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 linux/amd64 镜像并推送到私有仓库(不推 Docker Hub)
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${REGISTRY:-registry.rc707blog.top/rose_cat707}"
|
||||
PLATFORM="${PLATFORM:-linux/amd64}"
|
||||
TAG="${TAG:-latest}"
|
||||
IMAGE="${IMAGE:-wormhole}"
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE}:${TAG}"
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
echo "==> 私有仓库: ${FULL_IMAGE}"
|
||||
echo "==> 平台: ${PLATFORM}"
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "error: Docker 未运行,请先启动 Docker Desktop" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> building (local cache, no Docker Hub pull)..."
|
||||
docker build \
|
||||
--platform "${PLATFORM}" \
|
||||
--pull=false \
|
||||
-f Dockerfile \
|
||||
-t "${FULL_IMAGE}" \
|
||||
.
|
||||
|
||||
echo "==> pushing to private registry..."
|
||||
docker push "${FULL_IMAGE}"
|
||||
|
||||
echo "==> done"
|
||||
echo " ${FULL_IMAGE}"
|
||||
echo ""
|
||||
echo "服务端: docker run ... ${FULL_IMAGE}"
|
||||
echo "Agent: docker run ... ${FULL_IMAGE} agent -server <ip>:8528 -key <key> [-tls]"
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建并推送多架构镜像 (amd64 + arm64)
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${REGISTRY:-registry.rc707blog.top/rose_cat707}"
|
||||
PLATFORM="${PLATFORM:-linux/amd64,linux/arm64}"
|
||||
TAG="${TAG:-latest}"
|
||||
IMAGE="${IMAGE:-wormhole}"
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "error: Docker 未运行" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker buildx inspect wormhole-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name wormhole-builder --use
|
||||
else
|
||||
docker buildx use wormhole-builder
|
||||
fi
|
||||
docker buildx inspect --bootstrap >/dev/null
|
||||
|
||||
docker buildx build \
|
||||
--platform "$PLATFORM" \
|
||||
-f Dockerfile \
|
||||
-t "${REGISTRY}/${IMAGE}:${TAG}" \
|
||||
--push \
|
||||
.
|
||||
|
||||
echo "pushed ${REGISTRY}/${IMAGE}:${TAG} (${PLATFORM})"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
exec /usr/local/bin/wormhole "$@"
|
||||
Vendored
-172
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-13
@@ -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>Wormhole</title>
|
||||
<script type="module" crossorigin src="/assets/index-6-ZkUqlB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-WASRcumQ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user