Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c74b5aa
docs: plan full PostgreSQL migration
liubing0806 Oct 21, 2026
fa0decc
feat(db): add asynchronous PostgreSQL persistence and baseline
liubing0806 Oct 21, 2026
016efae
test(cluster): expose reconnect race during asynchronous node edits
liubing0806 Oct 21, 2026
3225b07
feat(db): migrate repositories and account stores to PostgreSQL
liubing0806 Oct 21, 2026
893cb3c
feat(db): migrate usage logging statistics and billing queries
liubing0806 Oct 21, 2026
86f0335
test(cluster): require host discovery to await platform persistence
liubing0806 Oct 21, 2026
440cb47
test(logging): require maintenance to drain scheduled cleanup
liubing0806 Oct 21, 2026
052dcd9
test(keys): cover bigint request quotas beyond the safe integer range
liubing0806 Oct 21, 2026
aa11209
test(quota): expose bigint counter concatenation after PostgreSQL mig…
liubing0806 Oct 21, 2026
d2c2b8a
test(migration): preserve fractional scheduling weights from SQLite
liubing0806 Oct 21, 2026
99f32e1
test(db): expose precision loss in PostgreSQL counters and summaries
liubing0806 Oct 21, 2026
65c545a
test(panel): require async mutations to remain handled
liubing0806 Oct 21, 2026
e712c0b
test(stores): preserve bigint values through usage preprocessing
liubing0806 Oct 21, 2026
720a047
feat(migration): import SQLite snapshots and convert legacy backups
liubing0806 Oct 21, 2026
1a5a752
feat(backup): use PostgreSQL snapshots and guarded restoration
liubing0806 Oct 21, 2026
c2e56a2
feat(deploy): configure PostgreSQL operations and database overview
liubing0806 Oct 21, 2026
f41e589
test: cover pending scheduled result persistence
liubing0806 Oct 21, 2026
6cb132b
fix(db): preserve integer precision across PostgreSQL usage counters
liubing0806 Oct 21, 2026
777d2e2
test(migration): retain fractional weights in legacy runtime rows
liubing0806 Oct 21, 2026
3fbdc4b
test(pg): isolate integration tests on PostgreSQL and preserve restar…
liubing0806 Oct 21, 2026
9feedbb
test(backup): cover scheduler shutdown race
liubing0806 Oct 21, 2026
38f6db6
feat(runtime): await PostgreSQL persistence throughout application li…
liubing0806 Oct 21, 2026
fad3e77
test(migration): retain fractional API endpoint priorities
liubing0806 Oct 21, 2026
71ea797
fix(migration): preserve supported fractional scheduling values
liubing0806 Oct 21, 2026
4ad444d
fix(backup): drain scheduler checks before shutdown
liubing0806 Oct 21, 2026
6aa877d
fix(backup): distinguish maintenance and isolate unrelated panel scen…
liubing0806 Oct 21, 2026
3054bfa
test(pg): isolate restores by schema without database checkpoints
liubing0806 Oct 21, 2026
cf9462d
docs(pg): record migration validation and baseline comparisons
liubing0806 Oct 21, 2026
d755e29
test(logs): reproduce PostgreSQL NUL persistence failures
liubing0806 Oct 21, 2026
fcd568c
fix(logs): escape NUL characters before PostgreSQL writes
liubing0806 Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ HOST=0.0.0.0
# PUBLIC_BASE_URL=https://example.com
# PUBLIC_HOST=127.0.0.1
# GITHUB_TOKEN= # 可选。面板查 GitHub Release 提高限额;也可用 VM2API_GITHUB_TOKEN

# PostgreSQL 17. The installer fills a random password for a new local database.
# External PG: set VM2API_DATABASE_URL and clear COMPOSE_PROFILES.
COMPOSE_PROFILES=postgres
VM2API_POSTGRES_PORT=55432
VM2API_POSTGRES_PASSWORD=
VM2API_DATABASE_URL=
# Preserve VM2API_DB_SECRET when importing an existing SQLite database.
30 changes: 28 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ permissions:
jobs:
unit:
runs-on: ubuntu-latest
timeout-minutes: 8
timeout-minutes: 20
services:
postgres:
image: postgres:17-bookworm
env:
POSTGRES_USER: vm2api
POSTGRES_PASSWORD: vm2api-ci-test
POSTGRES_DB: vm2api_test
ports: [5432:5432]
options: >-
--health-cmd "pg_isready -U vm2api -d vm2api_test"
--health-interval 5s --health-timeout 3s --health-retries 12
env:
VM2API_TEST_DATABASE_URL: postgresql://vm2api:vm2api-ci-test@127.0.0.1:5432/vm2api_test
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
Expand All @@ -30,8 +43,18 @@ jobs:
go-version: '1.25.x'
cache-dependency-path: worker/go.sum
- run: npm ci
- name: PostgreSQL 17 backup clients
run: |
sudo install -d /usr/share/postgresql-common/pgdg
curl --fail --silent --show-error https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc >/dev/null
. /etc/os-release
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $VERSION_CODENAME-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list >/dev/null
sudo apt-get update
sudo apt-get install -y postgresql-client-17
echo /usr/lib/postgresql/17/bin >> "$GITHUB_PATH"
/usr/lib/postgresql/17/bin/pg_dump --version
- name: panel user and onboarding regression tests
run: node --test test/unit/panel-users.test.mjs test/unit/onboarding.test.mjs test/e2e/panel-users.e2e.test.mjs
run: node --import ./test/pg-setup.mjs --test test/unit/panel-users.test.mjs test/unit/onboarding.test.mjs test/e2e/panel-users.e2e.test.mjs
- name: format-check
run: npx biome format
- name: syntax-check Node source and scripts
Expand All @@ -42,8 +65,11 @@ jobs:
npm run fetch:payload
npm run build:egress
- run: npm run test:unit
- run: npm run test:e2e
- name: ARM64 host preparation and rollback tests
run: python3 -m unittest discover -s test/deploy -v
- name: PostgreSQL application role permissions
run: python3 test/deploy/smoke_postgres_role.py

arm64:
runs-on: ubuntu-24.04-arm
Expand Down
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ FROM node:22-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates iptables iproute2 python3 \
&& rm -rf /var/lib/apt/lists/*
# Debian Bookworm defaults to PG15; backups require the PG17 client.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& install -d /usr/share/postgresql-common/pgdg \
&& curl --fail --silent --show-error -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc \
&& echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client-17 \
&& pg_dump --version | grep -E ' 17\.' \
&& pg_restore --version | grep -E ' 17\.' \
&& rm -rf /var/lib/apt/lists/*
COPY --from=docker:27-cli /usr/local/bin/docker /usr/local/bin/docker
WORKDIR /opt/vm2api
COPY package.json package-lock.json ./
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ chmod 600 .env
docker compose pull && docker compose up -d
```

数据库使用 PostgreSQL 17。新安装由安装脚本生成内置数据库密码;手工 Compose 安装先配置 `.env` 中的 `VM2API_POSTGRES_PASSWORD`。已有 SQLite 安装须先离线导入,保留原 `VM2API_DB_SECRET`。外部数据库、备份恢复和验证步骤见 [PostgreSQL 部署说明](docs/deploy/postgresql.md)。


### 3. 访问与调用

服务启动后,系统暴露统一服务端口(默认 `8787`):
Expand Down
11 changes: 11 additions & 0 deletions deploy/Dockerfile.arm64-control
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ FROM --platform=linux/arm64 ${NODE_IMAGE}
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates iptables iproute2 python3 \
&& rm -rf /var/lib/apt/lists/*
# Debian Bookworm defaults to PG15; backups require the PG17 client.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& install -d /usr/share/postgresql-common/pgdg \
&& curl --fail --silent --show-error -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc \
&& echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client-17 \
&& pg_dump --version | grep -E ' 17\.' \
&& pg_restore --version | grep -E ' 17\.' \
&& rm -rf /var/lib/apt/lists/*
COPY --from=dockercli /usr/local/bin/docker /usr/local/bin/docker
WORKDIR /opt/vm2api
COPY package.json package-lock.json ./
Expand Down
7 changes: 6 additions & 1 deletion deploy/init-arm64-env.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ def main() -> int:

root = Path(__file__).resolve().parents[1]
env_path = root / ".env"
postgres_password = secrets.token_hex(32)
values = {
"VM2API_ADMIN_USER": "admin",
"VM2API_ADMIN_PASSWORD": secrets.token_urlsafe(24),
"VM2API_API_KEY": secrets.token_hex(32),
"VM2API_DB_SECRET": secrets.token_hex(32),
"COMPOSE_PROFILES": "postgres",
"VM2API_POSTGRES_PORT": "55432",
"VM2API_POSTGRES_PASSWORD": postgres_password,
"VM2API_DATABASE_URL": f"postgresql://vm2api:{postgres_password}@127.0.0.1:55432/vm2api",
"VM2API_CONTAINER_NAME": args.container_name,
"VM2API_BIND_HOST": args.bind_host,
"PORT": str(args.port),
Expand All @@ -72,7 +77,7 @@ def main() -> int:
fd = os.open(env_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
print("Existing .env preserved. Review it locally before starting this deployment.")
missing = [key for key in ("COMPOSE_FILE", "VM2API_HOST_ROOT") if not env_has(env_path, key)]
missing = [key for key in ("COMPOSE_FILE", "VM2API_HOST_ROOT", "VM2API_DATABASE_URL") if not env_has(env_path, key)]
if missing:
print(f"Add to .env: COMPOSE_FILE={COMPOSE_FILE} and VM2API_HOST_ROOT={root} "
f"(missing: {', '.join(missing)}).")
Expand Down
40 changes: 34 additions & 6 deletions deploy/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,19 @@ env_set() {
local name="$1"
local value="$2"
local envf="${INSTALL_DIR}/.env"
if grep -qE "^${name}=" "$envf" 2>/dev/null; then
sed -i "s|^${name}=.*|${name}=${value}|" "$envf"
else
printf '%s=%s\n' "$name" "$value" >>"$envf"
fi
VM2API_ENV_KEY="$name" VM2API_ENV_VALUE="$value" VM2API_ENV_FILE="$envf" python3 - <<'PYENV'
import os
from pathlib import Path
p = Path(os.environ["VM2API_ENV_FILE"])
key = os.environ["VM2API_ENV_KEY"] + "="
value = key + os.environ["VM2API_ENV_VALUE"] + "\n"
lines = p.read_text().splitlines(keepends=True) if p.exists() else []
if any(line.startswith(key) for line in lines):
p.write_text("".join(value if line.startswith(key) else line for line in lines))
else:
text = "".join(lines)
p.write_text(text + ("\n" if text and not text.endswith("\n") else "") + value)
PYENV
}

env_set_if_empty() {
Expand Down Expand Up @@ -380,6 +388,16 @@ ensure_env() {
cp "$example" "$envf"
fi
chmod 600 "$envf" || true
if [ -z "$(env_get VM2API_DATABASE_URL)" ] && [ -z "$(env_get KIN_DATABASE_URL)" ]; then
if [ -f "${INSTALL_DIR}/data/kin.db" ]; then
err "SQLite 数据尚未迁移。保留运行容器,按 docs/deploy/postgresql.md 完成离线导入后再升级。"
return 1
fi
env_set_if_empty COMPOSE_PROFILES postgres || true
env_set_if_empty VM2API_POSTGRES_PORT 55432 || true
env_set_if_empty VM2API_POSTGRES_PASSWORD "$(gen_secret)" || true
env_set_if_empty VM2API_DATABASE_URL "postgresql://vm2api:$(env_get VM2API_POSTGRES_PASSWORD)@127.0.0.1:$(env_get VM2API_POSTGRES_PORT)/vm2api" || true
fi
env_set_if_empty VM2API_ADMIN_USER "$DEFAULT_ADMIN_USER" || true
if env_set_if_empty VM2API_ADMIN_PASSWORD "$DEFAULT_ADMIN_PASSWORD"; then
WROTE_DEFAULT_PASSWORD=1
Expand Down Expand Up @@ -519,9 +537,14 @@ fetch_file() {
# 镜像安装:只落部署文件,不 clone 仓库,不在本机构建。
fetch_release_files() {
local tag="$1"
mkdir -p "${INSTALL_DIR}/deploy"
mkdir -p "${INSTALL_DIR}/deploy" "${INSTALL_DIR}/docs/deploy"
info "下载部署文件 ${tag} → ${INSTALL_DIR}"
fetch_file "$tag" docker-compose.yml "${INSTALL_DIR}/docker-compose.yml"
if grep -q 'vm2api-postgres-entrypoint' "${INSTALL_DIR}/docker-compose.yml"; then
fetch_file "$tag" deploy/postgres-entrypoint.sh "${INSTALL_DIR}/deploy/postgres-entrypoint.sh"
fetch_file "$tag" deploy/postgres-init.sh "${INSTALL_DIR}/deploy/postgres-init.sh"
fetch_file "$tag" docs/deploy/postgresql.md "${INSTALL_DIR}/docs/deploy/postgresql.md"
fi
fetch_file "$tag" .env.example "${INSTALL_DIR}/.env.example"
fetch_file "$tag" VERSION "${INSTALL_DIR}/VERSION"
fetch_file "$tag" CHANGELOG.md "${INSTALL_DIR}/CHANGELOG.md" 1
Expand Down Expand Up @@ -631,6 +654,11 @@ start_stack() {
fi
cd "${INSTALL_DIR}"
auto_sync_wrap="${KIN_AUTO_SYNC_WRAP:-0}"
# An existing SQLite installation must pass the read-only import check before replacement.
if [ -f data/kin.db ] || [ -f .local/arm64/data/kin.db ]; then
if [ "$mode" = image ]; then compose pull vm2api; else compose build vm2api; fi
compose run --rm --no-deps -T --entrypoint node vm2api scripts/check-postgres-upgrade.mjs || return 1
fi
if [ "$mode" = image ]; then
info "docker compose pull && up -d(拉预构建镜像,本机不构建)"
if ! compose pull; then
Expand Down
10 changes: 10 additions & 0 deletions deploy/postgres-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Bootstrap postgres never shares the application's password. No password is logged or passed in argv.
if [ -z "${VM2API_POSTGRES_PASSWORD:-}" ]; then
echo 'VM2API_POSTGRES_PASSWORD is required for the application role.' >&2
exit 1
fi
POSTGRES_PASSWORD="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
export POSTGRES_PASSWORD
exec /usr/local/bin/docker-entrypoint.sh "$@"
8 changes: 8 additions & 0 deletions deploy/postgres-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Official entrypoint sources this only when initializing an empty data directory.
psql --no-password --set=ON_ERROR_STOP=1 --username=postgres --dbname=postgres <<'SQL'
\getenv app_password VM2API_POSTGRES_PASSWORD
CREATE ROLE vm2api LOGIN PASSWORD :'app_password' NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
CREATE DATABASE vm2api OWNER vm2api;
SQL
29 changes: 29 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ services:
container_name: ${VM2API_CONTAINER_NAME:-vm2api}
restart: unless-stopped
init: true
depends_on:
postgres:
condition: service_healthy
required: false
network_mode: host
cap_add:
- NET_ADMIN
Expand All @@ -17,6 +21,7 @@ services:
environment:
KIN_PROJECT_ROOT: /opt/vm2api
KIN_DATA_DIR: /opt/vm2api/data
VM2API_DATABASE_URL: ${VM2API_DATABASE_URL:-${KIN_DATABASE_URL:-postgresql://vm2api:${VM2API_POSTGRES_PASSWORD:-}@127.0.0.1:${VM2API_POSTGRES_PORT:-55432}/vm2api}}
KIN_KERNEL_BIN: /opt/vm2api/bin/kin-kernel
KIN_EGRESS_BIN: /opt/vm2api/bin/kin-egress
KIN_WORKER_BIN: /opt/vm2api/bin/kin-worker
Expand All @@ -36,3 +41,27 @@ services:
- ./bin:/opt/vm2api/bin
- ./share:/opt/vm2api/share
- ./src/config:/opt/vm2api/src/config

postgres:
image: postgres:17-bookworm
profiles: [postgres]
restart: unless-stopped
entrypoint: [/bin/bash, /usr/local/bin/vm2api-postgres-entrypoint]
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
VM2API_POSTGRES_PASSWORD: ${VM2API_POSTGRES_PASSWORD:-}
ports:
- "127.0.0.1:${VM2API_POSTGRES_PORT:-55432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./deploy/postgres-entrypoint.sh:/usr/local/bin/vm2api-postgres-entrypoint:ro
- ./deploy/postgres-init.sh:/docker-entrypoint-initdb.d/10-vm2api.sh:ro
healthcheck:
test: [CMD-SHELL, "pg_isready -U vm2api -d vm2api && psql -U vm2api -d vm2api -Atqc 'SELECT 1' >/dev/null"]
interval: 5s
timeout: 3s
retries: 12
volumes:
postgres-data:
2 changes: 1 addition & 1 deletion docs/OAUTH.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OAuth 凭证生命周期

活票只写在槽位 `credentials.json`。换票由控制面 host `RefreshIfNeeded` 经槽 SOCKS5 完成;`vm.json`、SQLite 只存脱敏元数据。Go `kin-worker` 不再做 hop / refresh。
活票只写在槽位 `credentials.json`。换票由控制面 host `RefreshIfNeeded` 经槽 SOCKS5 完成;`vm.json`、PostgreSQL 只存脱敏元数据。Go `kin-worker` 不再做 hop / refresh。

```text
sessionKey(默认 Setup Token)或授权码
Expand Down
Loading
Loading