VIP 专线服务器 — 部署运维手册

从零开始在新服务器上完成完整部署的操作指南

目录 1. 前置条件与服务器要求 2. 系统架构总览 3. 基础环境安装 4. 目录结构创建 5. Nginx 配置与 secure_link 6. SSL 证书申请 7. videoop Pipeline 部署 8. R2 数据同步 9. Cloudflare Worker 配置激活 10. 用户 VIP 状态管理 11. 端到端验证 12. 日常运维 13. 安全加固 14. 故障排查 15. 完整环境变量参考 16. 关键文件一览

1 前置条件与服务器要求

服务器最低配置

资源最低要求推荐配置说明
CPU8 核16+ 核FFmpeg 转码 + CLIP 推理需要算力
内存16 GB32 GBCLIP ViT-B-32 模型加载约需 2GB
系统盘50 GB SSD100 GB SSD操作系统 + 应用程序
数据盘2 TB SSD4+ TB SSD视频 + 预览 + 缩略图存储
带宽100 Mbps1 Gbps CN 优化中国大陆优化线路是核心
操作系统Ubuntu 22.04 / 24.04 LTS本手册基于 Ubuntu 24.04

你需要准备的信息

信息用途获取位置
服务器 IPSSH 登录 / DNS 解析服务器供应商面板
SSH 端口 / 密码远程管理服务器供应商邮件
域名VIP 线路访问地址vip.mhana.top
Cloudflare API TokenWorker 部署Cloudflare Dashboard → Profile → API Tokens
R2 Access Key数据同步 + Pipeline 上传Cloudflare Dashboard → R2 → API Tokens
VIP HMAC Secret签名密钥(Worker + Nginx 共享)自行生成:openssl rand -hex 32

2 系统架构总览

┌─────────────────────────────────────────────────────────────────┐
│                     用户浏览器 (前端 Next.js)                      │
│  ┌──────────────────────┐    ┌───────────────────────────────┐  │
│  │ 普通线路              │    │ VIP 线路 (直连)               │  │
│  │ API → CF Worker       │    │ 视频/图片 → VIP Server        │  │
│  │ 文件 → CF Worker → R2 │    │ API → CF Worker (仅元数据)    │  │
│  └──────────┬───────────┘    └──────────────┬────────────────┘  │
└─────────────┼───────────────────────────────┼──────────────────┘
              │                               │
              ▼                               ▼
┌─────────────────────────┐    ┌───────────────────────────────┐
│  Cloudflare Worker      │    │  VIP Server (本服务器)         │
│  ┌───────────────────┐  │    │  ┌───────────────────────────┐│
│  │ Hono API Server   │  │    │  │ Nginx (secure_link)       ││
│  │ - Feed API        │  │    │  │ - /media/videos/*.dat     ││
│  │ - Auth API        │  │    │  │ - /media/thumbs/*.jpg     ││
│  │ - VIP Quota API   │  │    │  │ - /media/previews/*.webp  ││
│  │ - signVipUrl()    │  │    │  └───────────────────────────┘│
│  └───────┬───────────┘  │    │  ┌───────────────────────────┐│
│          │              │    │  │ videoop Pipeline (PM2)    ││
│  ┌───────▼───────────┐  │    │  │ Telegram → FFmpeg → CLIP  ││
│  │ D1 / R2 / KV      │  │    │  │ → R2 Upload + Local Save  ││
│  └───────────────────┘  │    │  └───────────────────────────┘│
└─────────────────────────┘    └───────────────────────────────┘

数据流向

普通用户请求路径:

浏览器 → Cloudflare Worker (API + 文件代理) → R2 存储 → 回传用户

VIP 用户请求路径:

浏览器 → Cloudflare Worker (仅 JSON 元数据, ~5KB)
浏览器 → VIP Server Nginx (视频/图片直连, CN 优化专线)

Pipeline 处理路径:

Telegram 频道 → 下载 → FFmpeg 转码 → 缩略图/预览 → CLIP 嵌入 → NudeNet 检测
  → 上传到 R2 (普通线路)
  → 保存到本地 /data/videos/ (VIP 线路)
  → 同步元数据到 D1

3 基础环境安装

# SSH 到新服务器
ssh -p {SSH_PORT} root@{SERVER_IP}

# 更新系统包
apt-get update && apt-get upgrade -y

# 安装基础依赖
apt-get install -y nginx python3 python3-pip python3-venv \
    ffmpeg curl git rclone certbot python3-certbot-nginx

# 安装 Node.js 20 (PM2 需要)
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs

# 安装 PM2 进程管理器
npm install -g pm2

# 设置 PM2 开机自启
pm2 startup

# 验证安装
nginx -v            # nginx/1.24+
python3 --version   # Python 3.12+
ffmpeg -version     # FFmpeg 6+
node -v             # v20+
pm2 -v              # 6+
rclone version      # 1.60+

4 目录结构创建

# 创建视频存储目录
mkdir -p /data/videos/{videos,previews,thumbs}

# 创建 Web 文档目录
mkdir -p /var/www/html

# 验证
tree /data/videos/
# /data/videos/
# ├── videos/     ← MP4 视频文件 (.dat 扩展名)
# ├── previews/   ← WebP 预览动图
# └── thumbs/     ← JPEG 缩略图

关于 .dat 扩展名:视频文件使用 .dat 而非 .mp4,这是有意为之的混淆。Nginx 通过 types 指令将 .dat 映射为 video/mp4,浏览器可以正常播放。

5 Nginx 配置与 secure_link

5.1 生成 HMAC 密钥

# 生成 64 字符的 hex 随机密钥
openssl rand -hex 32
# 输出示例: 201047bc9d1da378f6cc2c20766d7059d3b14c69fcf68800a7d2fe2051cc37a0
# ⚠️ 这个密钥必须与 Cloudflare Worker 的 VIP_HMAC_SECRET 完全一致

5.2 完整 Nginx 配置

文件路径:/etc/nginx/sites-available/vip-video

server {
    server_name {DOMAIN} {SERVER_IP};

    root /var/www/html;
    index index.html;

    # ─── CORS 跨域(前端直连需要) ───
    add_header Access-Control-Allow-Origin "*" always;
    add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Range" always;
    add_header Access-Control-Expose-Headers "Content-Range, Content-Length" always;

    # ─── OPTIONS 预检请求 ───
    location = /options {
        return 204;
    }

    # ─── 媒体文件(核心:secure_link 签名保护) ───
    location /media/ {
        alias /data/videos/;

        # secure_link 参数:URL 中的 sig 和 expires
        secure_link $arg_sig,$arg_expires;

        # 签名验证公式:MD5("{expires}{uri} {secret}")
        # 必须与 Worker 端 signVipUrl() 的算法完全一致
        secure_link_md5 "$secure_link_expires$uri {VIP_HMAC_SECRET}";

        # 无效签名 → 403
        if ($secure_link = "") {
            return 403;
        }

        # 链接已过期 → 410
        if ($secure_link = "0") {
            return 410;
        }

        # MIME 类型映射
        types {
            video/mp4  dat mp4;    # .dat → video/mp4
            image/webp webp;       # .webp → image/webp
            image/jpeg jpg jpeg;   # .jpg → image/jpeg
        }

        # 支持 Range 请求(视频拖拽进度条)
        add_header Accept-Ranges bytes;
        add_header Cache-Control "public, max-age=604800, immutable";
        add_header Access-Control-Allow-Origin "*" always;
    }

    # ─── 健康检查端点 ───
    location /health {
        return 200 "ok";
        add_header Content-Type text/plain;
    }

    # ─── SSL(certbot 会自动添加以下内容) ───
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/{DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{DOMAIN}/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

# HTTP → HTTPS 强制跳转
server {
    if ($host = {DOMAIN}) {
        return 301 https://$host$request_uri;
    }
    listen 80;
    server_name {DOMAIN} {SERVER_IP};
    return 404;
}

5.3 签名算法详解

Worker(TypeScript)和 Nginx 使用完全相同的签名算法,两端必须一致:

// ─── Worker 端签名生成 (TypeScript) ───
// 文件: videoworker/src/services/sign.ts

export async function signVipUrl(
    r2Key: string,      // 例: "videos/5a7e08c3.dat"
    vipServerUrl: string, // 例: "https://vip.mhana.top"
    vipSecret: string,    // HMAC 密钥
    expiresInSeconds = 3600,
): Promise<string> {
    // 1. 计算过期时间戳(UNIX 秒)
    const expires = Math.floor(Date.now() / 1000) + expiresInSeconds;

    // 2. 构建 URI(与 Nginx location 匹配)
    const uri = `/media/${r2Key}`;

    // 3. 拼接签名原文:"{expires}{uri} {secret}"
    //    注意:uri 和 secret 之间有一个空格
    const raw = `${expires}${uri} ${vipSecret}`;

    // 4. 计算 MD5
    const hashBuffer = await crypto.subtle.digest("MD5", new TextEncoder().encode(raw));

    // 5. Base64URL 编码(Nginx secure_link 兼容格式)
    const sig = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
        .replace(/\+/g, "-")   // + → -
        .replace(/\//g, "_")   // / → _
        .replace(/=+$/, "");   // 移除尾部 =

    // 6. 返回完整的签名 URL
    return `${vipServerUrl}${uri}?sig=${sig}&expires=${expires}`;
}

// ─── 输出示例 ───
// https://vip.mhana.top/media/videos/5a7e08c3.dat?sig=Sb9EN0DdQcG_8gAQnWtwCQ&expires=1775398917
# ─── Nginx 端签名验证 ───
# secure_link_md5 的格式必须与 Worker 端的 raw 变量完全对应
#
# Worker:  raw = `${expires}${uri} ${secret}`
# Nginx:   "$secure_link_expires$uri {SECRET}"
#
# $secure_link_expires = URL 中的 expires 参数值
# $uri = 当前请求的 URI (如 /media/videos/5a7e08c3.dat)
# {SECRET} = 硬编码的 HMAC 密钥(两端必须相同)
# ─── Python 验证脚本(调试用) ───
import hashlib, base64, time

secret = "你的HMAC密钥"
expires = int(time.time()) + 3600
uri = "/media/videos/00001358.dat"
raw = f"{expires}{uri} {secret}"
md5 = hashlib.md5(raw.encode()).digest()
sig = base64.b64encode(md5).decode() \
    .replace('+', '-').replace('/', '_').rstrip('=')
print(f"https://vip.mhana.top{uri}?sig={sig}&expires={expires}")

5.4 启用配置

# 删除默认站点
rm -f /etc/nginx/sites-enabled/default

# 创建符号链接
ln -sf /etc/nginx/sites-available/vip-video /etc/nginx/sites-enabled/

# 测试配置语法
nginx -t

# 重载配置(不中断连接)
systemctl reload nginx

# 验证
curl -s http://{SERVER_IP}/health   # 应返回 "ok"

6 SSL 证书申请

前提:DNS A 记录必须已经指向服务器 IP,且不能开启 Cloudflare 代理(灰色云朵)。

# 1. 在 DNS 管理面板添加 A 记录
#    类型: A
#    名称: vip (或你的子域名)
#    内容: {SERVER_IP}
#    代理: 仅 DNS(灰色云朵,不走 CF 代理)

# 2. 验证 DNS 生效
dig +short {DOMAIN} A
# 应返回你的服务器 IP

# 3. 申请证书(自动修改 Nginx 配置)
certbot --nginx -d {DOMAIN} \
    --non-interactive \
    --agree-tos \
    --email {YOUR_EMAIL} \
    --redirect

# 4. 验证 HTTPS
curl -s https://{DOMAIN}/health   # 应返回 "ok"

# 5. certbot 自动续期(已自动配置)
certbot renew --dry-run   # 测试续期是否正常

7 videoop Pipeline 部署

7.1 传输代码

# 在源服务器(当前运行 videoop 的机器)上打包
cd /workspace   # 或 videoop 所在的父目录
tar czf /tmp/videoop.tar.gz \
    --exclude='videoop/data/downloads/*' \
    --exclude='videoop/data/processed/*' \
    --exclude='videoop/__pycache__' \
    --exclude='videoop/*/__pycache__' \
    --exclude='videoop/.venv' \
    --exclude='videoop/*.log' \
    videoop/

# 传输到新服务器
scp -P {SSH_PORT} /tmp/videoop.tar.gz root@{SERVER_IP}:/opt/

# 在新服务器上解压
cd /opt && tar xzf videoop.tar.gz && rm videoop.tar.gz

7.2 安装 Python 依赖

cd /opt/videoop

# 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate

# 安装核心依赖
pip install telethon aiosqlite boto3 nudenet imagehash \
    Pillow httpx python-dotenv

# 安装 PyTorch (CPU 版,无需 GPU)
pip install torch --index-url https://download.pytorch.org/whl/cpu

# 安装 OpenCLIP (视频向量嵌入)
pip install open_clip_torch

7.3 配置环境变量

文件路径:/opt/videoop/.env

# ─── Telegram ────────────────────────────────────────
TELEGRAM_API_ID={你的API_ID}
TELEGRAM_API_HASH={你的API_HASH}
TELEGRAM_SESSION_NAME=videoop_session

# ─── Cloudflare R2 ───────────────────────────────────
R2_ACCOUNT_ID={CF账户ID}
R2_ACCESS_KEY_ID={R2访问密钥ID}
R2_SECRET_ACCESS_KEY={R2访问密钥}
R2_BUCKET_NAME=video-storage
R2_ENDPOINT=https://{CF账户ID}.r2.cloudflarestorage.com

# ─── Workers API ─────────────────────────────────────
WORKERS_API_URL=https://videoserver.mhana.top
WORKERS_ADMIN_TOKEN={管理员JWT}

# ─── Processing Limits ───────────────────────────────
MAX_FILE_SIZE_MB=2048
MAX_DURATION_SECONDS=7200
MAX_DOWNLOAD_DIR_CAPACITY_GB=50

# ─── VIP Local Storage (关键!) ───────────────────────
VIP_LOCAL_STORAGE_ENABLED=true
VIP_STORAGE_DIR=/data/videos

重要:VIP_LOCAL_STORAGE_ENABLED=true 使得 Pipeline 处理完的文件同时保存到本地 /data/videos/,这样新处理的视频不需要额外同步就能通过 VIP 线路访问。

7.4 VIP 本地存储的代码逻辑

文件路径:/opt/videoop/main.py(核心片段)

# 在 R2 上传成功后(Step 6.5),复制到本地 VIP 存储
if config.VIP_LOCAL_STORAGE_ENABLED:
    import shutil
    try:
        # 视频文件: videos/{hash}.dat
        src_video = output_dir / f"{video_hash}.dat"
        dst_video = config.VIP_STORAGE_DIR / "videos" / f"{video_hash}.dat"
        shutil.copy2(src_video, dst_video)

        # 缩略图: thumbs/{hash}.jpg
        src_thumb = output_dir / f"{video_hash}.jpg"
        dst_thumb = config.VIP_STORAGE_DIR / "thumbs" / f"{video_hash}.jpg"
        shutil.copy2(src_thumb, dst_thumb)

        # 预览动图: previews/{hash}.webp
        src_preview = output_dir / f"{video_hash}.webp"
        dst_preview = config.VIP_STORAGE_DIR / "previews" / f"{video_hash}.webp"
        shutil.copy2(src_preview, dst_preview)

        log.info(f"VIP local copy done: {video_hash}")
    except Exception as e:
        # 非致命错误:不影响 Pipeline 主流程
        log.warning(f"VIP local copy failed: {e}")

7.5 PM2 进程管理

启动模式说明(重要)

main.py 有三种启动模式,必须使用 --server 模式

模式命令行为适用场景
--server main.py --server 常驻守护进程,轮询 D1 命令队列。收到 Admin 页面的 start 命令时起子进程采集,收到 stop 时杀子进程,守护进程不退出 生产环境唯一正确的模式
--watch main.py --watch 启动后立即开始持续监控采集,Admin stop 会导致进程退出,PM2 autorestart 会重新拉起 不适合配合 Admin 页面使用
默认 ❌ main.py 单次运行,跑完所有频道后退出,PM2 autorestart 又会重新拉起 不适合配合 PM2 使用

关键区别:--server 模式下,Pipeline 的启停由 Admin 页面控制(通过 D1 命令总线),PM2 只负责守护 server 进程本身不崩溃。其他模式会与 PM2 的 autorestart 冲突。

文件路径:/opt/videoop/ecosystem.config.js

module.exports = {
  apps: [{
    name: 'videoop',
    script: '/opt/videoop/.venv/bin/python',
    args: 'main.py --server',    // ⚠️ 必须用 --server 模式
    cwd: '/opt/videoop',
    interpreter: 'none',
    autorestart: true,
    max_restarts: 10,
    restart_delay: 5000,
    watch: false,
    log_file: '/opt/videoop/pipeline.log',
    error_file: '/opt/videoop/pipeline-error.log',
    out_file: '/opt/videoop/pipeline-out.log',
    env: {
      PATH: '/opt/videoop/.venv/bin:/usr/local/bin:/usr/bin:/bin'
    }
  }]
};
# 启动 Pipeline 守护进程
pm2 start /opt/videoop/ecosystem.config.js

# 配置开机自启
pm2 startup systemd -u root --hp /root
pm2 save

# 常用命令
pm2 logs videoop        # 实时日志
pm2 stop videoop        # 停止守护进程(一般不需要,用 Admin 页面控制采集即可)
pm2 restart videoop     # 重启守护进程
pm2 monit               # 资源监控面板
pm2 status              # 查看所有进程

Admin 页面与 Pipeline 的通信机制

Admin 页面                    Cloudflare Worker (D1)              VIP 服务器
┌──────────┐    POST /pipeline/command    ┌──────────────┐    GET /pipeline/commands/pending    ┌──────────────┐
│  点击     │ ──────────────────────────→ │ pipeline_    │ ←─────────────────────────────────── │ main.py      │
│  Start   │                             │ commands 表  │                                      │ --server     │
│  Stop    │    GET /pipeline/status      │              │    PATCH /pipeline/status             │ (轮询D1)     │
│  状态显示 │ ←────────────────────────── │ pipeline_    │ ←─────────────────────────────────── │              │
│          │                             │ status 表    │                                      │              │
└──────────┘                             └──────────────┘                                      └──────────────┘

# 流程:
# 1. Admin 点 Start → 写入 pipeline_commands 表 (action: "start")
# 2. main.py --server 轮询到 pending 命令 → 起子进程运行 pipeline
# 3. 子进程定期更新 pipeline_status 表 → Admin 页面展示状态
# 4. Admin 点 Stop → 写入 pipeline_commands 表 (action: "stop")
# 5. main.py --server 轮询到 stop → 杀掉子进程,守护进程继续等待

8 R2 数据同步

8.1 配置 rclone

文件路径:/root/.config/rclone/rclone.conf

[r2]
type = s3
provider = Cloudflare
access_key_id = {R2_ACCESS_KEY_ID}
secret_access_key = {R2_SECRET_ACCESS_KEY}
endpoint = https://{CF_ACCOUNT_ID}.r2.cloudflarestorage.com
acl = private
no_check_bucket = true
# 验证连接
rclone lsd r2:video-storage/
# 应显示: videos/ previews/ thumbs/ 等目录

8.2 全量同步

# 三路并行同步(后台运行)
nohup rclone sync r2:video-storage/videos/ /data/videos/videos/ \
    --transfers 16 --checkers 32 --fast-list \
    --log-file=/root/rclone-videos.log --log-level INFO --stats 5m &

nohup rclone sync r2:video-storage/previews/ /data/videos/previews/ \
    --transfers 16 --checkers 32 --fast-list \
    --log-file=/root/rclone-previews.log --log-level INFO --stats 5m &

nohup rclone sync r2:video-storage/thumbs/ /data/videos/thumbs/ \
    --transfers 16 --checkers 32 --fast-list \
    --log-file=/root/rclone-thumbs.log --log-level INFO --stats 5m &

# 监控进度
tail -f /root/rclone-videos.log
du -sh /data/videos/*/

同步耗时参考:~57,000 个视频,约 1.5TB 数据。在 1Gbps 带宽下约需 4-8 小时。预览图和缩略图体积小,通常 1-2 小时即可完成。

注意:在同步完成前不要启动 videoop Pipeline,也不要给用户开启 VIP 线路,否则部分视频会 404。

9 Cloudflare Worker 配置激活

9.1 修改 wrangler.toml

文件路径:videoworker/wrangler.toml

# 在 [vars] 部分修改以下两行
[vars]
# ... 其他配置 ...
VIP_SERVER_URL = "https://{DOMAIN}"           # 空字符串 = VIP 功能关闭
VIP_HMAC_SECRET = "{与Nginx相同的HMAC密钥}"   # 必须与 Nginx 配置一致

9.2 部署 Worker

cd /path/to/videoworker

# 设置 API Token
export CLOUDFLARE_API_TOKEN="{你的CF API Token}"

# 部署
npx wrangler deploy

# 验证:VIP_SERVER_URL 应显示你的域名
# Vars:
#   VIP_SERVER_URL: "https://vip.mhana.top"
#   VIP_HMAC_SECRET: "201047bc..."

VIP 功能的开关机制:

这意味着你可以随时通过修改这个变量来开关 VIP 线路,无需改任何代码。

10 用户 VIP 状态管理

10.1 数据库字段

字段类型默认值说明
is_vipINTEGER00=普通用户, 1=VIP
vip_quota_secondsINTEGER3000VIP 配额(秒),默认50分钟
vip_used_secondsINTEGER0已使用时间(秒)

10.2 通过 D1 SQL 管理

# 设置用户为 VIP
CLOUDFLARE_API_TOKEN="{token}" npx wrangler d1 execute video-db --remote \
    --command "UPDATE users SET is_vip = 1, vip_quota_seconds = 3000, vip_used_seconds = 0 WHERE username = 'boy'"

# 取消 VIP
CLOUDFLARE_API_TOKEN="{token}" npx wrangler d1 execute video-db --remote \
    --command "UPDATE users SET is_vip = 0 WHERE username = 'boy'"

# 调整配额(设为 2 小时)
CLOUDFLARE_API_TOKEN="{token}" npx wrangler d1 execute video-db --remote \
    --command "UPDATE users SET vip_quota_seconds = 7200 WHERE username = 'boy'"

# 重置已用时间
CLOUDFLARE_API_TOKEN="{token}" npx wrangler d1 execute video-db --remote \
    --command "UPDATE users SET vip_used_seconds = 0 WHERE username = 'boy'"

# 查看所有 VIP 用户
CLOUDFLARE_API_TOKEN="{token}" npx wrangler d1 execute video-db --remote \
    --command "SELECT id, username, is_vip, vip_quota_seconds, vip_used_seconds FROM users WHERE is_vip = 1"

10.3 通过 Admin API 管理

# 需要管理员 JWT Token

# 设置 VIP
curl -X PUT https://videoserver.mhana.top/api/admin/users/{user_id}/vip \
    -H "Authorization: Bearer {ADMIN_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{"is_vip": true, "vip_quota_seconds": 3000}'

# 重置用量
curl -X PUT https://videoserver.mhana.top/api/admin/users/{user_id}/vip \
    -H "Authorization: Bearer {ADMIN_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{"reset_usage": true}'

11 端到端验证

# ─── 1. 健康检查 ───
curl -s https://{DOMAIN}/health
# 期望: "ok"

# ─── 2. 签名 URL 访问 ───
python3 -c "
import hashlib, base64, time
secret = '{VIP_HMAC_SECRET}'
expires = int(time.time()) + 3600
uri = '/media/videos/{任意已同步的hash}.dat'
raw = f'{expires}{uri} {secret}'
md5 = hashlib.md5(raw.encode()).digest()
sig = base64.b64encode(md5).decode().replace('+','-').replace('/','_').rstrip('=')
print(f'https://{DOMAIN}{uri}?sig={sig}&expires={expires}')
"
# 用输出的 URL 请求,期望: HTTP 200, Content-Type: video/mp4

# ─── 3. 安全验证 ───
curl -o /dev/null -w "%{http_code}" "https://{DOMAIN}/media/videos/test.dat"
# 期望: 403 (无签名)

curl -o /dev/null -w "%{http_code}" "https://{DOMAIN}/media/videos/test.dat?sig=fake&expires=9999999999"
# 期望: 403 (签名无效)

# ─── 4. Range 请求 ───
curl -o /dev/null -w "%{http_code}" -H "Range: bytes=0-1023" "{签名URL}"
# 期望: 206 Partial Content

# ─── 5. Feed API 验证 ───
TOKEN=$(curl -s https://videoserver.mhana.top/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"{VIP用户名}","password":"{密码}"}' \
    | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")

curl -s "https://videoserver.mhana.top/api/feed?limit=1" \
    -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: 返回中包含 vip_video_url, vip_first_frame_url, vip_preview_webp_url 字段

# ─── 6. VIP 配额 ───
curl -s https://videoserver.mhana.top/api/user/vip-quota \
    -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: { is_vip: true, quota: 3000, used: 0, remaining: 3000 }

12 日常运维

# ═══ Pipeline 管理 ═══
pm2 start /opt/videoop/ecosystem.config.js   # 首次启动
pm2 stop videoop                              # 停止
pm2 restart videoop                           # 重启
pm2 logs videoop --lines 50                   # 最近50行日志
pm2 monit                                     # 实时监控面板

# ═══ Nginx 管理 ═══
nginx -t                                      # 测试配置
systemctl reload nginx                        # 重载(不中断连接)
systemctl restart nginx                       # 重启
tail -f /var/log/nginx/error.log              # 错误日志

# ═══ 磁盘监控 ═══
df -h /                                       # 总磁盘
du -sh /data/videos/*/                        # 各子目录大小
ls /data/videos/videos/ | wc -l               # 视频文件数

# ═══ SSL 证书 ═══
certbot certificates                          # 查看证书信息和到期时间
certbot renew --dry-run                       # 测试自动续期
# certbot 已配置 systemd timer 自动续期,通常无需手动操作

# ═══ 增量同步(如果 R2 有新文件需要手动同步) ═══
rclone sync r2:video-storage/videos/ /data/videos/videos/ \
    --transfers 16 --checkers 32 --fast-list --progress

# ═══ 系统资源 ═══
htop                                          # CPU/内存实时监控
free -h                                       # 内存使用
ss -tlnp                                      # 监听端口

13 安全加固

# ─── 1. 修改 root 密码 ───
NEW_PASS=$(openssl rand -base64 24)
echo "root:$NEW_PASS" | chpasswd
echo "新密码: $NEW_PASS"   # 立即保存到安全位置!

# ─── 2. 配置 SSH 密钥登录(推荐) ───
# 在本地机器上:
ssh-keygen -t ed25519 -C "vip-server"
ssh-copy-id -p {SSH_PORT} root@{SERVER_IP}

# ─── 3. 禁用密码登录(可选,配置密钥后) ───
# 编辑 /etc/ssh/sshd_config:
#   PasswordAuthentication no
# systemctl restart sshd

# ─── 4. 防火墙(可选) ───
ufw allow {SSH_PORT}/tcp    # SSH
ufw allow 80/tcp            # HTTP
ufw allow 443/tcp           # HTTPS
ufw enable

14 故障排查

症状可能原因排查命令
VIP URL 返回 403 签名不匹配(密钥不一致或算法错误) 用 Python 脚本手动生成签名对比;检查 Nginx 和 Worker 的 HMAC 密钥是否一致
VIP URL 返回 410 链接已过期 检查服务器时钟:date;链接默认 1 小时有效
VIP URL 返回 404 文件不存在(尚未同步或路径错误) ls /data/videos/videos/{hash}.dat
Feed 中无 vip_*_url 字段 用户非 VIP 或 VIP_SERVER_URL 为空 D1 查询 is_vip;检查 wrangler.toml 中 VIP_SERVER_URL
CORS 错误 Nginx 缺少 CORS 头 curl -v 检查响应头是否包含 Access-Control-Allow-Origin
Pipeline 崩溃 内存不足(CLIP 模型)或网络问题 pm2 logs videoopfree -h
Nginx 不监听端口 配置错误导致静默失败 nginx -tss -tlnp | grep nginxsystemctl restart nginx
rclone 连接失败 R2 密钥过期或配置错误 rclone lsd r2:video-storage/

15 完整环境变量参考

videoworker (wrangler.toml [vars])

变量说明示例
VIP_SERVER_URLVIP 服务器地址(空=关闭)https://vip.mhana.top
VIP_HMAC_SECRET签名密钥(与 Nginx 一致)201047bc9d1d...

videoop (.env)

变量说明示例
VIP_LOCAL_STORAGE_ENABLED启用本地存储true
VIP_STORAGE_DIR本地存储根目录/data/videos

Nginx (vip-server 配置)

配置项说明
secure_link_md5 中的密钥必须与 VIP_HMAC_SECRET 完全一致
alias /data/videos/必须与 VIP_STORAGE_DIR 对应

16 关键文件一览

# ─── VIP 服务器 ───
/etc/nginx/sites-available/vip-video       # Nginx 站点配置
/etc/nginx/sites-enabled/vip-video         # ↑ 的符号链接
/etc/letsencrypt/live/{DOMAIN}/            # SSL 证书
/root/.config/rclone/rclone.conf           # rclone R2 连接配置
/data/videos/                              # 视频文件根目录
/opt/videoop/                              # Pipeline 程序
/opt/videoop/.env                          # Pipeline 环境变量
/opt/videoop/ecosystem.config.js           # PM2 进程配置
/var/www/html/index.html                   # 文档首页

# ─── 开发机 / 部署源 ───
videoworker/wrangler.toml                  # Worker 配置(含 VIP_SERVER_URL)
videoworker/src/services/sign.ts           # 签名算法
videoworker/src/routes/feed.ts             # Feed 注入 VIP URL
videoworker/src/routes/user.ts             # VIP 配额查询/上报
videoworker/src/routes/admin.ts            # 管理员 VIP 设置
videoworker/src/routes/auth.ts             # 登录返回 is_vip
videoworker/nginx/vip-server.conf          # Nginx 配置模板
videoagt/src/store/vipStore.ts             # 前端 VIP 状态管理
videoagt/src/app/profile/page.tsx          # VIP 开关 UI
videoop/config.py                          # VIP_LOCAL_STORAGE 配置
videoop/main.py                            # Pipeline 本地存储逻辑