从零开始在新服务器上完成完整部署的操作指南
| 资源 | 最低要求 | 推荐配置 | 说明 |
|---|---|---|---|
| CPU | 8 核 | 16+ 核 | FFmpeg 转码 + CLIP 推理需要算力 |
| 内存 | 16 GB | 32 GB | CLIP ViT-B-32 模型加载约需 2GB |
| 系统盘 | 50 GB SSD | 100 GB SSD | 操作系统 + 应用程序 |
| 数据盘 | 2 TB SSD | 4+ TB SSD | 视频 + 预览 + 缩略图存储 |
| 带宽 | 100 Mbps | 1 Gbps CN 优化 | 中国大陆优化线路是核心 |
| 操作系统 | Ubuntu 22.04 / 24.04 LTS | 本手册基于 Ubuntu 24.04 | |
| 信息 | 用途 | 获取位置 |
|---|---|---|
| 服务器 IP | SSH 登录 / DNS 解析 | 服务器供应商面板 |
| SSH 端口 / 密码 | 远程管理 | 服务器供应商邮件 |
| 域名 | VIP 线路访问地址 | 如 vip.mhana.top |
| Cloudflare API Token | Worker 部署 | Cloudflare Dashboard → Profile → API Tokens |
| R2 Access Key | 数据同步 + Pipeline 上传 | Cloudflare Dashboard → R2 → API Tokens |
| VIP HMAC Secret | 签名密钥(Worker + Nginx 共享) | 自行生成:openssl rand -hex 32 |
┌─────────────────────────────────────────────────────────────────┐
│ 用户浏览器 (前端 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
# 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+
# 创建视频存储目录
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,浏览器可以正常播放。
# 生成 64 字符的 hex 随机密钥
openssl rand -hex 32
# 输出示例: 201047bc9d1da378f6cc2c20766d7059d3b14c69fcf68800a7d2fe2051cc37a0
# ⚠️ 这个密钥必须与 Cloudflare Worker 的 VIP_HMAC_SECRET 完全一致
文件路径:/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;
}
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}")
# 删除默认站点
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"
前提: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 # 测试续期是否正常
# 在源服务器(当前运行 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
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
文件路径:/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 线路访问。
文件路径:/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}")
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 页面 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 → 杀掉子进程,守护进程继续等待
文件路径:/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/ 等目录
# 三路并行同步(后台运行)
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。
文件路径:videoworker/wrangler.toml
# 在 [vars] 部分修改以下两行
[vars]
# ... 其他配置 ...
VIP_SERVER_URL = "https://{DOMAIN}" # 空字符串 = VIP 功能关闭
VIP_HMAC_SECRET = "{与Nginx相同的HMAC密钥}" # 必须与 Nginx 配置一致
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_SERVER_URL = ""(空字符串)→ 所有 VIP 代码路径不生效,系统按普通模式运行VIP_SERVER_URL = "https://vip.mhana.top" → VIP 功能激活这意味着你可以随时通过修改这个变量来开关 VIP 线路,无需改任何代码。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
is_vip | INTEGER | 0 | 0=普通用户, 1=VIP |
vip_quota_seconds | INTEGER | 3000 | VIP 配额(秒),默认50分钟 |
vip_used_seconds | INTEGER | 0 | 已使用时间(秒) |
# 设置用户为 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"
# 需要管理员 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}'
# ─── 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 }
# ═══ 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 # 监听端口
# ─── 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
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 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 videoop;free -h |
| Nginx 不监听端口 | 配置错误导致静默失败 | nginx -t;ss -tlnp | grep nginx;systemctl restart nginx |
| rclone 连接失败 | R2 密钥过期或配置错误 | rclone lsd r2:video-storage/ |
| 变量 | 说明 | 示例 |
|---|---|---|
VIP_SERVER_URL | VIP 服务器地址(空=关闭) | https://vip.mhana.top |
VIP_HMAC_SECRET | 签名密钥(与 Nginx 一致) | 201047bc9d1d... |
| 变量 | 说明 | 示例 |
|---|---|---|
VIP_LOCAL_STORAGE_ENABLED | 启用本地存储 | true |
VIP_STORAGE_DIR | 本地存储根目录 | /data/videos |
| 配置项 | 说明 |
|---|---|
secure_link_md5 中的密钥 | 必须与 VIP_HMAC_SECRET 完全一致 |
alias /data/videos/ | 必须与 VIP_STORAGE_DIR 对应 |
# ─── 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 本地存储逻辑