11 Commits

Author SHA1 Message Date
39724e6e73 fix(audit): 修复3个审计发现的bug
- migrateCloudConfigs 缺 } 导致 notify_config/cloud_type_uid 嵌套在 promotion_account 内不迁移
- cloud_configs_v2 INSERT/SELECT 缺 cloud_type_uid/cookie_uid 导致数据丢失
- deploy.sh 密码嵌入改成 docker exec -e 传参(防特殊字符注入)
2026-05-19 14:23:26 +08:00
288d30698a hotfix: 补回system_configs表缺失的); 2026-05-19 05:21:28 +08:00
a9dc056506 hotfix: 移除push_users表多余的);(导致SQL语法错误→容器重启) 2026-05-19 05:13:24 +08:00
9ef58b5724 fix: push_users表 + cookie_uid列 + __uid正则修复 + deploy.sh重写 2026-05-19 05:06:17 +08:00
c9067179ff fix: save_records表补全config_id/promotion_account列(修复转存500) 2026-05-19 04:52:08 +08:00
5e3b5e83a4 fix(deploy): 不再硬编码CORS_ORIGIN + 强制覆盖管理员密码(解决401) 2026-05-19 04:44:19 +08:00
d4e62b1fb1 feat: 一键部署脚本 deploy.sh (curl | bash 方式) 2026-05-19 04:32:19 +08:00
7471d9aece v0.4.8: docker-compose添加DATA_DIR=/data确保secrets.json路径一致 2026-05-19 03:34:23 +08:00
f0c0d0f487 v0.4.8: 同步根VERSION文件 2026-05-19 02:29:12 +08:00
e4e1890da3 v0.4.8: 补播link_invalid_keywords种子键 2026-05-19 02:28:49 +08:00
470ebac20e v0.4.7: 修复网盘类型开关key不匹配+补全10个缺失种子配置键 2026-05-19 02:02:48 +08:00
6 changed files with 158 additions and 97 deletions

View File

@@ -1 +1 @@
0.4.6
0.4.13

View File

@@ -70,6 +70,7 @@ services:
- CORS_ORIGIN=${CORS_ORIGIN}
# ── 数据库 & 缓存 ──
- DATA_DIR=/data
- DB_PATH=${DB_PATH:-/data/database.sqlite}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}

View File

@@ -1 +1 @@
0.4.6
0.4.13

View File

@@ -1,91 +1,128 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# CloudSearch 一键部署
# curl -sS https://gitea.timxx.cn/admin/CloudSearch/raw/branch/master/source_clean/deploy.sh | bash
#
# 环境变量(可选):
# CORS_ORIGIN - 域名 (默认自动检测公网IP)
# ADMIN_PASSWORD - 管理员密码 (留空自动生成)
# JWT_SECRET - JWT密钥 (留空自动生成)
# DEPLOY_DIR - 部署目录 (默认 /opt/cloudsearch)
# 如果 docker-compose.yml 不存在,自动下载
if [ ! -f docker-compose.yml ]; then
echo "📥 下载 docker-compose.yml..."
wget -q https://gitea.timxx.cn/admin/CloudSearch/raw/branch/master/source_clean/docker-compose.yml || {
echo "❌ 下载失败,请检查网络"
exit 1
}
REPO_URL="https://gitea.timxx.cn/admin/CloudSearch.git"
DEPLOY_DIR="${DEPLOY_DIR:-/opt/cloudsearch}"
info() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*"; }
err() { echo "[ERROR] $*"; }
command -v docker &>/dev/null || { err "Docker未安装"; exit 1; }
# -- CORS_ORIGIN --
if [ -z "$CORS_ORIGIN" ]; then
PUBLIC_IP=$(curl -s --connect-timeout 3 ifconfig.me 2>/dev/null || curl -s --connect-timeout 3 ip.sb 2>/dev/null || echo "")
if [ -n "$PUBLIC_IP" ]; then
CORS_ORIGIN="http://${PUBLIC_IP}:9527"
else
CORS_ORIGIN="http://localhost:9527"
fi
warn "CORS_ORIGIN=$CORS_ORIGIN (自动检测)"
fi
echo "🔍 检测 Redis..."
EXISTING_REDIS=$(docker ps --format '{{.Names}}' | grep -i redis | head -1)
if [ -n "$EXISTING_REDIS" ]; then
if docker network inspect cloudsearch-net --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -qw "$EXISTING_REDIS"; then
echo "✅ 已有 Redis: $EXISTING_REDIS (已加入 cloudsearch-net),跳过创建"
else
echo "✅ 已有 Redis: $EXISTING_REDIS,正在加入网络..."
docker network connect cloudsearch-net "$EXISTING_REDIS" 2>/dev/null || true
echo " ✅ 已加入 cloudsearch-net"
fi
# 检测 Redis 密码 (多种来源)
REDIS_PASS=""
# 方法1: 从命令行参数 --requirepass
PASS_FROM_CMD=$(docker inspect "$EXISTING_REDIS" --format '{{range .Config.Cmd}}{{println .}}{{end}}' 2>/dev/null | grep -A1 'requirepass' | tail -1 | tr -d '[:space:]' || true)
if [ -n "$PASS_FROM_CMD" ] && [ "$PASS_FROM_CMD" != "redis-server" ] && [ "$PASS_FROM_CMD" != "/etc/redis/redis.conf" ]; then
REDIS_PASS="$PASS_FROM_CMD"
echo " 🔑 从启动参数检测到 Redis 密码"
fi
# 方法2: 从 redis.conf 读取 (1Panel 常见配置方式)
if [ -z "$REDIS_PASS" ]; then
PASS_FROM_CONF=$(docker exec "$EXISTING_REDIS" cat /etc/redis/redis.conf 2>/dev/null | grep '^requirepass ' | awk '{print $2}' | tr -d '"' || true)
if [ -n "$PASS_FROM_CONF" ]; then
REDIS_PASS="$PASS_FROM_CONF"
echo " 🔑 从 redis.conf 检测到 Redis 密码"
fi
fi
# 方法3: 尝试无密码连接测试
if [ -z "$REDIS_PASS" ]; then
if docker exec "$EXISTING_REDIS" redis-cli ping 2>/dev/null | grep -q PONG; then
echo " Redis 无密码(已通过连接测试验证)"
else
# 有密码但检测不到 — 尝试从 redis-cli 的错误消息中提取
AUTH_ERR=$(docker exec "$EXISTING_REDIS" redis-cli ping 2>&1 || true)
if echo "$AUTH_ERR" | grep -q "NOAUTH\|AUTH"; then
echo " ⚠️ Redis 需要密码但无法自动检测,请手动设置 REDIS_URL"
echo " 提示: 在 .env 中设置 REDIS_URL=redis://:密码@${EXISTING_REDIS}:6379"
fi
fi
fi
if [ -n "$REDIS_PASS" ]; then
# 对密码中的特殊字符进行URL编码
ENCODED_PASS=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REDIS_PASS', safe=''))" 2>/dev/null || echo "$REDIS_PASS")
REDIS_URL="redis://:${ENCODED_PASS}@${EXISTING_REDIS}:6379"
else
REDIS_URL="redis://${EXISTING_REDIS}:6379"
fi
PROFILE=""
# -- 拉取仓库 --
info "拉取仓库..."
if [ -d "$DEPLOY_DIR/.git" ]; then
cd "$DEPLOY_DIR" && git pull --ff-only origin master 2>/dev/null || true
else
echo "📦 未检测到 Redis将自动创建..."
REDIS_URL="redis://CloudSearch_Redis:6379"
PROFILE="--profile full"
rm -rf "$DEPLOY_DIR"
git clone --depth 1 "$REPO_URL" "$DEPLOY_DIR"
fi
cd "$DEPLOY_DIR/source_clean"
# -- 切换镜像模式 --
if grep -q '^ build:' docker-compose.yml 2>/dev/null; then
info "切换镜像模式..."
sed -i 's/^ build:/ # build:/' docker-compose.yml
sed -i 's/^ context:/ # context:/' docker-compose.yml
sed -i 's/^ dockerfile:/ # dockerfile:/' docker-compose.yml
sed -i 's|^ # image: gitea.timxx.cn/admin/cloudsearch:latest| image: gitea.timxx.cn/admin/cloudsearch:latest|' docker-compose.yml
fi
# 生成 .env
cat > .env <<EOF
# -- 检测 Redis --
info "检测Redis..."
EXISTING_REDIS=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i redis | grep -v CloudSearch | head -1)
if [ -n "$EXISTING_REDIS" ]; then
REDIS_URL="redis://${EXISTING_REDIS}:6379"
info "复用已有Redis: $EXISTING_REDIS"
else
REDIS_URL="redis://redis:6379"
info "使用Compose自带Redis"
fi
# -- 生成密钥 --
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-$(openssl rand -base64 12 | tr -d '=+/' | head -c 16)}"
# -- 生成 .env --
cat > .env <<ENVEOF
CORS_ORIGIN=${CORS_ORIGIN}
JWT_SECRET=${JWT_SECRET}
ADMIN_PASSWORD=${ADMIN_PASSWORD}
REDIS_URL=${REDIS_URL}
CORS_ORIGIN=https://zy.hk.timxx.cn
JWT_SECRET=cloudsearch-jwt-secret-2024
ADMIN_PASSWORD=0nL5kLhMIJ1121PYmQb25A
LOG_LEVEL=info
EOF
LOG_LEVEL=${LOG_LEVEL:-info}
ENVEOF
echo ""
echo "🚀 启动服务..."
docker compose $PROFILE up -d
info "管理员: admin / ${ADMIN_PASSWORD}"
echo ""
echo "✅ 部署完成"
docker compose ps
# -- 部署 --
info "拉取镜像..."
docker compose pull app 2>/dev/null || true
info "停止旧服务..."
docker compose down --remove-orphans 2>/dev/null || true
info "启动服务..."
docker compose up -d
# -- 等待就绪 --
info "等待服务就绪..."
for i in $(seq 1 20); do
if curl -s -o /dev/null -w '%{http_code}' http://localhost:9527/health 2>/dev/null | grep -q '200'; then
break
fi
sleep 2
done
# -- 强制写入管理员密码 --
info "同步管理员密码..."
sleep 3
docker exec -e ADMIN_PASSWORD="$ADMIN_PASSWORD" CloudSearch_App node -e '
var bcrypt = require("bcryptjs");
var Database = require("better-sqlite3");
var db = new Database("/data/database.sqlite");
var pw = process.env.ADMIN_PASSWORD || ""; var hash = bcrypt.hashSync(pw, 10);
var existing = db.prepare("SELECT id FROM admins WHERE username = ?").get("admin");
if (existing) {
db.prepare("UPDATE admins SET password_hash = ? WHERE username = ?").run(hash, "admin");
} else {
db.prepare("INSERT INTO admins (username, password_hash) VALUES (?, ?)").run("admin", hash);
}
db.close();
' 2>/dev/null && info "密码已同步" || warn "密码同步失败,请稍后重试"
# -- 验证 --
if docker compose ps 2>/dev/null | grep -q 'Up'; then
echo ""
echo "=============================================="
echo " CloudSearch 部署完成"
echo "=============================================="
docker compose ps
echo ""
echo " 管理后台: ${CORS_ORIGIN}/admin/login"
echo " 用户名: admin"
echo " 密码: ${ADMIN_PASSWORD}"
else
err "启动失败: docker compose logs"
exit 1
fi

View File

@@ -35,9 +35,9 @@ function decryptCookie(encrypted: string): string {
function extractCookieUid(cookie: string): string {
if (!cookie) return '';
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
let m = cookie.match(/__uid=([^;]+)/);
if (m) return m[1];
m = cookie.match(/b-user-id=([a-zA-Z0-9-]+)/);
m = cookie.match(/b-user-id=([^;]+)/);
if (m) return m[1];
return '';
}

View File

@@ -40,6 +40,7 @@ function runMigrations(db: Database.Database): void {
cloud_type TEXT NOT NULL,
cookie TEXT,
cloud_type_uid TEXT DEFAULT NULL,
cookie_uid TEXT DEFAULT NULL,
nickname TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
@@ -109,6 +110,13 @@ function runMigrations(db: Database.Database): void {
description TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS push_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account TEXT NOT NULL UNIQUE,
notify_config TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS content_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -149,6 +157,8 @@ function migrateSaveRecords(db: Database.Database): void {
{ col: 'request_url', def: 'TEXT' },
{ col: 'ip_location', def: 'TEXT' },
{ col: 'original_folder_name', def: 'TEXT' },
{ col: 'config_id', def: 'INTEGER' },
{ col: 'promotion_account', def: 'TEXT' },
];
for (const { col, def } of newCols) {
try {
@@ -207,6 +217,7 @@ function migrateCloudConfigs(db: Database.Database): void {
cookie TEXT,
cloud_type_uid TEXT DEFAULT NULL,
nickname TEXT,
cookie_uid TEXT DEFAULT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
storage_used TEXT,
storage_total TEXT,
@@ -219,8 +230,8 @@ function migrateCloudConfigs(db: Database.Database): void {
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, cloud_type_uid, cookie_uid, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
SELECT id, cloud_type, cookie, cloud_type_uid, cookie_uid, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
DROP TABLE cloud_configs;
ALTER TABLE cloud_configs_v2 RENAME TO cloud_configs;
`);
@@ -246,6 +257,7 @@ function migrateCloudConfigs(db: Database.Database): void {
if (!hasPromotionAccount) {
db.exec("ALTER TABLE cloud_configs ADD COLUMN promotion_account TEXT DEFAULT NULL");
console.log('[DB] cloud_configs migration: promotion_account column added');
}
// v0.3.5: notify_config for per-cloud push notification settings
const hasNotifyConfig = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%notify_config%'").get();
@@ -289,18 +301,19 @@ function seedSystemConfigs(db: Database.Database): void {
{ key: 'search_proxy_url', value: '', description: '搜索代理地址 (如 http://127.0.0.1:7890)' },
{ key: 'search_strategy', value: 'wait_all', description: '搜索结果展示方式: wait_all=等待全部后展示, stream_channel=频道逐步展示' },
{ key: 'link_validation_enabled', value: 'true', description: '资源链接有效性检测开关true/false' },
{ key: 'cloud_enabled_quark', value: 'true', description: '夸克网盘' },
{ key: 'cloud_enabled_baidu', value: 'true', description: '百度网盘' },
{ key: 'cloud_enabled_aliyun', value: 'true', description: '阿里云盘' },
{ key: 'cloud_enabled_115', value: 'true', description: '115 网盘' },
{ key: 'cloud_enabled_tianyi', value: 'true', description: '天翼云盘' },
{ key: 'cloud_enabled_123pan', value: 'true', description: '123 云盘' },
{ key: 'cloud_enabled_uc', value: 'true', description: 'UC 网盘' },
{ key: 'cloud_enabled_xunlei', value: 'true', description: '迅雷网盘' },
{ key: 'cloud_enabled_pikpak', value: 'true', description: 'PikPak 网盘' },
{ key: 'cloud_enabled_magnet', value: 'true', description: '磁力链接' },
{ key: 'cloud_enabled_ed2k', value: 'true', description: '电驴链接' },
{ key: 'cloud_enabled_others', value: 'false', description: '其他类型(默认关闭)' },
{ key: 'link_invalid_keywords', value: '', description: '链接失效关键词(一行一条,命中的链接判定为失效)' },
{ key: 'cloud_type_quark_enabled', value: 'true', description: '夸克网盘' },
{ key: 'cloud_type_baidu_enabled', value: 'true', description: '百度网盘' },
{ key: 'cloud_type_aliyun_enabled', value: 'true', description: '阿里云盘' },
{ key: 'cloud_type_115_enabled', value: 'true', description: '115 网盘' },
{ key: 'cloud_type_tianyi_enabled', value: 'true', description: '天翼云盘' },
{ key: 'cloud_type_123pan_enabled', value: 'true', description: '123 云盘' },
{ key: 'cloud_type_uc_enabled', value: 'true', description: 'UC 网盘' },
{ key: 'cloud_type_xunlei_enabled', value: 'true', description: '迅雷网盘' },
{ key: 'cloud_type_pikpak_enabled', value: 'true', description: 'PikPak 网盘' },
{ key: 'cloud_type_magnet_enabled', value: 'true', description: '磁力链接' },
{ key: 'cloud_type_ed2k_enabled', value: 'true', description: '电驴链接' },
{ key: 'cloud_type_others_enabled', value: 'false', description: '其他类型(默认关闭)' },
{ key: 'search_result_limit', value: '10', description: '每类网盘最多展示的有效结果数' },
{ key: 'search_fallback_image', value: '', description: '无图资源的兜底封面图 URL留空使用渐变色' },
{ key: 'site_logo', value: '', description: '网站 LOGO 图片 URL留空使用默认图标/文字)' },
@@ -329,6 +342,16 @@ function seedSystemConfigs(db: Database.Database): void {
{ key: 'search_all_channels', value: 'false', description: '使用所有频道参与搜索(包含未启用频道)' },
{ key: 'ip_geo_provider', value: 'apihz', description: 'IP 归属地查询接口提供商' },
{ key: 'auto_update_enabled', value: 'false', description: '自动更新镜像(预留,暂未实现)' },
{ key: 'cleanup_auto_refresh_storage', value: 'false', description: '自动刷新网盘空间信息(每天检查一次)' },
{ key: 'cleanup_verify_enabled', value: 'false', description: '启用转存后自动验证链接有效性' },
{ key: 'cleanup_verify_interval', value: '3600', description: '自动验证间隔(秒)' },
{ key: 'cleanup_whitelist_dirs', value: '', description: '清理文件白名单目录(逗号分隔,保留不删)' },
{ key: 'proxy_url', value: '', description: 'HTTP 代理地址(用于搜索请求代理)' },
{ key: 'quark_ad_keywords', value: '', description: '夸克广告文件关键词(逗号分隔)' },
{ key: 'quark_sus_extensions', value: '', description: '夸克可疑文件后缀(逗号分隔)' },
{ key: 'quark_warning_folder_names', value: '', description: '夸克警示文件夹名称(逗号分隔)' },
{ key: 'storage_refresh_interval', value: '86400', description: '空间信息刷新间隔默认24小时' },
];
const insert = db.prepare(
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'