Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

90
scripts/bluey_update.py Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""从 TMDB 网页数据解析剧集信息,通过 Emby API 批量更新元数据"""
import re, json, subprocess, sys
# TMDB 数据(从 web_fetch 结果手动整理)
def parse_tmdb(text):
eps = []
# 匹配: 数字\n\n### [集名](...) ... • Xm\n\n 简介 \n [展开]
blocks = re.split(r'\n\s*/tv/82728-bluey/season/\d+/episode/\d+\?language=zh-CN\s*\n', text)
for block in blocks:
m = re.match(r'\s*(\d+)\s*\n\n###\s*\[([^\]]+)\]', block)
if not m:
continue
num = int(m.group(1))
name = m.group(2)
# 找简介:在 • Xm 之后,[展开] 之前
ov_match = re.search(r'\s*\d+m\s*\n\s*(.*?)\n\s*\[展开\]', block, re.DOTALL)
overview = ov_match.group(1).strip() if ov_match else ""
# 清理简介中的换行
overview = re.sub(r'\s+', ' ', overview)
eps.append({"number": num, "name": name, "overview": overview})
return eps
EMBY = "http://localhost:8096/emby"
KEY = "e3e52b1dcb8b47c39d46b5256bf87081"
SERIES_ID = "10"
SEASON_MAP = {1: "11", 2: "12", 3: "13"}
def ssh_cmd(cmd):
full = f"sshpass -p 'fJ7#vP9s@tL2qX!d' ssh -o StrictHostKeyChecking=no -i /tmp/koipy_key root@155.103.67.95 \"{cmd}\""
r = subprocess.run(full, shell=True, capture_output=True, text=True, timeout=30)
return r.stdout.strip()
def get_episodes(season_id):
out = ssh_cmd(f"curl -s '{EMBY}/Shows/{SERIES_ID}/Episodes?SeasonId={season_id}&api_key={KEY}'")
return json.loads(out).get("Items", [])
def update_episode(item_id, name, overview):
# 用 Emby API 更新 Name 和 Overview
# 需要先 GET 完整 item修改后 POST 回去
escaped_name = name.replace("'", "'\\''").replace('"', '\\"')
escaped_ov = overview.replace("'", "'\\''").replace('"', '\\"')
cmd = f"""
python3 -c '
import json, urllib.request
url = "{EMBY}/Items/{item_id}?api_key={KEY}"
data = json.loads(urllib.request.urlopen(url).read())
data["Name"] = "{escaped_name}"
data["Overview"] = "{escaped_ov}"
req = urllib.request.Request(url, data=json.dumps(data).encode(), method="POST", headers={{"Content-Type": "application/json"}})
urllib.request.urlopen(req)
print("ok")
'
"""
return ssh_cmd(cmd.strip())
# 读取保存的 TMDB 数据
with open("/Users/jianzhang/.openclaw/workspace/scripts/bluey_tmdb.json") as f:
tmdb_data = json.load(f)
total = 0
for season_num in [1, 2, 3]:
season_id = SEASON_MAP[season_num]
tmdb_eps = tmdb_data.get(str(season_num), [])
emby_eps = get_episodes(season_id)
print(f"\n=== 第{season_num}季 === TMDB: {len(tmdb_eps)}集, Emby: {len(emby_eps)}")
# 按 IndexNumber 匹配
emby_map = {ep.get("IndexNumber"): ep for ep in emby_eps}
for tmdb_ep in tmdb_eps:
num = tmdb_ep["number"]
name = tmdb_ep["name"]
overview = tmdb_ep["overview"]
if num not in emby_map:
print(f" E{num}: 跳过Emby中不存在")
continue
emby_ep = emby_map[num]
item_id = emby_ep["Id"]
result = update_episode(item_id, name, overview)
status = "" if "ok" in result else f"{result}"
print(f" E{num} {name}: {status}")
total += 1
print(f"\n完成!共更新 {total}")

25
scripts/bookapi-proxy.js Normal file
View File

@@ -0,0 +1,25 @@
const http = require("http");
const https = require("https");
const { URL } = require("url");
const TARGET = "https://tiger.bookapi.cc";
const PORT = 18801;
http.createServer((req, res) => {
const url = new URL(req.url, TARGET);
const headers = { ...req.headers, host: url.host, "user-agent": "curl/8.0" };
// Strip identifying headers
for (const k of Object.keys(headers)) {
if (k.startsWith("x-stainless") || k === "anthropic-dangerous-direct-browser-access" || k === "sec-fetch-mode") {
delete headers[k];
}
}
const proxy = https.request(url.href, { method: req.method, headers }, (pRes) => {
res.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(res);
});
proxy.on("error", (e) => { res.writeHead(502); res.end(e.message); });
req.pipe(proxy);
}).listen(PORT, "127.0.0.1", () => console.log(`Proxy on 127.0.0.1:${PORT}`));

15
scripts/boot-start.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/data/data/com.termux/files/usr/bin/bash
# Termux:Boot 开机自启脚本
# 修复权限Android 14 重启后会重置)
/system/bin/chmod +x /data/data/com.termux/files/usr/bin/* 2>/dev/null
/system/bin/chmod +x /data/data/com.termux/files/usr/libexec/* 2>/dev/null
# 启动 SSH
sshd
# 启动 OpenClaw
cd ~/.openclaw/workspace && nohup openclaw gateway run >> ~/.openclaw/gateway.log 2>&1 &
# 启动数据收集循环
nohup bash ~/.openclaw/workspace/scripts/collect-phone-data-loop.sh &>/dev/null &

View File

@@ -0,0 +1,183 @@
import telebot
import requests
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
BOT_TOKEN = "7741492900:AAG6M_faLy5JG7g0t2gBFj-sXtBIaBXQZHk"
ALLOWED_USERS = [165067365]
CF_API_TOKEN = "VfeGnC3v5McJ3sJ6bsjjhG0IhvOgY-5-v6lpYgH3"
bot = telebot.TeleBot(BOT_TOKEN)
ZONE_CACHE = {}
USER_STATE = {}
def get_zones():
headers = {"Authorization": f"Bearer {CF_API_TOKEN}"}
r = requests.get("https://api.cloudflare.com/client/v4/zones", headers=headers).json()
return r.get("result", [])
def get_zone_id(domain):
root = ".".join(domain.split(".")[-2:])
if root in ZONE_CACHE:
return ZONE_CACHE[root]
for z in get_zones():
if z["name"] == root:
ZONE_CACHE[root] = z["id"]
return z["id"]
return None
def is_authorized(uid):
return uid in ALLOWED_USERS
def main_menu():
markup = InlineKeyboardMarkup(row_width=2)
markup.add(
InlineKeyboardButton(' 添加记录', callback_data='add'),
InlineKeyboardButton('🗑 删除记录', callback_data='del'),
InlineKeyboardButton('☁️ 小黄云', callback_data='proxy'),
InlineKeyboardButton('📋 列出记录', callback_data='list')
)
return markup
def domain_menu(action):
zones = get_zones()
markup = InlineKeyboardMarkup(row_width=2)
for z in zones[:10]:
markup.add(InlineKeyboardButton(z['name'], callback_data=f"{action}:{z['name']}"))
markup.add(InlineKeyboardButton('🔙 返回', callback_data='back'))
return markup
@bot.message_handler(commands=['help', 'start'])
def start_cmd(message):
if not is_authorized(message.from_user.id): return
bot.send_message(message.chat.id, '🌐 CF DNS 管理', reply_markup=main_menu())
@bot.callback_query_handler(func=lambda c: True)
def callback_handler(call):
uid = call.from_user.id
if not is_authorized(uid): return
data = call.data
if data == 'back':
bot.edit_message_text('🌐 CF DNS 管理', call.message.chat.id, call.message.message_id, reply_markup=main_menu())
return
if data in ['add', 'del', 'proxy', 'list']:
bot.edit_message_text(f'选择域名:', call.message.chat.id, call.message.message_id, reply_markup=domain_menu(data))
return
if data.startswith('list:'):
domain = data.split(':')[1]
zone_id = get_zone_id(domain)
headers = {"Authorization": f"Bearer {CF_API_TOKEN}"}
r = requests.get(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", headers=headers).json()
records = r.get("result", [])
if not records:
txt = f'📋 {domain}\n\n无记录'
else:
lines = [f"{rec['name']} -> {rec['content']} ({'☁️' if rec['proxied'] else ''})" for rec in records[:15]]
txt = f'📋 {domain}\n\n' + '\n'.join(lines)
markup = InlineKeyboardMarkup()
markup.add(InlineKeyboardButton('🔙 返回', callback_data='back'))
bot.edit_message_text(txt, call.message.chat.id, call.message.message_id, reply_markup=markup)
return
if data.startswith('add:'):
domain = data.split(':')[1]
USER_STATE[uid] = {'action': 'add', 'domain': domain}
markup = InlineKeyboardMarkup()
markup.add(InlineKeyboardButton('🔙 取消', callback_data='back'))
bot.edit_message_text(f' 添加记录到 {domain}\n\n请输入: 子域名 IP\n例如: www 1.2.3.4', call.message.chat.id, call.message.message_id, reply_markup=markup)
return
if data.startswith('del:'):
domain = data.split(':')[1]
zone_id = get_zone_id(domain)
headers = {"Authorization": f"Bearer {CF_API_TOKEN}"}
r = requests.get(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", headers=headers).json()
records = r.get("result", [])
markup = InlineKeyboardMarkup(row_width=1)
for rec in records[:10]:
short = rec['name'].replace(f'.{domain}', '') if rec['name'] != domain else '@'
markup.add(InlineKeyboardButton(f"🗑 {short} -> {rec['content']}", callback_data=f"doit:{rec['id']}:{domain}"))
markup.add(InlineKeyboardButton('🔙 返回', callback_data='back'))
bot.edit_message_text(f'选择要删除的记录:', call.message.chat.id, call.message.message_id, reply_markup=markup)
return
if data.startswith('doit:'):
parts = data.split(':')
rec_id, domain = parts[1], parts[2]
zone_id = get_zone_id(domain)
headers = {"Authorization": f"Bearer {CF_API_TOKEN}"}
requests.delete(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", headers=headers)
bot.answer_callback_query(call.id, '✅ 已删除')
bot.edit_message_text('🌐 CF DNS 管理', call.message.chat.id, call.message.message_id, reply_markup=main_menu())
return
if data.startswith('proxy:'):
domain = data.split(':')[1]
zone_id = get_zone_id(domain)
headers = {"Authorization": f"Bearer {CF_API_TOKEN}"}
r = requests.get(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", headers=headers).json()
records = r.get("result", [])
markup = InlineKeyboardMarkup(row_width=1)
for rec in records[:10]:
short = rec['name'].replace(f'.{domain}', '') if rec['name'] != domain else '@'
icon = '☁️' if rec['proxied'] else ''
markup.add(InlineKeyboardButton(f"{icon} {short} -> {rec['content']}", callback_data=f"toggle:{rec['id']}:{domain}"))
markup.add(InlineKeyboardButton('🔙 返回', callback_data='back'))
bot.edit_message_text(f'点击切换小黄云:', call.message.chat.id, call.message.message_id, reply_markup=markup)
return
if data.startswith('toggle:'):
parts = data.split(':')
rec_id, domain = parts[1], parts[2]
zone_id = get_zone_id(domain)
headers = {"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}
r = requests.get(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", headers=headers).json()
rec = r.get("result", {})
new_state = not rec.get('proxied', False)
requests.patch(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{rec_id}", headers=headers, json={"proxied": new_state})
bot.answer_callback_query(call.id, f"✅ 小黄云已{'开启' if new_state else '关闭'}")
r = requests.get(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", headers=headers).json()
records = r.get("result", [])
markup = InlineKeyboardMarkup(row_width=1)
for rec in records[:10]:
short = rec['name'].replace(f'.{domain}', '') if rec['name'] != domain else '@'
icon = '☁️' if rec['proxied'] else ''
markup.add(InlineKeyboardButton(f"{icon} {short} -> {rec['content']}", callback_data=f"toggle:{rec['id']}:{domain}"))
markup.add(InlineKeyboardButton('🔙 返回', callback_data='back'))
bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup)
return
@bot.message_handler(func=lambda m: m.from_user.id in USER_STATE)
def handle_add(message):
uid = message.from_user.id
if not is_authorized(uid): return
state = USER_STATE.get(uid)
if not state or state['action'] != 'add': return
parts = message.text.strip().split()
if len(parts) < 2:
bot.reply_to(message, '格式错误,请输入: 子域名 IP')
return
sub, ip = parts[0], parts[1]
domain = state['domain']
del USER_STATE[uid]
zone_id = get_zone_id(domain)
if not zone_id:
bot.reply_to(message, f'找不到域名 {domain}')
return
name = f"{sub}.{domain}" if sub != '@' else domain
headers = {"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}
data = {"type": "A", "name": name, "content": ip, "ttl": 1, "proxied": False}
r = requests.post(f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records", headers=headers, json=data).json()
if r.get("success"):
bot.reply_to(message, f'✅ 添加成功: {name} -> {ip}', reply_markup=main_menu())
else:
bot.reply_to(message, f'❌ 失败: {r.get("errors", [])}')
if __name__ == '__main__':
print('CF Bot 启动...')
bot.infinity_polling()

101
scripts/checkin-cdp.mjs Normal file
View File

@@ -0,0 +1,101 @@
import fs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const WebSocket = require('ws');
const LOG = '/Users/jianzhang/.openclaw/workspace/scripts/checkin.log';
const ts = new Date().toISOString();
(async () => {
try {
const verRes = await fetch('http://127.0.0.1:18800/json/version');
const ver = await verRes.json();
const wsUrl = ver.webSocketDebuggerUrl;
if (!wsUrl) throw new Error('No webSocketDebuggerUrl');
const ws = new WebSocket(wsUrl);
let id = 1;
const pending = new Map();
ws.on('message', d => {
const m = JSON.parse(d);
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
});
const send = (method, params = {}) => new Promise(r => {
const i = id++;
pending.set(i, r);
ws.send(JSON.stringify({ id: i, method, params }));
});
await new Promise(r => ws.on('open', r));
// Create new tab
const target = await send('Target.createTarget', { url: 'about:blank' });
const targetId = target.result.targetId;
// Attach
const sess = await send('Target.attachToTarget', { targetId, flatten: true });
const sessionId = sess.result.sessionId;
const sendS = (method, params = {}) => new Promise(r => {
const i = id++;
pending.set(i, r);
ws.send(JSON.stringify({ id: i, method, params, sessionId }));
});
await sendS('Network.enable');
await sendS('Page.enable');
// Set cookie
await sendS('Network.setCookie', {
name: '_nk',
value: 'dc4d1551406351a93c09082ea08e2d2e',
domain: '.nodeseek.com',
path: '/',
httpOnly: true,
secure: true
});
// Navigate
await sendS('Page.navigate', { url: 'https://www.nodeseek.com' });
// Wait for page load
await new Promise(r => {
const handler = d => {
const m = JSON.parse(d);
if (m.method === 'Page.loadEventFired') { ws.removeListener('message', handler); r(); }
};
ws.on('message', handler);
});
// Wait 8s
await new Promise(r => setTimeout(r, 8000));
// Call attendance API
await sendS('Runtime.enable');
const result = await sendS('Runtime.evaluate', {
expression: `(async () => {
const res = await fetch('/api/attendance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ random: true })
});
return await res.text();
})()`,
awaitPromise: true,
returnByValue: true
});
const val = result.result?.result?.value || JSON.stringify(result.result);
const logMsg = `[${ts}] NodeSeek 签到: ${val}\n`;
fs.appendFileSync(LOG, logMsg);
console.log('签到结果:', val);
// Close tab
await send('Target.closeTarget', { targetId });
ws.close();
} catch (e) {
const logMsg = `[${ts}] 签到失败: ${e.message}\n`;
fs.appendFileSync(LOG, logMsg);
console.error('失败:', e.message);
}
})();

View File

@@ -0,0 +1,7 @@
#!/data/data/com.termux/files/usr/bin/bash
# 手动收集数据快捷方式
bash ~/.openclaw/workspace/scripts/collect-phone-data.sh
# 显示通知
termux-notification --title "数据收集完成" --content "电量、短信、通话记录已更新" --id collect-data

View File

@@ -0,0 +1,7 @@
#!/data/data/com.termux/files/usr/bin/bash
# 后台循环收集手机数据
while true; do
bash ~/.openclaw/workspace/scripts/collect-phone-data.sh >> ~/.openclaw/workspace/data/collect.log 2>&1
sleep 300 # 5分钟
done

View File

@@ -0,0 +1,19 @@
#!/data/data/com.termux/files/usr/bin/bash
# 收集手机数据到文件
DATA_DIR=~/.openclaw/workspace/data
mkdir -p $DATA_DIR
# 电量
termux-battery-status > $DATA_DIR/battery.json 2>&1
# 最近10条短信
termux-sms-list -l 10 > $DATA_DIR/sms.json 2>&1
# 最近20条通话记录
termux-call-log -l 20 > $DATA_DIR/call-log.json 2>&1
# 时间戳
date +%s > $DATA_DIR/last-update.txt
echo "Data collected at $(date)"

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openclaw.mac-guard</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/jianzhang/.openclaw/workspace/scripts/mac-guard.js</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/jianzhang/.openclaw/workspace/scripts/mac-guard.log</string>
<key>StandardErrorPath</key>
<string>/Users/jianzhang/.openclaw/workspace/scripts/mac-guard.log</string>
</dict>
</plist>

114
scripts/dd-reinstall.sh Normal file
View File

@@ -0,0 +1,114 @@
#!/bin/bash
# DD 重装系统脚本 - Debian 13
# 用法: bash dd-reinstall.sh
# 一键: bash <(curl -sL https://cdn.jsdelivr.net/gh/xmg0828888/dd-reinstall@main/dd-reinstall.sh)
set -e
# 颜色
R='\033[0;31m' G='\033[0;32m' Y='\033[0;33m' B='\033[0;34m' N='\033[0m'
echo -e "${B}╔══════════════════════════════════════╗${N}"
echo -e "${B}║ DD 重装系统 - Debian 13 ║${N}"
echo -e "${B}╚══════════════════════════════════════╝${N}"
echo
# 默认值
DEF_PWD="Mng@2026DD!"
DEF_PORT="22"
DEF_HOST="debian"
DEF_TZ="Asia/Hong_Kong"
DEF_SWAP="1024"
# 交互输入
read -p "$(echo -e ${G}主机名${N} [${DEF_HOST}]: )" MYHOST
MYHOST=${MYHOST:-$DEF_HOST}
read -p "$(echo -e ${G}SSH端口${N} [${DEF_PORT}]: )" MYPORT
MYPORT=${MYPORT:-$DEF_PORT}
read -sp "$(echo -e ${G}root密码${N} [默认: ${DEF_PWD}]: )" MYPWD
echo
MYPWD=${MYPWD:-$DEF_PWD}
read -p "$(echo -e ${G}时区${N} [${DEF_TZ}]: )" MYTZ
MYTZ=${MYTZ:-$DEF_TZ}
read -p "$(echo -e ${G}Swap大小MB${N} [${DEF_SWAP}]: )" MYSWAP
MYSWAP=${MYSWAP:-$DEF_SWAP}
read -p "$(echo -e ${G}启用BBR${N} [Y/n]: )" BBR
BBR=${BBR:-Y}
# 系统选择
echo
echo -e "${Y}选择系统:${N}"
echo " 1) Debian 13 (默认)"
echo " 2) Debian 12"
echo " 3) Ubuntu 24.04"
echo " 4) Ubuntu 22.04"
echo " 5) CentOS 9"
echo " 6) Alpine 3.19"
read -p "$(echo -e ${G}选择${N} [1]: )" OS_CHOICE
OS_CHOICE=${OS_CHOICE:-1}
case $OS_CHOICE in
1) OS_FLAG="-debian 13" ;;
2) OS_FLAG="-debian 12" ;;
3) OS_FLAG="-ubuntu 24.04" ;;
4) OS_FLAG="-ubuntu 22.04" ;;
5) OS_FLAG="-centos 9" ;;
6) OS_FLAG="-alpine 3.19" ;;
*) OS_FLAG="-debian 13" ;;
esac
BBR_FLAG=""
[[ "${BBR,,}" != "n" ]] && BBR_FLAG="--bbr"
# 确认
echo
echo -e "${Y}════════ 确认配置 ════════${N}"
echo -e " 系统: ${B}${OS_FLAG}${N}"
echo -e " 主机名: ${B}${MYHOST}${N}"
echo -e " SSH端口: ${B}${MYPORT}${N}"
echo -e " 密码: ${B}******${N}"
echo -e " 时区: ${B}${MYTZ}${N}"
echo -e " Swap: ${B}${MYSWAP}MB${N}"
echo -e " BBR: ${B}${BBR_FLAG:-关闭}${N}"
echo -e "${Y}══════════════════════════${N}"
echo
read -p "$(echo -e ${R}确认重装? 数据将全部丢失!${N} [y/N]: )" CONFIRM
[[ "${CONFIRM,,}" != "y" ]] && echo "已取消" && exit 0
# 下载并执行
echo -e "${G}下载 InstallNET.sh ...${N}"
wget --no-check-certificate -qO InstallNET.sh \
'https://raw.githubusercontent.com/leitbogioro/Tools/master/Linux_reinstall/InstallNET.sh'
chmod a+x InstallNET.sh
echo -e "${G}开始重装...${N}"
bash InstallNET.sh $OS_FLAG \
-port "$MYPORT" \
-pwd "$MYPWD" \
-hostname "$MYHOST" \
-timezone "$MYTZ" \
-swap "$MYSWAP" \
$BBR_FLAG
# 修复 GRUB timeout防止卡在菜单
GRUB_CFG="/boot/grub/grub.cfg"
if [ -f "$GRUB_CFG" ]; then
if ! grep -q "^set timeout=" "$GRUB_CFG"; then
sed -i '/^set default=/a set timeout=5' "$GRUB_CFG"
echo -e "${G}已添加 GRUB timeout=5${N}"
fi
fi
echo
echo -e "${Y}重装完成10秒后自动重启...${N}"
for i in $(seq 10 -1 1); do
echo -ne "\r${R}${i}${N} 秒后重启... (Ctrl+C 取消)"
sleep 1
done
echo
reboot

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
const EMBY_HOST = '145.239.143.92:8096';
const EMBY_API_KEY = 'e3e52b1dcb8b47c39d46b5256bf87081';
function curl(url, opts = '') {
return execSync(`curl -s ${opts} "${url}"`, { encoding: 'utf8' });
}
async function loginJellyseerr() {
const res = curl('http://145.239.143.92:5055/api/v1/auth/local',
`-X POST -H "Content-Type: application/json" -d '{"email":"admin","password":"admin"}' -i`);
const match = res.match(/connect\.sid=([^;]+)/);
if (!match) throw new Error('Login failed');
return `connect.sid=${match[1]}`;
}
function getRecentRequests(cookie) {
const data = curl('http://145.239.143.92:5055/api/v1/request?take=20&skip=0&sort=added&filter=all',
`-H "Cookie: ${cookie}"`);
return JSON.parse(data);
}
function checkEmbyItem(tmdbId) {
const data = curl(`http://${EMBY_HOST}/emby/Items?Recursive=true&AnyProviderIdEquals=Tmdb.${tmdbId}&api_key=${EMBY_API_KEY}`);
return JSON.parse(data);
}
function refreshMetadata(itemId) {
curl(`http://${EMBY_HOST}/emby/Items/${itemId}/Refresh?MetadataRefreshMode=FullRefresh&ImageRefreshMode=FullRefresh&ReplaceAllMetadata=true&ReplaceAllImages=true&api_key=${EMBY_API_KEY}`, '-X POST');
}
async function main() {
const cookie = await loginJellyseerr();
const requests = getRecentRequests(cookie);
const now = Date.now();
const oneDayAgo = now - 24 * 60 * 60 * 1000;
const recent = requests.results.filter(r => new Date(r.createdAt).getTime() > oneDayAgo);
if (recent.length === 0) {
console.log('✅ 最近24小时无新求片');
return;
}
const issues = [];
const fixed = [];
for (const req of recent) {
const result = checkEmbyItem(req.media.tmdbId);
if (result.Items.length === 0) continue;
const item = result.Items[0];
if (!item.ImageTags || Object.keys(item.ImageTags).length === 0) {
issues.push(`${item.Name} (${item.Id})`);
refreshMetadata(item.Id);
fixed.push(item.Name);
}
}
if (issues.length > 0) {
console.log(`⚠️ 发现 ${issues.length} 个刮削问题,已触发修复:\n${fixed.join('\n')}`);
} else {
console.log(`✅ 检查完成,${recent.length} 个求片项刮削正常`);
}
}
main().catch(console.error);

15
scripts/fix-telegram-menu.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
TOKEN=$(python3 -c "import json; c=json.load(open('$HOME/.openclaw/openclaw.json')); tg=c.get('telegram',c.get('channels',{}).get('telegram',{})); print(tg.get('botToken',''))")
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/setMyCommands" \
-H "Content-Type: application/json" \
-d '{
"commands": [
{"command": "new", "description": "新对话"},
{"command": "stop", "description": "停止生成"},
{"command": "status", "description": "查看状态"},
{"command": "models", "description": "查看可用模型"},
{"command": "reasoning", "description": "切换推理模式"},
{"command": "restart", "description": "重启 Gateway"},
{"command": "help", "description": "帮助"}
]
}'

Submodule scripts/gitea-backup/dd-reinstall added at 726430ccc6

Submodule scripts/gitea-backup/oc-monitor added at 2e6b86381b

Submodule scripts/gitea-backup/ss-rust added at 0a158bbcae

Submodule scripts/gitea-backup/sub-bot added at ea233c31b6

Submodule scripts/gitea-backup/tcp-bbr added at b7fb2756a5

Submodule scripts/gitea-backup/tg-user-monitor added at 8b185c799a

Submodule scripts/gitea-backup/vps-management-bot added at 76a263d0f9

Submodule scripts/gitea-backup/vps-snapshot added at ef303c7857

112
scripts/mac-guard.js Normal file
View File

@@ -0,0 +1,112 @@
/**
* Mac 登录失败监控 & 自动封禁脚本
* 检测 SSH/登录失败,同一 IP 失败 5 次自动封禁
*/
const { spawn } = require('child_process');
const https = require('https');
const fs = require('fs');
// ========== 配置 ==========
const CONFIG = {
maxFailures: 5,
banDurationMin: 60,
tgBotToken: '8300905342:AAH3Q78FuR5Exrw2zWYJHFRyVeLlws3xnww',
tgChatId: '165067365',
dataFile: '/Users/jianzhang/.openclaw/workspace/scripts/ban-data.json',
whitelistIPs: ['127.0.0.1', '192.168.1.1']
};
let failedAttempts = {};
let bannedIPs = {};
// ========== Telegram 通知 ==========
function sendTG(message) {
const data = JSON.stringify({ chat_id: CONFIG.tgChatId, text: message, parse_mode: 'HTML' });
const req = https.request({
hostname: 'api.telegram.org',
path: `/bot${CONFIG.tgBotToken}/sendMessage`,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
req.write(data);
req.end();
}
// ========== 数据持久化 ==========
function saveData() {
fs.writeFileSync(CONFIG.dataFile, JSON.stringify({ failedAttempts, bannedIPs }, null, 2));
}
function loadData() {
try {
if (fs.existsSync(CONFIG.dataFile)) {
const d = JSON.parse(fs.readFileSync(CONFIG.dataFile, 'utf8'));
failedAttempts = d.failedAttempts || {};
bannedIPs = d.bannedIPs || {};
}
} catch (e) {}
}
// ========== 封禁/解封 IP ==========
function banIP(ip) {
if (CONFIG.whitelistIPs.includes(ip) || bannedIPs[ip]) return;
console.log(`[BAN] ${ip}`);
bannedIPs[ip] = Date.now();
spawn('sudo', ['pfctl', '-t', 'bruteforce', '-T', 'add', ip]).on('close', (code) => {
if (code === 0) {
sendTG(`🚫 <b>IP 已封禁</b>\n\nIP: <code>${ip}</code>\n原因: 登录失败 ${CONFIG.maxFailures}\n时长: ${CONFIG.banDurationMin} 分钟`);
}
});
setTimeout(() => unbanIP(ip), CONFIG.banDurationMin * 60 * 1000);
saveData();
}
function unbanIP(ip) {
if (!bannedIPs[ip]) return;
console.log(`[UNBAN] ${ip}`);
delete bannedIPs[ip];
delete failedAttempts[ip];
spawn('sudo', ['pfctl', '-t', 'bruteforce', '-T', 'delete', ip]);
saveData();
}
// ========== 记录失败 ==========
function recordFailure(ip) {
if (CONFIG.whitelistIPs.includes(ip) || bannedIPs[ip]) return;
if (!failedAttempts[ip]) failedAttempts[ip] = { count: 0, firstTime: Date.now() };
failedAttempts[ip].count++;
console.log(`[FAIL] ${ip} x${failedAttempts[ip].count}`);
if (failedAttempts[ip].count >= CONFIG.maxFailures) banIP(ip);
saveData();
}
// ========== 解析日志 ==========
function parseLine(line) {
let m = line.match(/Failed password for .* from ([\d.]+)/);
if (m) return m[1];
m = line.match(/Invalid user .* from ([\d.]+)/);
if (m) return m[1];
m = line.match(/authentication failure.*rhost=([\d.]+)/);
if (m) return m[1];
return null;
}
// ========== 启动监控 ==========
function start() {
console.log('[START] Mac Guard 启动');
loadData();
const log = spawn('log', ['stream', '--predicate', 'process == "sshd"', '--style', 'syslog']);
log.stdout.on('data', (d) => {
d.toString().split('\n').forEach(line => {
const ip = parseLine(line);
if (ip) recordFailure(ip);
});
});
log.on('close', () => { console.log('[RESTART]'); setTimeout(start, 5000); });
}
start();

73
scripts/mteam-batch-dl.sh Normal file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# M-Team 批量搜索下载脚本 - OVH KS2 迁移用
# 搜索动画种子并添加到 OVH qBittorrent
JWT="eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ4bWcwODI4OCIsInVpZCI6MzgxNDg3LCJqdGkiOiIwMTljOGNkMy01YTlhLTcyODctODQ0OS04ZTYwZjkxZWIyOTEiLCJpc3MiOiJodHRwczovL2FwaS5tLXRlYW0uaW8iLCJpYXQiOjE3NzE4ODkxODcsImV4cCI6MTc3NDQ4MTE4N30.5Xn_bYGXP8biz6yEEYP05wSaoyPI0zBX9a6YmUctyhvUYGlf58m60ta-OzOtz_GHj13xuoNVxyZXAQLmqIaatA"
SECRET="HLkPcWmycL57mfJt"
API="https://api.m-team.io"
QB_HOST="http://145.239.143.92:8080"
QB_USER="admin"
QB_PASS="Mango2026!"
# 登录 qB
login_qb() {
SID=$(curl -s -c - "$QB_HOST/api/v2/auth/login" -d "username=$QB_USER&password=$QB_PASS" | grep SID | awk '{print $NF}')
echo "$SID"
}
# M-Team API 搜索
search_mt() {
local keyword="$1"
local mode="${2:-normal}" # normal or adult
curl -s "$API/api/torrent/search" \
-H "authorization: $JWT" \
-H "Content-Type: application/json" \
-d "{\"keyword\":\"$keyword\",\"mode\":\"$mode\",\"pageNumber\":1,\"pageSize\":30}"
}
# 生成下载 token
gen_dl_token() {
local torrent_id="$1"
local ts_ms=$(date +%s%3N)
local ts_s=$((ts_ms / 1000))
local sign_str="POST&/api/torrent/genDlToken&${ts_ms}"
local sign=$(echo -n "$sign_str" | openssl dgst -sha1 -hmac "$SECRET" -binary | base64)
curl -s "$API/api/torrent/genDlToken" \
-H "authorization: $JWT" \
-H "ts: $ts_s" \
-H "visitorId: ff841bb2fb467c6f2261348af1672d67" \
-H "version: 1.1.4" \
-H "webVersion: 1140" \
-H "did: a8b989661e274ff89aae7bdd2b67663e" \
-F "id=$torrent_id" \
-F "_timestamp=$ts_ms" \
-F "_sgin=$sign"
}
# 添加种子到 qB
add_to_qb() {
local dl_url="$1"
local sid="$2"
local savepath="${3:-/downloads}"
curl -s -b "SID=$sid" "$QB_HOST/api/v2/torrents/add" \
--data-urlencode "urls=$dl_url" \
-d "savepath=$savepath"
}
echo "=== M-Team 批量下载 ==="
echo "目标: OVH KS2 (145.239.143.92)"
echo ""
# 登录 qB
echo "[1] 登录 qBittorrent..."
SID=$(login_qb)
echo "SID: $SID"
if [ -z "$SID" ]; then
echo "ERROR: qB 登录失败"
exit 1
fi
echo "[2] 开始搜索种子..."
echo ""

36
scripts/mteam-guide.md Normal file
View File

@@ -0,0 +1,36 @@
# M-Team 馒头 PT 下载流程
## 前置条件
- 桌面浏览器(openclaw profile)已登录 kp.m-team.cc
- localStorage key: `auth` (JWT token)
- VPS qBittorrent: 155.103.67.95:8080
## 搜索
browser navigate → https://kp.m-team.cc/browse?keyword=关键词
snapshot 读取结果
## 选版本优先级
中配 > 中字 > 无中文 | 4K > 1080p | H.265 > H.264 | 做种数>5
## 获取下载链接 (浏览器JS)
API: https://api.m-team.io/api/torrent/genDlToken (POST)
签名: HMAC-SHA1, secret `HLkPcWmycL57mfJt`
sign string: "POST&/api/torrent/genDlToken&毫秒时间戳" → Base64
FormData: id, _timestamp, _sgin
Headers: ts(秒), visitorId(ff841bb2fb467c6f2261348af1672d67), version(1.1.4), webVersion(1140), authorization(JWT), did(a8b989661e274ff89aae7bdd2b67663e)
间隔500ms避免限流
## 添加到 qBittorrent
ssh -i /tmp/koipy_key root@155.103.67.95
登录: curl http://127.0.0.1:8080/api/v2/auth/login -d "username=admin&password=Mango2026!"
添加: curl -b "SID=xxx" http://127.0.0.1:8080/api/v2/torrents/add --data-urlencode "urls=dlv2_URL"
## 下载完成后
autorun脚本 /data/qbittorrent/auto_link.sh 自动创建Emby软链接+扫描
手动: ln -s 源 /data/media/动画/名(年)/Season X/SxxExx.ext
扫描: curl http://127.0.0.1:8096/Library/Refresh?api_key=e3e52b1dcb8b47c39d46b5256bf87081
## 常见问题
- JWT过期: 重新登录浏览器, localStorage.getItem("auth")
- dlv2→google.com: 用api.m-team.io不是api2.m-team.cc
- 端口: 用51413不是6881(被屏蔽)

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# NodeSeek 自动签到 - 使用 OpenClaw browser 工具
# 通过 browser 工具控制 Chrome 浏览器签到
import json
import subprocess
import time
from datetime import datetime
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
STATE_FILE = SCRIPT_DIR / "nodeseek-checkin-state.json"
ACCOUNTS = {
"朦胧": "dc4d1551406351a93c09082ea08e2d2e",
"VP404": "3cfeb30b562daec31ba63bf64fdb3838"
}
def browser_cmd(action, **kwargs):
"""执行 OpenClaw browser 命令"""
cmd = ["openclaw", "browser", action]
for k, v in kwargs.items():
cmd.extend([f"--{k}", str(v)])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
try:
return json.loads(result.stdout)
except:
return {"ok": True}
return {"ok": False, "error": result.stderr}
def checkin_account(name, cookie, target_id):
"""签到单个账号"""
print(f"[{name}] 签到中...")
# 设置 Cookie
set_cookie_js = f"async () => {{ document.cookie = '_nk={cookie}; domain=.nodeseek.com; path=/'; location.reload(); }}"
browser_cmd("act", profile="openclaw", targetId=target_id, fn=set_cookie_js)
time.sleep(3)
# 执行签到
checkin_js = "async () => { const res = await fetch('https://www.nodeseek.com/api/attendance?random=false', {method: 'POST', credentials: 'include'}); return await res.json(); }"
result = browser_cmd("act", profile="openclaw", targetId=target_id, fn=checkin_js)
if result.get("result", {}).get("success"):
reward = result["result"].get("data", "未知")
print(f"[{name}] ✅ 签到成功!奖励: {reward}")
return {"status": "success", "reward": reward, "time": datetime.now().isoformat()}
else:
message = result.get("result", {}).get("message", "未知错误")
print(f"[{name}] ❌ {message}")
return {"status": "failed", "error": message, "time": datetime.now().isoformat()}
def main():
# 检查今天是否已签到
state = {}
if STATE_FILE.exists():
state = json.loads(STATE_FILE.read_text())
today = datetime.now().strftime("%Y-%m-%d")
if state.get("lastCheckin") == today and state.get("accounts"):
print(f"今天已经签到过了 ({today})")
for name, result in state.get("accounts", {}).items():
status = result.get("status")
detail = result.get("reward") or result.get("error", "N/A")
print(f" {name}: {status} ({detail})")
return
print(f"开始签到 NodeSeek ({today})...")
# 启动浏览器
browser_cmd("start", profile="openclaw")
time.sleep(2)
# 打开 NodeSeek
result = browser_cmd("open", profile="openclaw", targetUrl="https://www.nodeseek.com")
target_id = result.get("targetId")
if not target_id:
print("❌ 无法打开浏览器")
return
time.sleep(3)
# 签到所有账号
results = {}
for name, cookie in ACCOUNTS.items():
results[name] = checkin_account(name, cookie, target_id)
time.sleep(3)
# 保存状态
state["lastCheckin"] = today
state["accounts"] = results
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
print("\n签到完成!")
for name, result in results.items():
status = result.get("status")
detail = result.get("reward") or result.get("error", "N/A")
print(f" {name}: {status} ({detail})")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
import puppeteer from 'puppeteer-core';
import { appendFileSync } from 'fs';
const LOG = '/Users/jianzhang/.openclaw/workspace/scripts/checkin.log';
function log(msg) {
const ts = new Date().toISOString();
appendFileSync(LOG, `[${ts}] ${msg}\n`);
console.log(msg);
}
async function run() {
const browser = await puppeteer.connect({
browserURL: 'http://127.0.0.1:18800'
});
const pages = await browser.pages();
let page = pages.find(p => p.url().includes('nodeseek.com'));
if (!page) {
page = await browser.newPage();
await page.goto('https://www.nodeseek.com');
log('创建新标签页');
} else {
log('使用已有标签页');
}
await new Promise(r => setTimeout(r, 3000));
const result = await page.evaluate(async () => {
const res = await fetch('/api/attendance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ random: true })
});
return res.json();
});
log(`签到结果: ${JSON.stringify(result)}`);
await browser.disconnect();
}
run().catch((e) => {
log(`错误: ${e.message}`);
process.exit(1);
});

View File

@@ -0,0 +1,15 @@
{
"lastCheckin": "2026-03-02",
"accounts": {
"朦胧": {
"status": "error",
"error": "HTTP 500",
"time": "2026-03-02T09:34:11.262202"
},
"VP404": {
"status": "error",
"error": "HTTP 500",
"time": "2026-03-02T09:34:15.067695"
}
}
}

View File

@@ -0,0 +1,200 @@
import WebSocket from 'ws';
import { appendFileSync } from 'fs';
const CDP_PORT = 18800;
const COOKIE_VALUE = '3cfeb30b562daec31ba63bf64fdb3838';
const TARGET_URL = 'https://www.nodeseek.com';
const LOG_FILE = '/Users/jianzhang/.openclaw/workspace/scripts/checkin.log';
let msgId = 1;
let pendingCallbacks = new Map();
let loadEventResolve = null;
function log(message) {
const timestamp = new Date().toISOString();
const logLine = `[${timestamp}] [VP404] ${message}\n`;
console.log(logLine.trim());
try {
appendFileSync(LOG_FILE, logLine);
} catch (e) {
console.error('Failed to write log:', e.message);
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function send(ws, method, params = {}) {
return new Promise((resolve, reject) => {
const id = msgId++;
const msg = JSON.stringify({ id, method, params });
pendingCallbacks.set(id, { resolve, reject });
ws.send(msg);
setTimeout(() => {
if (pendingCallbacks.has(id)) {
pendingCallbacks.delete(id);
reject(new Error(`Timeout for ${method}`));
}
}, 30000);
});
}
async function main() {
log('开始 NodeSeek VP404 签到任务');
let ws;
let targetId = null;
try {
// Step 1: Get browser WebSocket URL
log('获取 CDP 连接信息...');
const resp = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
const version = await resp.json();
const browserWsUrl = version.webSocketDebuggerUrl;
log(`Browser WS: ${browserWsUrl}`);
// Step 2: Connect to browser
ws = new WebSocket(browserWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('CDP 连接成功');
// Step 3: Create new target
log('创建新标签页...');
const targetResult = await send(ws, 'Target.createTarget', {
url: 'about:blank'
});
targetId = targetResult.targetId;
log(`Target ID: ${targetId}`);
// Step 4: Get target's WebSocket URL via HTTP
const targetsResp = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
const targets = await targetsResp.json();
const pageTarget = targets.find(t => t.id === targetId);
if (!pageTarget) {
throw new Error('找不到创建的标签页');
}
const pageWsUrl = pageTarget.webSocketDebuggerUrl;
log(`Page WS: ${pageWsUrl}`);
// Step 5: Close browser connection and connect to page
ws.close();
await sleep(500);
ws = new WebSocket(pageWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('已连接到页面');
// Step 6: Enable domains
await send(ws, 'Page.enable');
await send(ws, 'Network.enable');
// Step 7: Set cookie
log('设置 Cookie...');
await send(ws, 'Network.setCookie', {
name: '_nk',
value: COOKIE_VALUE,
domain: '.nodeseek.com',
path: '/'
});
log('Cookie 设置成功');
// Step 8: Navigate
log('导航到 NodeSeek...');
const loadPromise = new Promise(resolve => { loadEventResolve = resolve; });
await send(ws, 'Page.navigate', { url: TARGET_URL });
log('等待页面加载...');
await loadPromise;
log('页面加载完成');
// Step 9: Wait for stability
log('等待 5 秒...');
await sleep(5000);
// Step 10: Call attendance API
log('调用签到 API...');
const evalResult = await send(ws, 'Runtime.evaluate', {
expression: `fetch('/api/attendance',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({random:true})
}).then(r => r.json()).catch(e => ({error: e.message}))`,
returnByValue: true,
awaitPromise: true
});
const result = evalResult.result.value;
log(`签到结果: ${JSON.stringify(result)}`);
// Step 11: Close page
log('关闭标签页...');
await send(ws, 'Page.close');
ws.close();
log('签到任务完成 ✓');
} catch (error) {
log(`错误: ${error.message}`);
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.close();
} catch (e) {}
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# NodeSeek 签到 - VP404 (Chrome CDP)
CDP="http://127.0.0.1:18800"
LOG="$HOME/.openclaw/workspace/scripts/checkin.log"
SESSION="0f20d87bfa1e3ddbe44b3f0eff84359a"
PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
# 确保 Chrome CDP 在跑
if ! curl -s "$CDP/json/version" >/dev/null 2>&1; then
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=18800 --user-data-dir=/tmp/chrome-debug-profile &>/dev/null &
sleep 5
fi
TAB_WS=$(curl -s "$CDP/json/list" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const t=JSON.parse(d).find(t=>t.type==='page');console.log(t?.webSocketDebuggerUrl||'')})")
[ -z "$TAB_WS" ] && echo "$(date '+%Y-%m-%d %H:%M') VP404: no tab" >> "$LOG" && exit 1
RESULT=$(cd /tmp && node -e "
const c=new(require('ws'))('$TAB_WS');
c.on('open',()=>c.send(JSON.stringify({id:1,method:'Network.setCookie',params:{name:'session',value:'$SESSION',domain:'www.nodeseek.com',path:'/',secure:true}})));
let s=0;c.on('message',m=>{s++;
if(s===1)c.send(JSON.stringify({id:2,method:'Page.navigate',params:{url:'https://www.nodeseek.com'}}));
else if(s===2)setTimeout(()=>c.send(JSON.stringify({id:3,method:'Runtime.evaluate',params:{expression:\"fetch('/api/attendance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({random:true})}).then(r=>r.json()).then(d=>JSON.stringify(d))\",awaitPromise:true}})),3000);
else{const r=JSON.parse(m.toString());console.log(r.result?.result?.value||'error');c.close();}});
setTimeout(()=>process.exit(1),15000);
" 2>&1)
echo "$(date '+%Y-%m-%d %H:%M') VP404: $RESULT" >> "$LOG"

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env node
const WebSocket = require("ws");
const fs = require("fs");
const CDP_URL = process.argv[2];
const COOKIE_VALUE = "dc4d1551406351a93c09082ea08e2d2e";
const TARGET_URL = "https://www.nodeseek.com";
const LOG_FILE = process.env.HOME + "/.openclaw/workspace/scripts/checkin.log";
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
let msgId = 1;
function send(ws, method, params = {}, sessionId) {
return new Promise((resolve, reject) => {
const id = msgId++;
const payload = { id, method, params };
if (sessionId) payload.sessionId = sessionId;
ws.send(JSON.stringify(payload));
const handler = (data) => {
try {
const msg = JSON.parse(data);
if (msg.id === id) {
ws.removeListener("message", handler);
if (msg.error) reject(msg.error);
else resolve(msg.result);
}
} catch(e) {}
};
ws.on("message", handler);
});
}
async function main() {
const ws = new WebSocket(CDP_URL);
await new Promise((resolve, reject) => {
ws.on("open", resolve);
ws.on("error", reject);
});
console.log("Connected to CDP");
// 创建新标签页
const { targetId } = await send(ws, "Target.createTarget", {
url: "about:blank"
});
console.log("Created target:", targetId);
// 连接到新标签页
const { sessionId } = await send(ws, "Target.attachToTarget", {
targetId,
flatten: true
});
console.log("Attached to target, sessionId:", sessionId);
// 设置 cookie
await send(ws, "Network.setCookie", {
name: "_nk",
value: COOKIE_VALUE,
domain: ".nodeseek.com",
path: "/"
}, sessionId);
console.log("Cookie set");
// 启用 Page 域
await send(ws, "Page.enable", {}, sessionId);
// 导航到页面并等待加载
const loadPromise = new Promise((resolve) => {
const handler = (data) => {
try {
const msg = JSON.parse(data);
if (msg.method === "Page.loadEventFired") {
ws.removeListener("message", handler);
resolve();
}
} catch(e) {}
};
ws.on("message", handler);
});
await send(ws, "Page.navigate", { url: TARGET_URL }, sessionId);
console.log("Navigating to:", TARGET_URL);
await loadPromise;
console.log("Page loaded, waiting 8 seconds...");
await sleep(8000);
// 执行签到
const result = await send(ws, "Runtime.evaluate", {
expression: `fetch("/api/attendance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ random: true })
}).then(r => r.json())`,
returnByValue: true,
awaitPromise: true
}, sessionId);
console.log("Check-in result:", JSON.stringify(result.result?.value || result));
// 记录日志
const logEntry = `[${new Date().toISOString()}] NodeSeek 朦胧签到: ${JSON.stringify(result.result?.value || result)}\n`;
fs.appendFileSync(LOG_FILE, logEntry);
console.log("Logged to:", LOG_FILE);
// 关闭标签页
await send(ws, "Target.closeTarget", { targetId });
console.log("Target closed");
ws.close();
console.log("Done");
return result.result?.value || result;
}
main().catch(err => {
console.error("Error:", err);
process.exit(1);
});

View File

@@ -0,0 +1,152 @@
import WebSocket from 'ws';
import { appendFileSync } from 'fs';
const CDP_BASE = 'http://127.0.0.1:18800';
const COOKIE = 'dc4d1551406351a93c09082ea08e2d2e';
const TARGET_URL = 'https://www.nodeseek.com';
const LOG_FILE = process.env.HOME + '/.openclaw/workspace/scripts/checkin.log';
let ws;
let msgId = 0;
let pending = new Map();
let targetId;
let sessionId;
function log(msg) {
const timestamp = new Date().toISOString();
const line = `[${timestamp}] ${msg}\n`;
console.log(line.trim());
appendFileSync(LOG_FILE, line);
}
async function getBrowserWsUrl() {
const resp = await fetch(`${CDP_BASE}/json/version`);
const data = await resp.json();
return data.webSocketDebuggerUrl;
}
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++msgId;
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify(msg));
// 10秒超时
setTimeout(() => {
if (pending.has(id)) {
pending.delete(id);
reject(new Error(`Timeout waiting for ${method}`));
}
}, 10000);
});
}
async function main() {
try {
log('开始 NodeSeek 朦胧账号签到...');
// 获取 CDP WebSocket URL
const wsUrl = await getBrowserWsUrl();
log(`CDP URL: ${wsUrl}`);
// 连接 CDP
ws = new WebSocket(wsUrl);
// 处理所有消息
ws.on('message', (data) => {
const msg = JSON.parse(data);
// 处理命令响应
if (msg.id !== undefined && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
if (msg.error) reject(msg.error);
else resolve(msg.result);
}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('CDP 连接成功');
// 创建新标签页
const target = await send('Target.createTarget', { url: 'about:blank' });
targetId = target.targetId;
log(`创建标签页: ${targetId}`);
// 附着到标签页
const session = await send('Target.attachToTarget', { targetId, flatten: true });
sessionId = session.sessionId;
log(`附着到会话: ${sessionId}`);
// 启用必要域
await send('Page.enable');
await send('Runtime.enable');
await send('Network.enable');
log('启用域成功');
// 设置 cookie
await send('Network.setCookie', {
name: '_nk',
value: COOKIE,
domain: '.nodeseek.com',
path: '/'
});
log('Cookie 设置成功');
// 设置页面加载监听
const loadPromise = new Promise(resolve => {
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.method === 'Page.loadEventFired') {
resolve();
}
});
});
// 导航
await send('Page.navigate', { url: TARGET_URL });
log(`导航到 ${TARGET_URL}`);
await loadPromise;
log('页面加载完成');
// 等待 8 秒
await new Promise(r => setTimeout(r, 8000));
log('等待 8 秒完成');
// 执行签到
const result = await send('Runtime.evaluate', {
expression: `fetch('/api/attendance',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({random:true})
}).then(r=>r.json()).catch(e=>({error:e.message}))`,
returnByValue: true,
awaitPromise: true
});
const signResult = result.result?.value;
log(`签到结果: ${JSON.stringify(signResult)}`);
// 关闭标签页
await send('Target.closeTarget', { targetId });
log('标签页已关闭');
ws.close();
log('签到完成');
return signResult;
} catch (err) {
log(`错误: ${err.message}`);
if (ws) ws.close();
throw err;
}
}
main().catch(() => process.exit(1));

159
scripts/nodeseek-checkin.js Normal file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env node
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const CDP_URL = 'ws://127.0.0.1:18800/devtools/browser/61ae08b1-1fab-420a-a3df-c5635446065e';
const COOKIE = '_nk=dc4d1551406351a93c09082ea08e2d2e';
const TARGET_URL = 'https://www.nodeseek.com';
const LOG_FILE = path.join(__dirname, 'checkin.log');
let ws;
let sessionId;
let targetId;
let messageId = 1;
function log(msg) {
const timestamp = new Date().toISOString();
const logMsg = `[${timestamp}] ${msg}\n`;
console.log(logMsg.trim());
fs.appendFileSync(LOG_FILE, logMsg);
}
function send(method, params = {}) {
const id = messageId++;
const message = JSON.stringify({ id, method, params, sessionId });
ws.send(message);
return id;
}
function waitForMessage(id) {
return new Promise((resolve) => {
const handler = (data) => {
const msg = JSON.parse(data);
if (msg.id === id) {
ws.removeListener('message', handler);
resolve(msg);
}
};
ws.on('message', handler);
});
}
async function main() {
try {
log('开始 NodeSeek 朦胧签到');
// Connect to CDP
ws = new WebSocket(CDP_URL);
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('已连接到 Chrome CDP');
// Create new tab
const createId = send('Target.createTarget', { url: 'about:blank' });
const createResp = await waitForMessage(createId);
targetId = createResp.result.targetId;
log(`创建标签页: ${targetId}`);
// Attach to target
const attachId = send('Target.attachToTarget', { targetId, flatten: true });
const attachResp = await waitForMessage(attachId);
sessionId = attachResp.result.sessionId;
log(`附加到会话: ${sessionId}`);
// Enable necessary domains
send('Network.enable');
send('Page.enable');
send('Runtime.enable');
// Set cookie
const cookieId = send('Network.setCookie', {
name: '_nk',
value: 'dc4d1551406351a93c09082ea08e2d2e',
domain: '.nodeseek.com',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'Lax'
});
await waitForMessage(cookieId);
log('已设置 cookie');
// Navigate to NodeSeek
const navId = send('Page.navigate', { url: TARGET_URL });
await waitForMessage(navId);
log(`导航到 ${TARGET_URL}`);
// Wait for page load
await new Promise((resolve) => {
const handler = (data) => {
const msg = JSON.parse(data);
if (msg.method === 'Page.loadEventFired') {
ws.removeListener('message', handler);
resolve();
}
};
ws.on('message', handler);
});
log('页面加载完成');
// Wait 8 seconds
await new Promise(resolve => setTimeout(resolve, 8000));
log('等待 8 秒后执行签到');
// Execute check-in
const evalId = send('Runtime.evaluate', {
expression: `
fetch('/api/attendance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ random: true })
})
.then(async r => {
const text = await r.text();
try {
return JSON.stringify(JSON.parse(text));
} catch {
return JSON.stringify({ status: r.status, body: text.substring(0, 200) });
}
})
.catch(e => JSON.stringify({ error: e.message }))
`,
awaitPromise: true,
returnByValue: true
});
const evalResp = await waitForMessage(evalId);
if (evalResp.result && evalResp.result.result) {
const result = evalResp.result.result.value;
log(`签到结果: ${result}`);
} else {
log(`签到失败: ${JSON.stringify(evalResp)}`);
}
// Close tab
const closeId = send('Target.closeTarget', { targetId });
await waitForMessage(closeId);
log('已关闭标签页');
ws.close();
log('NodeSeek 朦胧签到完成');
} catch (error) {
log(`签到出错: ${error.message}`);
if (ws && ws.readyState === WebSocket.OPEN) {
if (targetId) {
send('Target.closeTarget', { targetId });
}
ws.close();
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,206 @@
import WebSocket from 'ws';
import { appendFileSync } from 'fs';
const CDP_PORT = 18800;
const COOKIE_VALUE = 'dc4d1551406351a93c09082ea08e2d2e';
const TARGET_URL = 'https://www.nodeseek.com';
const LOG_FILE = '/Users/jianzhang/.openclaw/workspace/scripts/checkin.log';
let msgId = 1;
let pendingCallbacks = new Map();
let loadEventResolve = null;
function log(message) {
const timestamp = new Date().toISOString();
const logLine = `[${timestamp}] ${message}\n`;
console.log(logLine.trim());
try {
appendFileSync(LOG_FILE, logLine);
} catch (e) {
console.error('Failed to write log:', e.message);
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function send(ws, method, params = {}) {
return new Promise((resolve, reject) => {
const id = msgId++;
const msg = JSON.stringify({ id, method, params });
pendingCallbacks.set(id, { resolve, reject });
ws.send(msg);
setTimeout(() => {
if (pendingCallbacks.has(id)) {
pendingCallbacks.delete(id);
reject(new Error(`Timeout for ${method}`));
}
}, 30000);
});
}
async function main() {
log('开始 NodeSeek 朦胧签到任务');
let ws;
let targetId = null;
try {
// Step 1: Get browser WebSocket URL
log('获取 CDP 连接信息...');
const resp = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
const version = await resp.json();
const browserWsUrl = version.webSocketDebuggerUrl;
log(`Browser WS: ${browserWsUrl}`);
// Step 2: Connect to browser
ws = new WebSocket(browserWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
// Handle command responses
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
// Handle load event
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {
// Ignore
}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('CDP 连接成功');
// Step 3: Create new target
log('创建新标签页...');
const targetResult = await send(ws, 'Target.createTarget', {
url: 'about:blank'
});
targetId = targetResult.targetId;
log(`Target ID: ${targetId}`);
// Step 4: Get target's WebSocket URL via HTTP
const targetsResp = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
const targets = await targetsResp.json();
const pageTarget = targets.find(t => t.id === targetId);
if (!pageTarget) {
throw new Error('找不到创建的标签页');
}
const pageWsUrl = pageTarget.webSocketDebuggerUrl;
log(`Page WS: ${pageWsUrl}`);
// Step 5: Close browser connection and connect to page
ws.close();
await sleep(500);
ws = new WebSocket(pageWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {
// Ignore
}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('已连接到页面');
// Step 6: Enable domains
await send(ws, 'Page.enable');
await send(ws, 'Network.enable');
// Step 7: Set cookie
log('设置 Cookie...');
await send(ws, 'Network.setCookie', {
name: '_nk',
value: COOKIE_VALUE,
domain: '.nodeseek.com',
path: '/'
});
log('Cookie 设置成功');
// Step 8: Navigate
log('导航到 NodeSeek...');
const loadPromise = new Promise(resolve => { loadEventResolve = resolve; });
await send(ws, 'Page.navigate', { url: TARGET_URL });
log('等待页面加载...');
await loadPromise;
log('页面加载完成');
// Step 9: Wait 8 seconds
log('等待 8 秒...');
await sleep(8000);
// Step 10: Call attendance API
log('调用签到 API...');
const evalResult = await send(ws, 'Runtime.evaluate', {
expression: `fetch('/api/attendance',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({random:true})
}).then(r => r.json()).catch(e => ({error: e.message}))`,
returnByValue: true,
awaitPromise: true
});
const result = evalResult.result.value;
log(`签到结果: ${JSON.stringify(result)}`);
// Step 11: Close page
log('关闭标签页...');
await send(ws, 'Page.close');
ws.close();
log('签到任务完成 ✓');
} catch (error) {
log(`错误: ${error.message}`);
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.close();
} catch (e) {}
}
process.exit(1);
}
}
main();

95
scripts/nodeseek-checkin.py Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# NodeSeek 签到 - 增强版(模拟真实浏览器)
import json
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
STATE_FILE = SCRIPT_DIR / "nodeseek-checkin-state.json"
ACCOUNTS = {
"朦胧": "dc4d1551406351a93c09082ea08e2d2e",
"VP404": "3cfeb30b562daec31ba63bf64fdb3838"
}
def checkin_account(name, cookie):
print(f"[{name}] 签到中...")
try:
req = urllib.request.Request(
"https://www.nodeseek.com/api/attendance?random=false",
method="POST",
headers={
"Cookie": f"_nk={cookie}",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.nodeseek.com/",
"Origin": "https://www.nodeseek.com",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"sec-ch-ua": '"Google Chrome";v="145", "Chromium";v="145", "Not-A.Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"'
}
)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode())
if data.get("success"):
reward = data.get("data", "未知")
print(f"[{name}] ✅ 签到成功!奖励: {reward}")
return {"status": "success", "reward": reward, "time": datetime.now().isoformat()}
else:
message = data.get("message", "未知错误")
print(f"[{name}] ❌ 签到失败: {message}")
return {"status": "failed", "error": message, "time": datetime.now().isoformat()}
except urllib.error.HTTPError as e:
print(f"[{name}] ❌ HTTP {e.code}: {e.reason}")
return {"status": "error", "error": f"HTTP {e.code}", "time": datetime.now().isoformat()}
except Exception as e:
print(f"[{name}] ❌ 请求失败: {e}")
return {"status": "error", "error": str(e), "time": datetime.now().isoformat()}
def main():
state = {}
if STATE_FILE.exists():
state = json.loads(STATE_FILE.read_text())
today = datetime.now().strftime("%Y-%m-%d")
if state.get("lastCheckin") == today and state.get("accounts"):
print(f"今天已经签到过了 ({today})")
for name, result in state.get("accounts", {}).items():
status = result.get("status")
detail = result.get("reward") or result.get("error", "N/A")
print(f" {name}: {status} ({detail})")
return
print(f"开始签到 NodeSeek ({today})...")
results = {}
for name, cookie in ACCOUNTS.items():
results[name] = checkin_account(name, cookie)
import time
time.sleep(3)
state["lastCheckin"] = today
state["accounts"] = results
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
print("\n签到完成!")
for name, result in results.items():
status = result.get("status")
detail = result.get("reward") or result.get("error", "N/A")
print(f" {name}: {status} ({detail})")
if __name__ == "__main__":
main()

79
scripts/nodeseek-checkin.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
# NodeSeek 自动签到脚本 v2.0
# 支持多账号、CDP 控制、状态记录
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATE_FILE="$SCRIPT_DIR/nodeseek-checkin-state.json"
CDP_URL="http://127.0.0.1:18800"
# 账号配置
declare -A ACCOUNTS=(
["menglong"]="dc4d1551406351a93c09082ea08e2d2e"
["vp404"]="3cfeb30b562daec31ba63bf64fdb3838"
)
declare -A ACCOUNT_NAMES=(
["menglong"]="朦胧"
["vp404"]="VP404"
)
# 初始化状态文件
if [ ! -f "$STATE_FILE" ]; then
echo '{"lastCheckin": null, "accounts": {}}' > "$STATE_FILE"
fi
TODAY=$(date '+%Y-%m-%d')
LAST_CHECKIN=$(jq -r '.lastCheckin // "never"' "$STATE_FILE")
if [ "$LAST_CHECKIN" = "$TODAY" ]; then
echo "今天已经签到过了 ($TODAY)"
jq -r '.accounts | to_entries[] | " \(.key): \(.value.status) (\(.value.reward // "N/A"))"' "$STATE_FILE"
exit 0
fi
echo "开始签到 NodeSeek ($TODAY)..."
# 签到函数
checkin_account() {
local key=$1
local cookie=$2
local name=${ACCOUNT_NAMES[$key]}
echo "[$name] 签到中..."
# 使用 curl 签到
response=$(curl -s -X POST "https://www.nodeseek.com/api/attendance?random=false" \
-H "Cookie: _nk=$cookie" \
-H "User-Agent: Mozilla/5.0" \
-H "Accept: application/json")
success=$(echo "$response" | jq -r '.success // false')
message=$(echo "$response" | jq -r '.message // "未知错误"')
if [ "$success" = "true" ]; then
reward=$(echo "$response" | jq -r '.data // "未知"')
echo "[$name] ✅ 签到成功!奖励: $reward"
jq --arg name "$name" --arg reward "$reward" \
'.accounts[$name] = {status: "success", reward: $reward, time: now|todate}' \
"$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
else
echo "[$name] ❌ 签到失败: $message"
jq --arg name "$name" --arg msg "$message" \
'.accounts[$name] = {status: "failed", error: $msg, time: now|todate}' \
"$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
fi
}
# 遍历所有账号签到
for key in "${!ACCOUNTS[@]}"; do
checkin_account "$key" "${ACCOUNTS[$key]}"
sleep 2
done
# 更新最后签到日期
jq --arg today "$TODAY" '.lastCheckin = $today' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
echo ""
echo "签到完成!"
jq -r '.accounts | to_entries[] | " \(.key): \(.value.status) (\(.value.reward // .value.error))"' "$STATE_FILE"

View File

@@ -0,0 +1,206 @@
import WebSocket from 'ws';
import { appendFileSync } from 'fs';
const CDP_PORT = 18800;
const COOKIE_VALUE = '3cfeb30b562daec31ba63bf64fdb3838';
const TARGET_URL = 'https://www.nodeseek.com';
const LOG_FILE = '/Users/jianzhang/.openclaw/workspace/scripts/checkin.log';
let msgId = 1;
let pendingCallbacks = new Map();
let loadEventResolve = null;
function log(message) {
const timestamp = new Date().toISOString();
const logLine = `[${timestamp}] [VP404] ${message}\n`;
console.log(logLine.trim());
try {
appendFileSync(LOG_FILE, logLine);
} catch (e) {
console.error('Failed to write log:', e.message);
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function send(ws, method, params = {}) {
return new Promise((resolve, reject) => {
const id = msgId++;
const msg = JSON.stringify({ id, method, params });
pendingCallbacks.set(id, { resolve, reject });
ws.send(msg);
setTimeout(() => {
if (pendingCallbacks.has(id)) {
pendingCallbacks.delete(id);
reject(new Error(`Timeout for ${method}`));
}
}, 30000);
});
}
async function main() {
log('开始 NodeSeek VP404 签到任务');
let ws;
let targetId = null;
try {
// Step 1: Get browser WebSocket URL
log('获取 CDP 连接信息...');
const resp = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
const version = await resp.json();
const browserWsUrl = version.webSocketDebuggerUrl;
log(`Browser WS: ${browserWsUrl}`);
// Step 2: Connect to browser
ws = new WebSocket(browserWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
// Handle command responses
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
// Handle load event
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {
// Ignore
}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('CDP 连接成功');
// Step 3: Create new target
log('创建新标签页...');
const targetResult = await send(ws, 'Target.createTarget', {
url: 'about:blank'
});
targetId = targetResult.targetId;
log(`Target ID: ${targetId}`);
// Step 4: Get target's WebSocket URL via HTTP
const targetsResp = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
const targets = await targetsResp.json();
const pageTarget = targets.find(t => t.id === targetId);
if (!pageTarget) {
throw new Error('找不到创建的标签页');
}
const pageWsUrl = pageTarget.webSocketDebuggerUrl;
log(`Page WS: ${pageWsUrl}`);
// Step 5: Close browser connection and connect to page
ws.close();
await sleep(500);
ws = new WebSocket(pageWsUrl);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.id && pendingCallbacks.has(msg.id)) {
const { resolve, reject } = pendingCallbacks.get(msg.id);
pendingCallbacks.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
} else {
resolve(msg.result);
}
}
if (msg.method === 'Page.loadEventFired' && loadEventResolve) {
loadEventResolve();
loadEventResolve = null;
}
} catch (e) {
// Ignore
}
});
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
log('已连接到页面');
// Step 6: Enable domains
await send(ws, 'Page.enable');
await send(ws, 'Network.enable');
// Step 7: Set cookie
log('设置 Cookie...');
await send(ws, 'Network.setCookie', {
name: '_nk',
value: COOKIE_VALUE,
domain: '.nodeseek.com',
path: '/'
});
log('Cookie 设置成功');
// Step 8: Navigate
log('导航到 NodeSeek...');
const loadPromise = new Promise(resolve => { loadEventResolve = resolve; });
await send(ws, 'Page.navigate', { url: TARGET_URL });
log('等待页面加载...');
await loadPromise;
log('页面加载完成');
// Step 9: Wait 8 seconds
log('等待 8 秒...');
await sleep(8000);
// Step 10: Call attendance API
log('调用签到 API...');
const evalResult = await send(ws, 'Runtime.evaluate', {
expression: `fetch('/api/attendance',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({random:true})
}).then(r => r.json()).catch(e => ({error: e.message}))`,
returnByValue: true,
awaitPromise: true
});
const result = evalResult.result.value;
log(`签到结果: ${JSON.stringify(result)}`);
// Step 11: Close page
log('关闭标签页...');
await send(ws, 'Page.close');
ws.close();
log('签到任务完成 ✓');
} catch (error) {
log(`错误: ${error.message}`);
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.close();
} catch (e) {}
}
process.exit(1);
}
}
main();

120
scripts/oc-status.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
# OpenClaw 状态监控 - 生成静态 HTML
OUT="${1:-/tmp/oc-status.html}"
CONFIG="$HOME/.openclaw/openclaw.json"
# 获取基础信息
GW_PID=$(pgrep -f "openclaw.*gateway" | head -1)
GW_STATUS="offline"; GW_UPTIME=""
if [ -n "$GW_PID" ]; then
GW_STATUS="online"
if [ "$(uname)" = "Darwin" ]; then
GW_START=$(ps -o lstart= -p "$GW_PID" 2>/dev/null)
else
GW_START=$(ps -o etimes= -p "$GW_PID" 2>/dev/null | xargs)
fi
fi
# 获取 session 信息
SESSIONS=$(cat ~/.openclaw/agents/main/sessions/sessions.json 2>/dev/null)
SESSION_COUNT=$(echo "$SESSIONS" | python3 -c "import json,sys;d=json.load(sys.stdin);print(len(d.get('sessions',{})))" 2>/dev/null || echo "?")
# 获取模型信息
MODEL_JSON=$(python3 -c "
import json
with open('$CONFIG') as f: c=json.load(f)
m=c.get('models',{})
default=m.get('default','')
providers=[]
for name,p in m.get('providers',{}).items():
if not isinstance(p,dict): continue
for mod in p.get('models',[]):
providers.append({
'provider':name,
'model':mod.get('id',''),
'name':mod.get('name',''),
'api':p.get('api',''),
'ctx':mod.get('contextWindow',0),
'maxTok':mod.get('maxTokens',0),
'base':p.get('baseUrl','')[:60]
})
print(json.dumps({'default':default,'providers':providers}))
" 2>/dev/null)
# 生成 HTML
python3 << 'PYEOF' > "$OUT"
import json,datetime,os
gw_status = os.environ.get("GW_STATUS","offline")
session_count = os.environ.get("SESSION_COUNT","?")
model_json = os.environ.get("MODEL_JSON","{}")
try:
data = json.loads(model_json)
except:
data = {"default":"","providers":[]}
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rows = ""
for p in data.get("providers",[]):
badge = "primary" if p["provider"] in ["newcli","terminalpub","bookapi"] else "secondary"
rows += f"""<tr>
<td><span class="badge {badge}">{p['provider']}</span></td>
<td><code>{p['model']}</code></td>
<td>{p['name']}</td>
<td>{p['api']}</td>
<td>{p['ctx']//1000}k</td>
<td>{p['maxTok']//1000}k</td>
<td class="url">{p['base']}</td>
</tr>"""
dot = "green" if gw_status == "online" else "red"
print(f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OpenClaw Monitor</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font-family:-apple-system,system-ui,sans-serif;background:#0d1117;color:#c9d1d9;padding:20px}}
.container{{max-width:1000px;margin:0 auto}}
h1{{font-size:1.5em;margin-bottom:20px;color:#58a6ff}}
.cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:24px}}
.card{{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px}}
.card .label{{font-size:.8em;color:#8b949e;margin-bottom:4px}}
.card .value{{font-size:1.4em;font-weight:600}}
.dot{{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px}}
.dot.green{{background:#3fb950}}.dot.red{{background:#f85149}}
table{{width:100%;border-collapse:collapse;background:#161b22;border:1px solid #30363d;border-radius:8px;overflow:hidden}}
th{{background:#21262d;text-align:left;padding:10px 12px;font-size:.85em;color:#8b949e}}
td{{padding:8px 12px;border-top:1px solid #30363d;font-size:.85em}}
code{{background:#30363d;padding:2px 6px;border-radius:4px;font-size:.85em}}
.badge{{padding:2px 8px;border-radius:10px;font-size:.75em;font-weight:600}}
.badge.primary{{background:#1f6feb;color:#fff}}.badge.secondary{{background:#30363d;color:#8b949e}}
.url{{color:#8b949e;font-size:.75em;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
.footer{{margin-top:16px;font-size:.75em;color:#484f58;text-align:center}}
</style>
</head>
<body>
<div class="container">
<h1>🐾 OpenClaw Monitor</h1>
<div class="cards">
<div class="card"><div class="label">Gateway</div><div class="value"><span class="dot {dot}"></span>{gw_status}</div></div>
<div class="card"><div class="label">Sessions</div><div class="value">{session_count}</div></div>
<div class="card"><div class="label">Default Model</div><div class="value" style="font-size:1em">{data.get('default','auto')}</div></div>
<div class="card"><div class="label">Providers</div><div class="value">{len(data.get('providers',[]))}</div></div>
</div>
<table>
<thead><tr><th>Provider</th><th>Model ID</th><th>Name</th><th>API</th><th>Context</th><th>Max Out</th><th>Endpoint</th></tr></thead>
<tbody>{rows}</tbody>
</table>
<div class="footer">Updated: {now} · OpenClaw Status Monitor</div>
</div>
</body>
</html>""")
PYEOF
echo "Generated: $OUT"

186
scripts/peekabo-monitor.js Normal file
View File

@@ -0,0 +1,186 @@
/**
* Peekabo Networks 自动续费监控脚本
* 每天 00:05 检测账单,有账单自动用余额续费
* 续费失败通知 Telegram
*/
const https = require('https');
// ========== 配置 ==========
const ACCOUNTS = [
{ email: 'mail@mailpre.com', password: 'hawvys-Vitcy7-kyxzuf' },
{ email: 'mf0@msn.com', password: '@a110110' },
{ email: 'yxvmhk@qq.com', password: '@a110110' }
];
const TG_BOT_TOKEN = '8300905342:AAH3Q78FuR5Exrw2zWYJHFRyVeLlws3xnww';
const TG_CHAT_ID = '165067365';
const BASE_URL = 'gigo.peekabo.io';
// ========== Telegram 通知 ==========
async function sendTG(message) {
const data = JSON.stringify({ chat_id: TG_CHAT_ID, text: message, parse_mode: 'HTML' });
return new Promise((resolve) => {
const req = https.request({
hostname: 'api.telegram.org',
path: `/bot${TG_BOT_TOKEN}/sendMessage`,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve(b)); });
req.write(data);
req.end();
});
}
// ========== HTTP 请求 ==========
function request(options, postData = null) {
return new Promise((resolve, reject) => {
const req = https.request(options, res => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({
status: res.statusCode,
headers: res.headers,
body,
cookies: res.headers['set-cookie'] || []
}));
});
req.on('error', reject);
if (postData) req.write(postData);
req.end();
});
}
// ========== 检测单个账户 ==========
async function checkAccount(account) {
const result = { email: account.email, invoices: [], paid: [], failed: [], error: null };
let cookies = '';
try {
// 1. 获取登录页面拿 token
const loginPage = await request({
hostname: BASE_URL,
path: '/index.php?rp=/login',
method: 'GET'
});
const tokenMatch = loginPage.body.match(/name="token" value="([^"]+)"/);
if (!tokenMatch) { result.error = '获取token失败'; return result; }
cookies = loginPage.cookies.map(c => c.split(';')[0]).join('; ');
// 2. 登录
const loginData = `token=${tokenMatch[1]}&username=${encodeURIComponent(account.email)}&password=${encodeURIComponent(account.password)}`;
const loginRes = await request({
hostname: BASE_URL,
path: '/index.php?rp=/login',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookies
}
}, loginData);
// 更新 cookies
if (loginRes.cookies.length) {
cookies = loginRes.cookies.map(c => c.split(';')[0]).join('; ');
}
// 3. 检查是否登录成功 - 访问客户区
const dashboard = await request({
hostname: BASE_URL,
path: '/clientarea.php',
method: 'GET',
headers: { 'Cookie': cookies }
});
if (!dashboard.body.includes('My Dashboard') && !dashboard.body.includes('Logout')) {
result.error = '登录失败';
return result;
}
// 4. 获取未付账单
const invoicesPage = await request({
hostname: BASE_URL,
path: '/clientarea.php?action=invoices',
method: 'GET',
headers: { 'Cookie': cookies }
});
// 匹配未付账单
const unpaidRegex = /viewinvoice\.php\?id=(\d+)[^>]*>.*?#\d+.*?<\/a>.*?\$([0-9.]+).*?Unpaid/gs;
let match;
while ((match = unpaidRegex.exec(invoicesPage.body)) !== null) {
result.invoices.push({ id: match[1], amount: parseFloat(match[2]) });
}
// 5. 如果有未付账单,尝试用余额支付
for (const inv of result.invoices) {
const payRes = await request({
hostname: BASE_URL,
path: `/viewinvoice.php?id=${inv.id}`,
method: 'GET',
headers: { 'Cookie': cookies }
});
// 检查是否有 Apply Credit 按钮
if (payRes.body.includes('applycredit')) {
const creditRes = await request({
hostname: BASE_URL,
path: `/viewinvoice.php?id=${inv.id}&applycredit=true`,
method: 'GET',
headers: { 'Cookie': cookies }
});
if (creditRes.body.includes('Paid') || creditRes.status === 302) {
result.paid.push(inv);
} else {
result.failed.push({ ...inv, reason: '支付失败' });
}
} else {
result.failed.push({ ...inv, reason: '无法使用余额' });
}
}
} catch (err) {
result.error = err.message;
}
return result;
}
// ========== 主函数 ==========
async function main() {
console.log(`[${new Date().toISOString()}] 开始检测...`);
let msg = '🔔 <b>Peekabo 账单检测</b>\n\n';
let needNotify = false;
for (const acc of ACCOUNTS) {
const r = await checkAccount(acc);
msg += `📧 <code>${r.email}</code>\n`;
if (r.error) {
msg += `${r.error}\n`;
needNotify = true;
} else if (r.invoices.length === 0) {
msg += `✅ 无待付账单\n`;
} else {
if (r.paid.length) msg += `✅ 已付: ${r.paid.map(i => '$' + i.amount).join(', ')}\n`;
if (r.failed.length) {
msg += `❌ 失败: ${r.failed.map(i => '$' + i.amount).join(', ')}\n`;
needNotify = true;
}
}
msg += '\n';
}
if (needNotify) {
await sendTG(msg);
console.log('已通知');
} else {
console.log('全部正常');
}
}
main().catch(console.error);

View File

@@ -0,0 +1,7 @@
# Mac Guard 防火墙规则
# 封禁爆破 IP
table <bruteforce> persist
# 封禁 bruteforce 表中的 IP
block drop quick from <bruteforce>

View File

@@ -0,0 +1,13 @@
#!/data/data/com.termux/files/usr/bin/bash
# Pixel 6 OpenClaw 开机自启脚本(带守护进程)
# 等待网络就绪
sleep 10
# 启动 termux-wake-lock 防止休眠
termux-wake-lock
# 启动守护进程(会自动启动 OpenClaw
nohup bash ~/watchdog.sh > ~/watchdog.log 2>&1 &
echo "OpenClaw 守护进程已启动 at $(date)" >> ~/openclaw-boot.log

View File

@@ -0,0 +1,58 @@
#!/data/data/com.termux/files/usr/bin/bash
# Pixel 6 Chromium + CDP 配置脚本
echo "=== 配置 Chromium + CDP 控制 ==="
# 1. 在 Ubuntu 容器里安装 Chromium
echo "[1/3] 安装 Chromium..."
proot-distro login ubuntu -- bash -c '
apt update
apt install -y chromium-browser
'
# 2. 创建 Chromium 启动脚本(带 CDP
echo "[2/3] 创建 CDP 启动脚本..."
cat > ~/start-chromium-cdp.sh << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
# 启动 X11 服务器
termux-x11 :0 &
sleep 2
# 设置 DISPLAY
export DISPLAY=:0
# 进入 Ubuntu 容器启动 Chromium开启远程调试
proot-distro login ubuntu -- bash -c '
export DISPLAY=:0
chromium-browser \
--remote-debugging-port=9222 \
--no-first-run \
--no-default-browser-check \
--disable-gpu \
--disable-software-rasterizer \
--disable-dev-shm-usage \
--user-data-dir=/tmp/chromium-profile \
&
'
echo "Chromium 已启动CDP 端口: 9222"
echo "OpenClaw 可以通过 http://192.168.1.138:9222 控制浏览器"
EOF
chmod +x ~/start-chromium-cdp.sh
# 3. 更新 OpenClaw 配置
echo "[3/3] 更新 OpenClaw 配置..."
proot-distro login ubuntu -- bash -c '
# 这里可以添加浏览器配置到 openclaw.json
echo "配置完成"
'
echo ""
echo "✅ 完成!"
echo ""
echo "使用方法:"
echo "1. 确保已安装 Termux:X11 应用"
echo "2. 运行: bash ~/start-chromium-cdp.sh"
echo "3. Chromium 会显示在屏幕上,同时开启 CDP 端口 9222"
echo "4. OpenClaw 可以通过 browser 工具控制它"

View File

@@ -0,0 +1,40 @@
#!/data/data/com.termux/files/usr/bin/bash
# Pixel 6 图形界面浏览器安装脚本
echo "=== 安装 Termux:X11 + Firefox ==="
# 1. 安装 X11 相关包
echo "[1/4] 安装 X11 包..."
pkg install -y x11-repo
pkg install -y termux-x11-nightly
# 2. 在 Ubuntu 容器里安装 Firefox
echo "[2/4] 在 Ubuntu 容器里安装 Firefox..."
proot-distro login ubuntu -- bash -c '
apt update
apt install -y firefox dbus-x11
'
# 3. 创建启动脚本
echo "[3/4] 创建启动脚本..."
cat > ~/start-firefox.sh << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
# 启动 X11 服务器
termux-x11 :0 &
sleep 2
# 设置 DISPLAY
export DISPLAY=:0
# 进入 Ubuntu 容器启动 Firefox
proot-distro login ubuntu -- bash -c 'export DISPLAY=:0 && firefox'
EOF
chmod +x ~/start-firefox.sh
echo "[4/4] 完成!"
echo ""
echo "使用方法:"
echo "1. 从 F-Droid 安装 Termux:X11 应用"
echo "2. 运行: bash ~/start-firefox.sh"
echo "3. Firefox 会显示在 Pixel 6 屏幕上"

View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -e
echo "=== Pixel 6 OpenClaw 重装脚本 ==="
# 1. 安装 proot-distro
echo "[1/8] 安装 proot-distro..."
pkg install -y proot-distro
# 2. 安装 Ubuntu
echo "[2/8] 安装 Ubuntu..."
proot-distro install ubuntu
# 3. 进入 Ubuntu 并执行安装
echo "[3/8] 配置 Ubuntu 环境..."
proot-distro login ubuntu -- bash -c '
set -e
# 更新系统
echo "[4/8] 更新系统..."
apt update && apt upgrade -y
# 安装依赖
echo "[5/8] 安装 curl 和 git..."
apt install -y curl git
# 安装 Node.js 22
echo "[6/8] 安装 Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
# 验证安装
node -v
npm -v
# 安装 OpenClaw
echo "[7/8] 安装 OpenClaw..."
npm install -g openclaw@latest
# 创建网络接口修复脚本
echo "[8/8] 创建网络接口修复..."
cat <<EOF > /root/hijack.js
const os = require("os");
os.networkInterfaces = () => ({});
EOF
# 配置自动加载
echo "export NODE_OPTIONS=\"-r /root/hijack.js\"" >> ~/.bashrc
source ~/.bashrc
echo "✅ OpenClaw 安装完成!"
echo "下一步: 恢复配置文件"
'
echo "=== 安装完成 ==="

View File

@@ -0,0 +1,31 @@
#!/data/data/com.termux/files/usr/bin/bash
# Pixel 6 屏幕读取工具
# 用法: bash pixel6-screen-reader.sh [screenshot|ui|ocr]
ACTION=${1:-screenshot}
OUTPUT_DIR=~/.openclaw/workspace/data
case $ACTION in
screenshot)
# 截图
screencap -p $OUTPUT_DIR/screen-latest.png
echo "Screenshot saved to $OUTPUT_DIR/screen-latest.png"
;;
ui)
# 读取界面元素
uiautomator dump $OUTPUT_DIR/ui-latest.xml 2>&1
cat $OUTPUT_DIR/ui-latest.xml
;;
ocr)
# 截图 + 保存(需要外部 OCR 工具)
screencap -p $OUTPUT_DIR/screen-for-ocr.png
echo "Screenshot saved for OCR: $OUTPUT_DIR/screen-for-ocr.png"
;;
*)
echo "Usage: $0 [screenshot|ui|ocr]"
exit 1
;;
esac

View File

@@ -0,0 +1,25 @@
#!/data/data/com.termux/files/usr/bin/bash
# OpenClaw 守护进程 - 自动重启
while true; do
# 检查 OpenClaw 是否在运行
if ! proot-distro login ubuntu -- bash -c 'pgrep -f openclaw' > /dev/null 2>&1; then
echo "[$(date)] OpenClaw 已停止,正在重启..."
# 清理旧进程
pkill -9 openclaw 2>/dev/null
pkill -9 node 2>/dev/null
# 启动 OpenClaw
proot-distro login ubuntu -- bash -c '
export NODE_OPTIONS="-r /root/hijack.js"
cd /root
nohup openclaw gateway --verbose > /tmp/openclaw.log 2>&1 &
'
echo "[$(date)] OpenClaw 已重启"
fi
# 每 30 秒检查一次
sleep 30
done

614
scripts/ss-rust.sh Normal file
View File

@@ -0,0 +1,614 @@
#!/bin/bash
# SS-Rust 一键安装脚本
# 支持: ss2022 (2022-blake3-aes-128-gcm) / ss128 (aes-128-gcm) / 双节点
# 自动生成 SS订阅 + Surge + Clash 配置
# GitHub: https://github.com/mango082888-bit/ss-rust
# ============ 颜色 ============
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# ============ 全局变量 ============
SERVER_IP=""
PORT_2022=""
KEY_2022=""
METHOD_2022=""
PORT_RAW=""
KEY_RAW=""
METHOD_RAW=""
NODE_MODE=""
# ============ 基础检测 ============
check_root() {
[[ $EUID -ne 0 ]] && error "请使用 root 用户运行"
}
get_pkg_manager() {
if command -v apt &>/dev/null; then
PKG="apt"
elif command -v yum &>/dev/null; then
PKG="yum"
elif command -v apk &>/dev/null; then
PKG="apk"
else
error "不支持的包管理器"
fi
}
install_deps() {
info "安装依赖..."
case $PKG in
apt) apt update -qq &>/dev/null; apt install -y -qq curl openssl xz-utils tar chrony python3 &>/dev/null ;;
yum) yum install -y -q curl openssl xz tar chrony python3 &>/dev/null ;;
apk) apk add --quiet curl openssl xz tar chrony python3 &>/dev/null ;;
esac
}
get_arch() {
case $(uname -m) in
x86_64) echo "x86_64-unknown-linux-gnu" ;;
aarch64) echo "aarch64-unknown-linux-gnu" ;;
armv7l) echo "armv7-unknown-linux-gnueabihf" ;;
*) error "不支持的架构: $(uname -m)" ;;
esac
}
get_ip() {
local ip
ip=$(curl -s4m5 ip.sb 2>/dev/null || curl -s4m5 ifconfig.me 2>/dev/null || curl -s4m5 ipinfo.io/ip 2>/dev/null)
[[ -z "$ip" ]] && error "无法获取公网IP"
echo "$ip"
}
# ============ 时间同步 ============
sync_time() {
info "同步系统时间..."
if command -v timedatectl &>/dev/null; then
timedatectl set-ntp true 2>/dev/null || true
fi
if command -v chronyd &>/dev/null; then
systemctl enable --now chronyd 2>/dev/null || true
chronyc makestep 2>/dev/null || true
fi
info "当前时间: $(date '+%Y-%m-%d %H:%M:%S %Z')"
}
# ============ 安装 ss-rust ============
install_ssrust() {
info "安装 shadowsocks-rust..."
local arch_name
arch_name=$(get_arch)
local latest
latest=$(curl -sLm10 https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest | grep tag_name | head -1 | grep -oP 'v[\d.]+')
[[ -z "$latest" ]] && latest="v1.24.0"
info "版本: $latest"
local url="https://github.com/shadowsocks/shadowsocks-rust/releases/download/${latest}/shadowsocks-${latest}.${arch_name}.tar.xz"
cd /tmp
rm -f ss-rust-dl.tar.xz ssserver sslocal ssurl ssmanager ssservice
curl -sL "$url" -o ss-rust-dl.tar.xz
local fsize
fsize=$(stat -c%s ss-rust-dl.tar.xz 2>/dev/null || stat -f%z ss-rust-dl.tar.xz 2>/dev/null)
[[ "$fsize" -lt 100000 ]] && error "下载失败 (${fsize} bytes)"
file ss-rust-dl.tar.xz | grep -q "XZ" || error "下载的文件不是有效的 XZ 压缩包"
tar xf ss-rust-dl.tar.xz || error "解压失败"
[[ ! -f ssserver ]] && error "找不到 ssserver"
cp -f ssserver /usr/local/bin/
cp -f sslocal /usr/local/bin/ 2>/dev/null || true
chmod +x /usr/local/bin/ssserver /usr/local/bin/sslocal 2>/dev/null
rm -f ss-rust-dl.tar.xz ssserver sslocal ssurl ssmanager ssservice
/usr/local/bin/ssserver --version && info "shadowsocks-rust 安装完成" || error "安装验证失败"
}
# ============ 节点选择 + 端口密码 + 写配置 ============
select_and_configure() {
SERVER_IP=$(get_ip)
mkdir -p /etc/shadowsocks-rust
# 清空
PORT_2022="" ; KEY_2022="" ; METHOD_2022=""
PORT_RAW="" ; KEY_RAW="" ; METHOD_RAW=""
echo ""
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo -e "${CYAN} 🔐 选择节点类型${NC}"
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo ""
echo -e " ${GREEN}1.${NC} SS2022-128 (2022-blake3-aes-128-gcm) — 新协议,推荐"
echo -e " ${GREEN}2.${NC} SS-AES-128 (aes-128-gcm) — 传统协议,兼容性好"
echo -e " ${GREEN}3.${NC} 双节点全装 (SS2022 + SS128)"
echo ""
read -rp "请选择 [1-3] (默认3): " node_choice
node_choice=${node_choice:-3}
if [[ "$node_choice" == "1" || "$node_choice" == "3" ]]; then
local dp=$((RANDOM % 10000 + 20000))
local dk=$(openssl rand -base64 16)
echo ""
echo -e " ${GREEN}SS2022-128 配置:${NC}"
read -rp " 端口 [回车=${dp}]: " PORT_2022
PORT_2022=${PORT_2022:-$dp}
read -rp " 密码 [回车=${dk}]: " KEY_2022
KEY_2022=${KEY_2022:-$dk}
METHOD_2022="2022-blake3-aes-128-gcm"
fi
if [[ "$node_choice" == "2" || "$node_choice" == "3" ]]; then
local dp2=$((RANDOM % 10000 + 30000))
local dk2=$(openssl rand -base64 16)
echo ""
echo -e " ${GREEN}SS-AES-128 配置:${NC}"
read -rp " 端口 [回车=${dp2}]: " PORT_RAW
PORT_RAW=${PORT_RAW:-$dp2}
read -rp " 密码 [回车=${dk2}]: " KEY_RAW
KEY_RAW=${KEY_RAW:-$dk2}
METHOD_RAW="aes-128-gcm"
fi
# 写 JSON 配置
python3 -c "
import json
servers = []
if '${PORT_2022}':
servers.append({
'server': '0.0.0.0',
'server_port': int('${PORT_2022}'),
'method': '${METHOD_2022}',
'password': '${KEY_2022}',
'timeout': 300,
'fast_open': True
})
if '${PORT_RAW}':
servers.append({
'server': '0.0.0.0',
'server_port': int('${PORT_RAW}'),
'method': '${METHOD_RAW}',
'password': '${KEY_RAW}',
'timeout': 300,
'fast_open': True
})
with open('/etc/shadowsocks-rust/config.json', 'w') as f:
json.dump({'servers': servers}, f, indent=4)
print('OK')
" || error "生成配置失败"
info "配置文件: /etc/shadowsocks-rust/config.json"
}
# ============ systemd 服务 ============
setup_service() {
cat > /etc/systemd/system/ss-rust.service << 'EOF'
[Unit]
Description=Shadowsocks-Rust Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ssserver -c /etc/shadowsocks-rust/config.json
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now ss-rust
sleep 2
if systemctl is-active --quiet ss-rust; then
info "ss-rust 服务启动成功"
else
journalctl -u ss-rust -n 5 --no-pager
error "ss-rust 服务启动失败"
fi
}
# ============ 读取现有配置 ============
load_config() {
[[ ! -f /etc/shadowsocks-rust/config.json ]] && return 1
SERVER_IP=$(get_ip)
PORT_2022="" ; KEY_2022="" ; METHOD_2022=""
PORT_RAW="" ; KEY_RAW="" ; METHOD_RAW=""
eval $(python3 -c "
import json
with open('/etc/shadowsocks-rust/config.json') as f:
c = json.load(f)
for s in c['servers']:
m = s['method']
if '2022' in m:
print(f'PORT_2022={s[\"server_port\"]}')
print(f'KEY_2022={s[\"password\"]}')
print(f'METHOD_2022={m}')
else:
print(f'PORT_RAW={s[\"server_port\"]}')
print(f'KEY_RAW={s[\"password\"]}')
print(f'METHOD_RAW={m}')
" 2>/dev/null)
return 0
}
# ============ 生成订阅 ============
gen_subscribe() {
load_config || return
local sub_dir="/etc/shadowsocks-rust/subscribe"
mkdir -p "$sub_dir"
local uris="" surge="" clash="" info_txt=""
if [[ -n "$PORT_2022" ]]; then
local uri="ss://$(echo -n "${METHOD_2022}:${KEY_2022}" | base64 -w0)@${SERVER_IP}:${PORT_2022}#SS2022-128"
URI_2022="$uri"
uris="${uris}${uri}\n"
surge="${surge}SS2022-128 = ss, ${SERVER_IP}, ${PORT_2022}, encrypt-method=${METHOD_2022}, password=${KEY_2022}\n"
clash="${clash} - name: SS2022-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_2022}\n cipher: ${METHOD_2022}\n password: \"${KEY_2022}\"\n\n"
info_txt="${info_txt}【SS2022-AES-128】新协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_2022}\n 加密: ${METHOD_2022}\n 密码: ${KEY_2022}\n\n"
fi
if [[ -n "$PORT_RAW" ]]; then
local uri="ss://$(echo -n "${METHOD_RAW}:${KEY_RAW}" | base64 -w0)@${SERVER_IP}:${PORT_RAW}#SS-AES-128"
URI_RAW="$uri"
uris="${uris}${uri}\n"
surge="${surge}SS-AES-128 = ss, ${SERVER_IP}, ${PORT_RAW}, encrypt-method=${METHOD_RAW}, password=${KEY_RAW}\n"
clash="${clash} - name: SS-AES-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_RAW}\n cipher: ${METHOD_RAW}\n password: \"${KEY_RAW}\"\n\n"
info_txt="${info_txt}【SS-AES-128】传统协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_RAW}\n 加密: ${METHOD_RAW}\n 密码: ${KEY_RAW}\n\n"
fi
echo -e "$uris" | base64 -w0 > "$sub_dir/subscribe.txt"
echo -e "# Surge SS | $(date '+%Y-%m-%d %H:%M:%S') | ${SERVER_IP}\n[Proxy]\n${surge}" > "$sub_dir/surge.conf"
echo -e "# Clash SS | $(date '+%Y-%m-%d %H:%M:%S')\nproxies:\n${clash}" > "$sub_dir/clash.yaml"
echo -e "═══ SS-Rust 节点 | $(date '+%Y-%m-%d %H:%M:%S') | ${SERVER_IP} ═══\n\n${info_txt}\n【SS 链接】\n${uris}" > "$sub_dir/info.txt"
}
# ============ 显示结果 ============
show_result() {
load_config || return
echo ""
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo -e "${CYAN} 🚀 Shadowsocks-Rust 安装完成${NC}"
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo ""
if [[ -n "$PORT_2022" ]]; then
echo -e "${GREEN}【SS2022-AES-128】新协议${NC}"
echo -e " 地址: ${SERVER_IP}"
echo -e " 端口: ${YELLOW}${PORT_2022}${NC}"
echo -e " 加密: ${METHOD_2022}"
echo -e " 密码: ${YELLOW}${KEY_2022}${NC}"
echo ""
fi
if [[ -n "$PORT_RAW" ]]; then
echo -e "${GREEN}【SS-AES-128】传统协议${NC}"
echo -e " 地址: ${SERVER_IP}"
echo -e " 端口: ${YELLOW}${PORT_RAW}${NC}"
echo -e " 加密: ${METHOD_RAW}"
echo -e " 密码: ${YELLOW}${KEY_RAW}${NC}"
echo ""
fi
echo -e "${CYAN}────────────────────────────────────────${NC}"
echo -e "${GREEN}【Surge 格式】${NC}"
[[ -n "$PORT_2022" ]] && echo " SS2022-128 = ss, ${SERVER_IP}, ${PORT_2022}, encrypt-method=${METHOD_2022}, password=${KEY_2022}"
[[ -n "$PORT_RAW" ]] && echo " SS-AES-128 = ss, ${SERVER_IP}, ${PORT_RAW}, encrypt-method=${METHOD_RAW}, password=${KEY_RAW}"
echo ""
echo -e "${CYAN}────────────────────────────────────────${NC}"
echo -e "${GREEN}【SS 链接】${NC}"
[[ -n "${URI_2022:-}" ]] && echo " ${URI_2022}"
[[ -n "${URI_RAW:-}" ]] && echo " ${URI_RAW}"
echo ""
echo -e "${CYAN}────────────────────────────────────────${NC}"
echo -e "${GREEN}【文件】${NC}"
echo " 配置: /etc/shadowsocks-rust/config.json"
echo " 订阅: /etc/shadowsocks-rust/subscribe/"
echo ""
echo -e "${CYAN}【管理】${NC} 再次运行脚本进入管理菜单"
echo -e "${CYAN}════════════════════════════════════════${NC}"
}
# ============ 查看配置 ============
show_config() {
load_config || error "未安装"
echo ""
echo -e "${CYAN} 📋 当前配置 | 状态: $(systemctl is-active ss-rust 2>/dev/null)${NC}"
echo ""
cat /etc/shadowsocks-rust/config.json
echo ""
[[ -f /etc/shadowsocks-rust/subscribe/info.txt ]] && cat /etc/shadowsocks-rust/subscribe/info.txt
}
# ============ 修改端口 ============
change_port() {
load_config || error "未安装"
echo ""
[[ -n "$PORT_2022" ]] && echo -e " 1) SS2022-128 | 端口 ${PORT_2022}"
[[ -n "$PORT_RAW" ]] && echo -e " 2) SS-AES-128 | 端口 ${PORT_RAW}"
echo ""
read -rp "节点编号: " pn
read -rp "新端口: " new_port
python3 -c "
import json
with open('/etc/shadowsocks-rust/config.json') as f:
c = json.load(f)
idx = int('${pn}') - 1
if 0 <= idx < len(c['servers']):
c['servers'][idx]['server_port'] = int('${new_port}')
with open('/etc/shadowsocks-rust/config.json','w') as f:
json.dump(c, f, indent=4)
"
systemctl restart ss-rust
gen_subscribe
info "端口已改为 ${new_port}"
}
# ============ 重置密钥 ============
reset_keys() {
load_config || error "未安装"
python3 -c "
import json,base64,os
with open('/etc/shadowsocks-rust/config.json') as f:
c = json.load(f)
for s in c['servers']:
s['password'] = base64.b64encode(os.urandom(16)).decode()
with open('/etc/shadowsocks-rust/config.json','w') as f:
json.dump(c, f, indent=4)
"
systemctl restart ss-rust
gen_subscribe
info "密钥已重置"
show_result
}
# ============ BBR 优化 ============
setup_bbr() {
echo ""
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo -e "${CYAN} ⚡ BBR Blast Smooth v2${NC}"
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo ""
# 检测是否已启用
local cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null)
local qd=$(sysctl -n net.core.default_qdisc 2>/dev/null)
if [[ "$cc" == "bbr" && "$qd" == "fq" ]] && grep -q "TCP Tuning" /etc/sysctl.conf 2>/dev/null; then
info "BBR + TCP 完整调优已启用,无需重复配置"
return 0
fi
if [[ "$cc" == "bbr" && "$qd" == "fq" ]]; then
info "检测到 BBR 已启用,升级为完整 TCP 调优..."
fi
# 检测系统
if [[ ! -f /etc/os-release ]]; then
warn "无法检测系统,跳过 BBR"; return 1
fi
. /etc/os-release
local os_name="$ID $VERSION_ID"
info "系统: $os_name"
# 检测内存,选择 profile
local mem=$(free -m | awk '/^Mem:/{print $2}')
local profile rmem wmem tcp_rmem tcp_wmem
if [[ "$mem" -lt 512 ]]; then
profile="micro"; rmem=8388608; wmem=8388608
tcp_rmem="4096 32768 8388608"; tcp_wmem="4096 32768 8388608"
elif [[ "$mem" -lt 1024 ]]; then
profile="small"; rmem=16777216; wmem=16777216
tcp_rmem="4096 65536 16777216"; tcp_wmem="4096 65536 16777216"
elif [[ "$mem" -lt 2048 ]]; then
profile="medium"; rmem=33554432; wmem=33554432
tcp_rmem="4096 87380 33554432"; tcp_wmem="4096 65536 33554432"
elif [[ "$mem" -lt 4096 ]]; then
profile="large"; rmem=67108864; wmem=67108864
tcp_rmem="4096 87380 67108864"; tcp_wmem="4096 65536 67108864"
else
profile="xlarge"; rmem=134217728; wmem=134217728
tcp_rmem="4096 87380 134217728"; tcp_wmem="4096 65536 134217728"
fi
info "内存: ${mem}MB | Profile: $profile | Buffer: $((rmem/1024/1024))MB"
# 备份
if [[ -f /etc/sysctl.conf ]]; then
cp /etc/sysctl.conf /etc/sysctl.conf.bak.$(date +%Y%m%d%H%M%S)
fi
# 写入配置
sed -i '/# === BBR Blast/,/# === END BBR/d' /etc/sysctl.conf 2>/dev/null || true
cat >> /etc/sysctl.conf <<SYSCTL
# === BBR Blast Smooth v2 + TCP Tuning (Profile: $profile) ===
# BBR
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
# Buffer
net.core.rmem_max=$rmem
net.core.wmem_max=$wmem
net.core.rmem_default=$((rmem/4))
net.core.wmem_default=$((wmem/4))
net.ipv4.tcp_rmem=$tcp_rmem
net.ipv4.tcp_wmem=$tcp_wmem
net.core.optmem_max=65536
net.core.netdev_max_backlog=16384
net.core.netdev_budget=600
net.core.netdev_budget_usecs=20000
# Connection
net.core.somaxconn=65535
net.ipv4.tcp_max_syn_backlog=65535
net.ipv4.tcp_max_tw_buckets=2000000
net.ipv4.tcp_max_orphans=65535
net.ipv4.ip_local_port_range=1024 65535
# Keepalive
net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=5
# Timeout & Reuse
net.ipv4.tcp_fin_timeout=8
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_syn_retries=3
net.ipv4.tcp_synack_retries=3
net.ipv4.tcp_retries2=8
net.ipv4.tcp_orphan_retries=2
# Performance
net.ipv4.tcp_window_scaling=1
net.ipv4.tcp_timestamps=1
net.ipv4.tcp_sack=1
net.ipv4.tcp_dsack=1
net.ipv4.tcp_fack=1
net.ipv4.tcp_no_metrics_save=1
net.ipv4.tcp_fastopen=3
net.ipv4.tcp_slow_start_after_idle=0
net.ipv4.tcp_mtu_probing=1
net.ipv4.tcp_ecn=0
net.ipv4.tcp_adv_win_scale=2
# Security
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_rfc1337=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
# IPv6 (disable if not needed)
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
# File descriptors
fs.file-max=2097152
fs.nr_open=2097152
# === END BBR ===
SYSCTL
sysctl -p >/dev/null 2>&1
# 验证
cc=$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null)
qd=$(sysctl -n net.core.default_qdisc 2>/dev/null)
if [[ "$cc" == "bbr" && "$qd" == "fq" ]]; then
info "BBR 启用成功 ✓ (congestion=$cc, qdisc=$qd)"
else
warn "BBR 可能需要重启生效"
fi
}
# ============ 卸载 ============
uninstall() {
warn "卸载 shadowsocks-rust..."
systemctl stop ss-rust 2>/dev/null
systemctl disable ss-rust 2>/dev/null
rm -f /etc/systemd/system/ss-rust.service
rm -f /usr/local/bin/ssserver /usr/local/bin/sslocal /usr/local/bin/ssurl
rm -rf /etc/shadowsocks-rust
systemctl daemon-reload
info "卸载完成"
}
# ============ 安装流程 ============
do_install() {
get_pkg_manager
install_deps
sync_time
install_ssrust
select_and_configure
setup_service
gen_subscribe
show_result
echo ""
read -rp "是否开启 BBR 加速? [Y/n]: " bbr_choice
bbr_choice=${bbr_choice:-Y}
[[ "$bbr_choice" =~ ^[Yy]$ ]] && setup_bbr
}
# ============ 管理菜单 ============
show_menu() {
echo ""
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo -e "${CYAN} 🚀 SS-Rust 管理面板${NC}"
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo ""
echo -e " ${GREEN}1.${NC} 重新安装 (选择节点类型)"
echo -e " ${GREEN}2.${NC} 查看配置 + 节点信息"
echo -e " ${GREEN}3.${NC} 修改端口"
echo -e " ${GREEN}4.${NC} 重置密钥"
echo -e " ${GREEN}5.${NC} 启动服务"
echo -e " ${GREEN}6.${NC} 停止服务"
echo -e " ${GREEN}7.${NC} 重启服务"
echo -e " ${GREEN}8.${NC} 查看日志"
echo -e " ${GREEN}10.${NC} ⚡ BBR 加速优化"
echo -e " ${RED}9.${NC} 卸载"
echo -e " ${YELLOW}0.${NC} 退出"
echo ""
read -rp "请选择 [0-10]: " choice
case "$choice" in
1) do_install ;;
2) show_config ;;
3) change_port ;;
4) reset_keys ;;
5) systemctl start ss-rust && info "已启动" ;;
6) systemctl stop ss-rust && info "已停止" ;;
7) systemctl restart ss-rust && info "已重启" ;;
8) journalctl -u ss-rust --no-pager -n 30 ;;
9) uninstall ;;
10) setup_bbr ;;
0) exit 0 ;;
*) warn "无效选择" ;;
esac
}
# ============ 主入口 ============
main() {
check_root
case "${1:-}" in
install) do_install ;;
uninstall|remove) uninstall ;;
show|config|info) show_config ;;
restart) systemctl restart ss-rust && info "已重启" ;;
start) systemctl start ss-rust && info "已启动" ;;
stop) systemctl stop ss-rust && info "已停止" ;;
log|logs) journalctl -u ss-rust --no-pager -n 30 ;;
reset) reset_keys ;;
bbr) setup_bbr ;;
*)
if [[ -f /etc/shadowsocks-rust/config.json ]]; then
show_menu
else
do_install
fi
;;
esac
}
main "$@"

203
scripts/tmdb_emby_sync.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3 -u
"""
Batch update Emby episode overviews from TMDB Chinese metadata.
Run with: python3 -u scripts/tmdb_emby_sync.py
"""
import json
import re
import sys
import time
import urllib.request
import urllib.parse
import urllib.error
from html import unescape
EMBY_URL = "http://145.239.143.92:8096"
API_KEY = "e3e52b1dcb8b47c39d46b5256bf87081"
ADMIN_UID = "0f026d40c1e04bb7a099aab75a501614"
SERIES = [
("小猪佩奇", "13", 12225),
("安全警长啦咘啦哆", "18", 219799),
("动物神探队", "11", 195407),
("啦咘啦哆警长大战羚羚羊", "12", 253041),
("布鲁伊", "1548", 82728),
("汪汪队立大功", "14", 57532),
("小恐龙大冒险", "1547", 82027),
("海底小纵队", "16", 37472),
("海底小纵队:中国之旅", "17", 132983),
("小马宝莉:友谊大魔法", "15", 20085),
]
def api_get(path, params=None):
if params is None:
params = {}
params["api_key"] = API_KEY
url = f"{EMBY_URL}{path}?{urllib.parse.urlencode(params)}"
with urllib.request.urlopen(url, timeout=30) as resp:
return json.loads(resp.read())
def api_post(path, data, params=None):
if params is None:
params = {}
params["api_key"] = API_KEY
url = f"{EMBY_URL}{path}?{urllib.parse.urlencode(params)}"
body = json.dumps(data).encode("utf-8")
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.status
def fetch_all_episodes():
"""Fetch all episodes once, return dict keyed by SeriesId."""
from collections import defaultdict
by_series = defaultdict(list)
start = 0
while True:
data = api_get("/Items", {
"Recursive": "true",
"IncludeItemTypes": "Episode",
"Fields": "Overview,ParentIndexNumber,IndexNumber,SeriesId,SeriesName",
"StartIndex": str(start),
"Limit": "200",
})
for item in data["Items"]:
by_series[str(item.get("SeriesId", ""))].append(item)
start += len(data["Items"])
if start >= data["TotalRecordCount"]:
break
return by_series
_all_eps = None
def fetch_emby_episodes(series_id):
global _all_eps
if _all_eps is None:
_all_eps = fetch_all_episodes()
return _all_eps.get(str(series_id), [])
def fetch_and_parse_tmdb_season(tmdb_id, season_num):
"""Fetch TMDB season page and parse episode overviews."""
url = f"https://www.themoviedb.org/tv/{tmdb_id}/season/{season_num}?language=zh-CN"
req = urllib.request.Request(url, headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Accept-Language": "zh-CN,zh;q=0.9",
})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
html = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
if e.code == 404:
return {}
raise
episodes = {}
# Phrases that indicate no real content on TMDB
PLACEHOLDER_PHRASES = [
"暂无英文版的简介",
"We don't have an overview",
"No overview",
"请添加内容帮助我们完善数据库",
]
cards = re.split(r'<div class="card"', html)
for card in cards[1:]:
ep_match = re.search(r'data-episode-number="(\d+)"', card)
if not ep_match:
continue
ep_num = int(ep_match.group(1))
ov_match = re.search(
r'<div class="overview">\s*<p>(.*?)</p>', card, re.DOTALL
)
if ov_match:
overview = re.sub(r'<[^>]+>', '', ov_match.group(1)).strip()
overview = unescape(overview).strip()
# Skip placeholder/empty overviews
if len(overview) < 5:
continue
if any(ph in overview for ph in PLACEHOLDER_PHRASES):
continue
episodes[ep_num] = overview
return episodes
def needs_update(ep):
ov = ep.get("Overview", "")
return not ov or len(ov.strip()) < 5
def process_series(series_name, series_id, tmdb_id):
print(f"\n{'='*50}", flush=True)
print(f"{series_name} (Emby:{series_id} TMDB:{tmdb_id})", flush=True)
emby_eps = fetch_emby_episodes(series_id)
missing = [e for e in emby_eps if needs_update(e)]
print(f"Total: {len(emby_eps)}, Missing: {len(missing)}", flush=True)
if not missing:
print("Nothing to update.", flush=True)
return 0
seasons = sorted(set(e.get("ParentIndexNumber", 0) for e in missing))
print(f"Seasons: {seasons}", flush=True)
updated = 0
no_tmdb = 0
for sn in seasons:
print(f" S{sn:02d}: fetching TMDB...", end=" ", flush=True)
try:
tmdb_eps = fetch_and_parse_tmdb_season(tmdb_id, sn)
print(f"{len(tmdb_eps)} eps found", flush=True)
except Exception as e:
print(f"ERROR: {e}", flush=True)
continue
time.sleep(1.5)
season_missing = [
e for e in missing if e.get("ParentIndexNumber") == sn
]
for ep in season_missing:
ep_num = ep.get("IndexNumber")
if ep_num is None:
continue
overview = tmdb_eps.get(ep_num)
if not overview:
no_tmdb += 1
continue
try:
item = api_get(f"/Users/{ADMIN_UID}/Items/{ep['Id']}")
item["Overview"] = overview
api_post(f"/Items/{ep['Id']}", item)
updated += 1
print(f" ✓ E{ep_num:02d}: {overview[:50]}", flush=True)
except Exception as e:
print(f" ✗ E{ep_num:02d}: {e}", flush=True)
time.sleep(0.2)
print(f" Done: {updated} updated, {no_tmdb} no TMDB data", flush=True)
return updated
def main():
target = sys.argv[1] if len(sys.argv) > 1 else None
total = 0
for name, sid, tid in SERIES:
if target and target not in name:
continue
total += process_series(name, sid, tid)
print(f"\n{'='*50}", flush=True)
print(f"TOTAL UPDATED: {total}", flush=True)
if __name__ == "__main__":
main()

43
scripts/vp404-checkin.mjs Normal file
View File

@@ -0,0 +1,43 @@
import puppeteer from 'puppeteer-core';
import fs from 'fs';
const browserURL = 'http://127.0.0.1:18800';
const cookie = {
name: '_nk',
value: '3cfeb30b562daec31ba63bf64fdb3838',
domain: '.nodeseek.com',
path: '/',
httpOnly: false,
secure: true
};
let browser, page;
try {
browser = await puppeteer.connect({ browserURL, defaultViewport: null });
page = await browser.newPage();
await page.setCookie(cookie);
await page.goto('https://www.nodeseek.com', { waitUntil: 'networkidle2', timeout: 30000 });
const response = await page.evaluate(async () => {
const res = await fetch('/api/attendance?random=1', { method: 'POST' });
return { status: res.status, data: await res.json() };
});
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] VP404签到: ${JSON.stringify(response)}\n`;
fs.appendFileSync('scripts/checkin.log', logEntry);
await page.close();
console.log('✅ 签到成功:', response.data);
} catch (err) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] VP404签到失败: ${err.message}\n`;
fs.appendFileSync('scripts/checkin.log', logEntry);
console.error('❌ 签到失败:', err.message);
if (page) await page.close().catch(() => {});
process.exit(1);
}

1341
scripts/vps-snapshot.sh Executable file

File diff suppressed because it is too large Load Diff