Skip to content
This repository was archived by the owner on Oct 31, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
DATABASE_URL=postgresql://user:password@localhost:5432/wikitdb
# 定时任务密钥(至少 16 字符)。Vercel Cron 和 GitHub Actions 用它调用 /api/cron/*。
# 生产环境必须设置,否则调度接口一律 401。
CRON_SECRET=

# 可选:Upstash Redis。配上之后全局限速在所有无服务器实例之间共享。
# 不配则只在单个实例内计数。
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=

# 无服务器函数不要用「直连 + 大连接池」。
# Neon / Supabase 请用带 pgbouncer 的 pooler 连接串,并带上 connection_limit=1。
# 在 Vercel 上如果漏了 connection_limit,运行时会自动补上。
DATABASE_URL=postgresql://user:password@localhost:5432/wikitdb?connection_limit=1

JWT_SECRET=replace-with-at-least-32-random-bytes
SITE_ORIGIN=https://example.com
TRUST_PROXY=false
Expand Down
126 changes: 126 additions & 0 deletions .github/workflows/serverless-jobs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Serverless jobs

# 替代原来的常驻 worker / systemd。
# 爬虫和归属同步优先在 Actions 里跑(单次最长约 8 分钟,游标写在数据库里)。
# 没配 DATABASE_URL 时,改为请求已部署站点的 /api/cron/*(需要 SITE_ORIGIN 与 CRON_SECRET)。
# 每天 12:00(Asia/Shanghai,04:00 UTC)用 wikit CLI 打备份包并上传 artifact。

on:
schedule:
- cron: '*/10 * * * *'
- cron: '0 4 * * *'
workflow_dispatch:
inputs:
job:
description: Which job to run
required: true
default: crawl
type: choice
options:
- crawl
- attribution
- backup

permissions:
contents: read

jobs:
crawl:
if: >-
(github.event_name == 'workflow_dispatch' && inputs.job == 'crawl') ||
(github.event_name == 'schedule' && github.event.schedule == '*/10 * * * *')
runs-on: ubuntu-latest
timeout-minutes: 15
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
WIKIDOT_BOT_USER: ${{ secrets.WIKIDOT_BOT_USER }}
WIKIDOT_BOT_PASS: ${{ secrets.WIKIDOT_BOT_PASS }}
SITE_ORIGIN: ${{ vars.SITE_ORIGIN }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Crawl with database
if: env.DATABASE_URL != ''
run: |
npm ci
npx prisma generate
node auto-crawler.js --for 480
- name: Crawl via deployed cron endpoint
if: env.DATABASE_URL == '' && env.SITE_ORIGIN != '' && env.CRON_SECRET != ''
run: |
curl -fsS -H "Authorization: Bearer $CRON_SECRET" "$SITE_ORIGIN/api/cron/crawl"
- name: Skip notice
if: env.DATABASE_URL == '' && (env.SITE_ORIGIN == '' || env.CRON_SECRET == '')
run: echo "未配置 DATABASE_URL,也没有 SITE_ORIGIN + CRON_SECRET,跳过爬虫。在仓库 Secrets / Variables 里补上即可。"

attribution:
if: >-
(github.event_name == 'workflow_dispatch' && inputs.job == 'attribution') ||
(github.event_name == 'schedule' && github.event.schedule == '*/10 * * * *')
runs-on: ubuntu-latest
timeout-minutes: 15
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SITE_ORIGIN: ${{ vars.SITE_ORIGIN }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Attribution with database
if: env.DATABASE_URL != ''
run: |
npm ci
npx prisma generate
node auto-attribution.js --for 480
- name: Attribution via deployed cron endpoint
if: env.DATABASE_URL == '' && env.SITE_ORIGIN != '' && env.CRON_SECRET != ''
run: |
curl -fsS -H "Authorization: Bearer $CRON_SECRET" "$SITE_ORIGIN/api/cron/attribution"
- name: Skip notice
if: env.DATABASE_URL == '' && (env.SITE_ORIGIN == '' || env.CRON_SECRET == '')
run: echo "未配置归属同步所需的 DATABASE_URL 或 SITE_ORIGIN + CRON_SECRET,跳过。"

backup:
if: >-
(github.event_name == 'workflow_dispatch' && inputs.job == 'backup') ||
(github.event_name == 'schedule' && github.event.schedule == '0 4 * * *')
runs-on: ubuntu-latest
timeout-minutes: 360
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Skip notice
if: env.DATABASE_URL == ''
run: echo "备份需要 Secrets.DATABASE_URL 来读取站点列表。未配置,跳过。"
- name: Install wikit CLI
if: env.DATABASE_URL != ''
run: |
curl -fsSL -o /tmp/wikit "https://github.com/WikitTeam/wikit/releases/download/v0.2.3/wikit-linux-amd64"
chmod +x /tmp/wikit
sudo mv /tmp/wikit /usr/local/bin/wikit
- name: Run backup
if: env.DATABASE_URL != ''
run: |
npm ci
npx prisma generate
node auto-crawler.js --backup
- uses: actions/upload-artifact@v4
if: env.DATABASE_URL != ''
with:
name: wikit-backup
path: wikit_data
if-no-files-found: ignore
retention-days: 14

53 changes: 34 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ WikitDB 是一个面向 Wikidot 社区生态的**非营利性同人项目**。
| 论坛渲染 | FTML (Wikidot 文本标记语言) WASM 内核 |
| HTML 消毒 | sanitize-html |
| 鉴权 | JWT (HS256) + bcrypt + httpOnly Cookie |
| 定时任务 | node-cron |
| 定时任务 | GitHub Actions(`.github/workflows/serverless-jobs.yml`)或 Vercel Cron |
| 邮件 | nodemailer |
| 缓存 | Upstash Redis (可选) |
| 语言 | JavaScript (逐步迁移至 TypeScript) |
Expand Down Expand Up @@ -86,39 +86,52 @@ npm run dev

默认监听 `http://localhost:3000`。

## 生产部署
## 生产部署(无服务器)

### 构建
站点本身是 Next.js,部署到 Vercel 后每个页面和 API 都是按需启动的函数,**不要再跑 `npm start` + PM2,也不要再跑常驻 worker**。

```bash
npm run build
npm start
```
状态都在 Postgres 里:爬虫游标(`crawler:cursor`)、运行状态(`crawler:status`)、任务日志(`logs:crawler`)、管理后台改过的站点列表(`site:config`)。函数结束不影响进度。

### 备份 Worker(可选)
### 1. 部署 Web

如需启用 Wikidot 站点定时备份,需将 wikit CLI 安装到服务器 PATH 中,并以常驻进程方式运行 worker:
把仓库接到 Vercel(或任何支持 Next.js 的无服务器平台),配上:

```bash
npm run worker
```
- `DATABASE_URL` — 用连接池地址(Neon / Supabase pooler),带 `connection_limit=1`
- `JWT_SECRET`、`SITE_ORIGIN`
- `CRON_SECRET` — 至少 16 字符,调度接口靠它鉴权
- `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` — 可选,配了之后限速跨实例共享

`vercel.json` 里有两条每天一次的 Cron(Hobby 计划不允许更密)。真正的推进频率靠下面的 GitHub Actions。

### 2. 调度爬虫和归属同步

Worker 会在每天 12:00(Asia/Shanghai 时区)对所有 `wikitdb.config.js` 中声明的站点执行备份,归档写入工作目录下的 `wikit_data/`。任务限定为单进程串行执行,且服务端始终传递 `--keep-removed`,确保从 sitemap 消失的页面仍保留在归档中。
`.github/workflows/serverless-jobs.yml` 每 10 分钟跑一片(最长约 8 分钟),游标没跑完下次接着跑。爬虫一轮结束后冷却 3 小时,归属同步冷却 30 分钟。

### 进程管理
在仓库 Secrets 里设置 `DATABASE_URL`(以及可选的 `WIKIDOT_BOT_USER` / `WIKIDOT_BOT_PASS`)。没有数据库密钥时,可以改成调用线上接口:仓库 Variables 设 `SITE_ORIGIN`,Secrets 设 `CRON_SECRET`。

推荐使用 PM2 守护 `npm start` 与 `npm run worker` 两个进程:
本地手动跑一整轮或一片:

```bash
pm2 start "npm start" --name wikitdb
pm2 start "npm run worker" --name wikitdb-worker
pm2 save
node auto-crawler.js # 把当前一轮爬完后退出
node auto-crawler.js --for 480 # 最多 480 秒
node auto-attribution.js --slice
```

`npm run worker` 仍指向爬虫脚本,但**跑完就退出**,不要再用 PM2 守护它。

### 3. 全站备份

`wikit` CLI 要二进制和磁盘,不能塞进函数。每天 12:00(北京时间)由同一条 Actions 工作流下载 `wikit`、执行 `node auto-crawler.js --backup`,把 `wikit_data/` 收成 artifact。

### 4. 仍然想自建虚拟机

可以 `npm run build && npm start`,但调度请用系统 cron 调用上面的一次性命令,不要再装 `deploy/wikitdb-attribution.service` 那种常驻进程。

## 项目结构

```
WikitDB-Server/
├── jobs/ # 无服务器任务切片(爬虫 / 归属 / 备份)
├── pages/ # Next.js Pages Router
│ ├── api/ # API 路由
│ ├── tools/ # 工具页面
Expand Down Expand Up @@ -148,7 +161,9 @@ WikitDB-Server/
| `npm run typecheck` | TypeScript 类型检查 (`tsc --noEmit`) |
| `npm test` | 运行单元测试 (`node --test tests/*.test.js`) |
| `npm run test:ftml` | FTML 运行时测试 |
| `npm run worker` | 启动备份定时任务 worker |
| `npm run worker` / `npm run job:crawl` | 跑一轮爬虫后退出(不再常驻) |
| `npm run job:attribution` | 跑一轮归属同步后退出 |
| `npm run job:backup` | 调用本机 wikit CLI 做备份 |
| `npm run prisma:generate` | 生成 Prisma Client |
| `npm run prisma:push` | 同步 schema 到数据库 |
| `npm run prisma:studio` | 打开 Prisma Studio 可视化管理数据库 |
Expand Down
Loading
Loading