Compare commits
2
Commits
5f76bfc9d6
...
34a7823e1a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a7823e1a | ||
|
|
ea4efbe1d7 |
@@ -0,0 +1,17 @@
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.tmp
|
||||
**/.vite
|
||||
*.log
|
||||
api*.json
|
||||
session-archive
|
||||
coverage
|
||||
*.tsbuildinfo
|
||||
.claude
|
||||
.agents
|
||||
agent
|
||||
+11
-3
@@ -1,10 +1,15 @@
|
||||
# ── new-api-key-usage · 本地环境变量示例 ──────────────────────────
|
||||
# ── new-api-key-usage · 环境变量示例 ──────────────────────────
|
||||
# 复制为 .env 后填写(.env 已被 .gitignore,勿提交)
|
||||
# 开发:任意可用的 PostgreSQL,DATABASE_URL 指向即可
|
||||
# 部署:docker compose up -d --build(见 README)
|
||||
|
||||
# 本服务端口(避开 3000/3001 等常见占用)
|
||||
# 本服务端口(避开 3000/3001 等常见占用;容器内固定 18456 即可)
|
||||
PORT=18456
|
||||
|
||||
# PostgreSQL 连接串(开发 compose 默认如下)
|
||||
# PostgreSQL 连接串——数据库配置的唯一来源
|
||||
# 开发:指向你本机/已有的库(主机 127.0.0.1 或 localhost)
|
||||
# 部署:同一份即可;compose 会把主机改写为容器服务名 postgres
|
||||
# 密码含 @ : / 等字符时请 URL 编码
|
||||
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/new_api_key_usage
|
||||
|
||||
# 已登记 sk-key 的加密密钥:≥32 字节随机数,base64url。
|
||||
@@ -24,6 +29,9 @@ COOKIE_SECURE=false
|
||||
# 业务日志级别:debug | info | warning | error | fatal(访问日志另由 hono/logger 输出)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 同步引擎周期(毫秒,≥1000;默认 300000 = 5 分钟)
|
||||
# SYNC_INTERVAL_MS=300000
|
||||
|
||||
# 空库双通道:可选。用户名与密码同时存在时,首次 setup-state 静默落库;
|
||||
# 只填一个视为未配置。此后改 env 不改密;改密走管理面表单。
|
||||
# ADMIN_INIT_USERNAME=
|
||||
|
||||
@@ -34,7 +34,7 @@ Bun 工具链(选型见 `docs/design/技术选型.md`)。**Windows 上 `bun`
|
||||
| `bun run dev:engine` | 开发跑同步引擎(独立进程,读 `SYNC_*` env) |
|
||||
| `bun run dev:web` | 开发跑前端 Vite |
|
||||
| `bun run build:web` | 前端类型检查 + 产物构建 |
|
||||
| `docker compose up -d` | 起开发用 PostgreSQL |
|
||||
| `docker compose up -d --build` | 部署一键起(server / engine / caddy / pg) |
|
||||
|
||||
环境变量见 `.env.example`(复制为 `.env` 后填写)。
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# 语法:docker/dockerfile:1
|
||||
# 构建产物:/app/naku-server · /app/naku-engine · /app/drizzle · /srv(前端)
|
||||
|
||||
# ---------- 构建:依赖 + 前端 + 两个单文件 ----------
|
||||
FROM oven/bun:1.4-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY apps ./apps
|
||||
COPY packages ./packages
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# 前端静态资源
|
||||
RUN bun run --cwd apps/web build-only
|
||||
|
||||
# 单文件(linux-x64;构建发生在 Linux 容器内,无需交叉编译)
|
||||
RUN mkdir -p /out \
|
||||
&& bun build --compile --outfile /out/naku-server apps/server/src/index.ts \
|
||||
&& bun build --compile --outfile /out/naku-engine apps/engine/src/index.ts
|
||||
|
||||
# ---------- 应用运行时:两个二进制 + 迁移 SQL ----------
|
||||
FROM alpine:3.20 AS app
|
||||
WORKDIR /app
|
||||
# Bun 单文件仍需 C++ 运行时与 CA;从构建镜像拷贝,避免 apk 联网
|
||||
COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libgcc_s.so.1 /usr/lib/
|
||||
COPY --from=build /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=build /out/naku-server /out/naku-engine /app/
|
||||
COPY --from=build /src/packages/db/drizzle /app/drizzle
|
||||
ENV MIGRATIONS_FOLDER=/app/drizzle
|
||||
# 默认跑 server;compose 里 engine 覆盖 command
|
||||
CMD ["/app/naku-server"]
|
||||
|
||||
# ---------- 网关:Caddy + 前端静态资源 ----------
|
||||
FROM caddy:2-alpine AS proxy
|
||||
COPY deploy/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --from=build /src/apps/web/dist /srv
|
||||
@@ -64,13 +64,12 @@ packages/
|
||||
|
||||
## 快速开始
|
||||
|
||||
前置:Bun、Docker(开发用 PostgreSQL)。
|
||||
前置:Bun、一个可用的 PostgreSQL(本机已有或别处均可)。
|
||||
|
||||
```bash
|
||||
bun install
|
||||
docker compose up -d # 起 PostgreSQL
|
||||
cp .env.example .env # 按注释填写
|
||||
bun run db:push # 推送 schema
|
||||
cp .env.example .env # DATABASE_URL 指向你已有的 PostgreSQL
|
||||
bun run db:push # 开发期推送 schema
|
||||
bun run dev:server # 后端(默认 18456)
|
||||
bun run dev:engine # 同步引擎(另开终端)
|
||||
bun run dev:web # 前端 Vite(5175)
|
||||
@@ -80,6 +79,59 @@ bun run dev:web # 前端 Vite(5175)
|
||||
|
||||
更多命令与协作约定见 [`AGENTS.md`](AGENTS.md)。
|
||||
|
||||
## 部署
|
||||
|
||||
生产形态:**Caddy**(前端静态 + `/api` 反代)+ **server / engine 单文件**(Bun `--compile`)+ PostgreSQL;启动时自动应用 `packages/db/drizzle` 迁移。开发不依赖本 Compose——把 `DATABASE_URL` 指到任意可用 PostgreSQL 即可。
|
||||
|
||||
### 起服务
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 必改:DATABASE_URL、SK_KEY_ENCRYPTION_SECRET、NEWAPI_BASE、COOKIE_SECURE
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
- 页面:`http://<服务器>/`;接口文档:`http://<服务器>/api/docs`
|
||||
- 升级:`git pull` 后重复 `docker compose up -d --build`(改表随启动迁移)
|
||||
|
||||
### 环境变量要点
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `DATABASE_URL` | 数据库配置的**唯一来源**(用户 / 密码 / 库名都在串里)。开发指向本机库;部署同一份即可,compose 会把主机改写为容器服务名 `postgres` |
|
||||
| `SK_KEY_ENCRYPTION_SECRET` | ≥32 字节随机数(base64url)。**生成一次、入库后不可更换**——丢失则已登记 key 无法解密,与库同等备份 |
|
||||
| `NEWAPI_BASE` | 上游 NewAPI 实例根地址 |
|
||||
| `COOKIE_SECURE` | HTTPS / 域名用 `true`;纯 IP + HTTP 用 `false`(否则登录后立刻 401) |
|
||||
| `ADMIN_INIT_USERNAME` / `ADMIN_INIT_PASSWORD` | 可选;同时存在时空库首次静默建管理员 |
|
||||
|
||||
### 服务分工
|
||||
|
||||
| 服务 | 职责 | 对外 |
|
||||
|------|------|------|
|
||||
| `caddy` | 前端静态 + `/api` 反代 | 仅 80 / 443 |
|
||||
| `server` | 管理面 / 成员视图 API | 内网 |
|
||||
| `engine` | 轮询上游、去重入库 | 内网 |
|
||||
| `postgres` | 数据(卷 `pgdata`) | 内网 |
|
||||
|
||||
### HTTPS
|
||||
|
||||
把 `deploy/Caddyfile` 站点行从 `:80` 改成域名(如 `your.domain.example`),重建后 Caddy 自动签证书;同时设 `COOKIE_SECURE=true`。
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
docker compose ps # 运行状态
|
||||
docker compose logs -f engine # 看同步
|
||||
docker compose restart server # 只重启后端
|
||||
docker compose down # 全停(数据在卷里,不丢)
|
||||
```
|
||||
|
||||
### 注意
|
||||
|
||||
1. `COOKIE_SECURE` 必须与访问方式匹配:HTTP + `true` 会「登录后立刻 401」
|
||||
2. `SK_KEY_ENCRYPTION_SECRET` 更换等于已登记 key 全部作废
|
||||
3. `DATABASE_URL` 密码含 `@` `:` `/` 等字符时做 URL 编码
|
||||
|
||||
## 文档地图
|
||||
|
||||
| 文档 | 什么时候读 | 状态 |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { appLogger, setupLogging } from "@new-api-key-usage/core";
|
||||
import { createDb, createRepos } from "@new-api-key-usage/db";
|
||||
import { createDb, createRepos, runMigrations } from "@new-api-key-usage/db";
|
||||
import { loadEnv } from "./env.ts";
|
||||
import { createSyncLoop } from "./loop.ts";
|
||||
|
||||
@@ -8,6 +9,13 @@ setupLogging(env.LOG_LEVEL);
|
||||
const log = appLogger("engine");
|
||||
|
||||
const db = createDb(env.DATABASE_URL);
|
||||
|
||||
const migrationsFolder =
|
||||
process.env.MIGRATIONS_FOLDER ??
|
||||
fileURLToPath(new URL("../../../packages/db/drizzle", import.meta.url));
|
||||
await runMigrations(db, migrationsFolder);
|
||||
log.info("数据库迁移已就绪");
|
||||
|
||||
const repos = createRepos(db);
|
||||
|
||||
const loop = createSyncLoop({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createApp } from "./app.ts";
|
||||
import { loadEnv } from "./env.ts";
|
||||
import { createDb } from "@new-api-key-usage/db";
|
||||
import { createDb, runMigrations } from "@new-api-key-usage/db";
|
||||
import { appLogger, setupLogging } from "@new-api-key-usage/core";
|
||||
|
||||
const env = loadEnv(process.env);
|
||||
@@ -8,6 +9,13 @@ setupLogging(env.LOG_LEVEL);
|
||||
|
||||
const db = createDb(env.DATABASE_URL);
|
||||
|
||||
const migrationsFolder =
|
||||
process.env.MIGRATIONS_FOLDER ??
|
||||
fileURLToPath(new URL("../../../packages/db/drizzle", import.meta.url));
|
||||
|
||||
await runMigrations(db, migrationsFolder);
|
||||
appLogger("server").info("数据库迁移已就绪");
|
||||
|
||||
const app = createApp({
|
||||
db,
|
||||
skKeyEncryptionSecret: env.SK_KEY_ENCRYPTION_SECRET,
|
||||
|
||||
@@ -17,10 +17,10 @@ declare module "vue-router" {
|
||||
export const router: Router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: "/", redirect: "/admin/overview" },
|
||||
{ path: "/", redirect: "/member" },
|
||||
...adminRoutes,
|
||||
...memberRoutes,
|
||||
{ path: "/:pathMatch(.*)*", redirect: "/admin/overview" },
|
||||
{ path: "/:pathMatch(.*)*", redirect: "/member" },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# 站点地址按部署环境改:
|
||||
# 本地/内网 HTTP → :80
|
||||
# 公网 HTTPS → your.domain.example(Caddy 自动签证书)
|
||||
:80 {
|
||||
root * /srv
|
||||
|
||||
# 接口反代到 server 容器
|
||||
handle /api/* {
|
||||
reverse_proxy server:18456
|
||||
}
|
||||
|
||||
# SPA:非 /api 的路径回退 index.html
|
||||
handle {
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# 把 DATABASE_URL 的主机改写为 compose 服务名 postgres,再启动应用
|
||||
set -e
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "DATABASE_URL is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DATABASE_URL="$(echo "$DATABASE_URL" | sed -E 's#(@)[^:/]+(:[0-9]+)?(/)#\1postgres\2\3#')"
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# 从 DATABASE_URL 拆出官方镜像建库所需的 POSTGRES_*,再交回原入口
|
||||
set -e
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "DATABASE_URL is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
u="${DATABASE_URL#*://}"
|
||||
auth="${u%%@*}"
|
||||
rest="${u#*@}"
|
||||
|
||||
export POSTGRES_USER="${auth%%:*}"
|
||||
export POSTGRES_PASSWORD="${auth#*:}"
|
||||
export POSTGRES_DB="${rest#*/}"
|
||||
export POSTGRES_DB="${POSTGRES_DB%%\?*}"
|
||||
|
||||
exec docker-entrypoint.sh postgres
|
||||
+64
-8
@@ -1,20 +1,76 @@
|
||||
# 部署一键起:docker compose up -d --build
|
||||
# 配置只认 DATABASE_URL(见 .env.example);开发不依赖本文件
|
||||
# 注意:PASSWORD 含 @ : / 等字符时需 URL 编码
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: new-api-key-usage-pg
|
||||
entrypoint: ["/bin/sh", "/entrypoint-pg.sh"]
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: new_api_key_usage
|
||||
ports:
|
||||
- "5432:5432"
|
||||
DATABASE_URL: ${DATABASE_URL:?set DATABASE_URL in .env}
|
||||
volumes:
|
||||
- ./deploy/entrypoint-pg.sh:/entrypoint-pg.sh:ro
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d new_api_key_usage"]
|
||||
test: ["CMD-SHELL", "pg_isready -U $$(echo $$DATABASE_URL | sed -E 's#^[^:]+://([^:]+):.*#\\1#')"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
retries: 12
|
||||
restart: unless-stopped
|
||||
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
target: app
|
||||
entrypoint: ["/bin/sh", "/entrypoint-app.sh"]
|
||||
command: ["/app/naku-server"]
|
||||
environment:
|
||||
PORT: "18456"
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
SK_KEY_ENCRYPTION_SECRET: ${SK_KEY_ENCRYPTION_SECRET:?set SK_KEY_ENCRYPTION_SECRET in .env}
|
||||
NEWAPI_BASE: ${NEWAPI_BASE:?set NEWAPI_BASE in .env}
|
||||
COOKIE_SECURE: ${COOKIE_SECURE:-true}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
ADMIN_INIT_USERNAME: ${ADMIN_INIT_USERNAME:-}
|
||||
ADMIN_INIT_PASSWORD: ${ADMIN_INIT_PASSWORD:-}
|
||||
MIGRATIONS_FOLDER: /app/drizzle
|
||||
volumes:
|
||||
- ./deploy/entrypoint-app.sh:/entrypoint-app.sh:ro
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
engine:
|
||||
build:
|
||||
context: .
|
||||
target: app
|
||||
entrypoint: ["/bin/sh", "/entrypoint-app.sh"]
|
||||
command: ["/app/naku-engine"]
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
SK_KEY_ENCRYPTION_SECRET: ${SK_KEY_ENCRYPTION_SECRET}
|
||||
NEWAPI_BASE: ${NEWAPI_BASE}
|
||||
SYNC_INTERVAL_MS: ${SYNC_INTERVAL_MS:-300000}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
MIGRATIONS_FOLDER: /app/drizzle
|
||||
volumes:
|
||||
- ./deploy/entrypoint-app.sh:/entrypoint-app.sh:ro
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
build:
|
||||
context: .
|
||||
target: proxy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- server
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
(拓扑见下文描述,实现时以此为准)
|
||||
```
|
||||
|
||||
- **管理面**(作者专属):入口为登录页(账号密码)→ 侧边栏布局;空库时按三态分流(见《交互设计-登录与会话》)
|
||||
- **成员视图**(成员专属):入口为登录壳(令牌)→ 侧边栏布局(与管理端同构壳,菜单三项:总览 / 调用日志 / 支付参数);与管理面物理隔离(两套路由、两套凭证中间件,令牌进不了管理面,会话进不了成员视图)
|
||||
- **没有第三种入口**:无开放查询页、无游客态;任何人第一眼看到的必然是两个登录入口之一
|
||||
- **管理面**(作者专属):入口为登录页(账号密码)→ 侧边栏布局;空库时按三态分流(见《交互设计-登录与会话》);地址栏访问 `/admin`(根路径不指向此处)
|
||||
- **成员视图**(成员专属):入口为登录壳(令牌)→ 侧边栏布局(与管理端同构壳,菜单三项:总览 / 调用日志 / 支付参数);与管理面物理隔离(两套路由、两套凭证中间件,令牌进不了管理面,会话进不了成员视图);**根路径 `/` 默认进入本世界**(未知路径同样回退到此)
|
||||
- **没有第三种入口**:无开放查询页、无游客态、无分发页;根地址即成员入口。管理面的入口形态待定(当前经地址栏 `/admin` 进入)
|
||||
- 401 分流统一约定:管理面任意请求 401 → 登录页(提示「会话已过期」);成员视图任意请求 401 → 登录壳(提示「令牌无效或已停用」)——各自回各自的家,不交叉
|
||||
|
||||
## 2. 管理面导航:侧边栏
|
||||
|
||||
@@ -39,7 +39,7 @@ GET 管理面入口
|
||||
|
||||
## 4. 成员登录壳
|
||||
|
||||
- 成员视图的唯一入口:令牌输入框(单字段)+ 进入按钮
|
||||
- **唯一入口**:令牌输入框(单字段)+ 进入按钮;**根路径 `/` 默认落到本页**(已持令牌则进成员总览);管理面入口形态(是否在界面露出)待定,当前经地址栏 `/admin` 进入
|
||||
- 提交 → 服务端哈希比对 + `status` 检查:
|
||||
- 通过 → 令牌存 sessionStorage → 进入成员视图(侧边栏布局,落地「总览」,见《交互设计-成员视图》)
|
||||
- 失败/停用 → 「令牌无效或已停用」(同一提示,不泄露停用与错误的区别——成员侧无需区分,反正都是找作者)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema.ts";
|
||||
|
||||
@@ -9,5 +10,10 @@ export function createDb(connectionString: string) {
|
||||
|
||||
export type Db = ReturnType<typeof createDb>;
|
||||
|
||||
/** 启动时按 journal 应用缺失迁移(幂等);`migrationsFolder` 指向 drizzle/ 目录 */
|
||||
export async function runMigrations(db: Db, migrationsFolder: string) {
|
||||
await migrate(db, { migrationsFolder });
|
||||
}
|
||||
|
||||
export * from "./schema.ts";
|
||||
export * from "./repos.ts";
|
||||
|
||||
Reference in New Issue
Block a user