Rename to hkt.sh
This commit is contained in:
90
scripts/bluey_update.py
Normal file
90
scripts/bluey_update.py
Normal 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
25
scripts/bookapi-proxy.js
Normal 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
15
scripts/boot-start.sh
Normal 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 &
|
||||
183
scripts/cf-bot/cf_multi_bot.py
Normal file
183
scripts/cf-bot/cf_multi_bot.py
Normal 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
101
scripts/checkin-cdp.mjs
Normal 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);
|
||||
}
|
||||
})();
|
||||
7
scripts/collect-data-shortcut.sh
Normal file
7
scripts/collect-data-shortcut.sh
Normal 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
|
||||
7
scripts/collect-phone-data-loop.sh
Normal file
7
scripts/collect-phone-data-loop.sh
Normal 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
|
||||
19
scripts/collect-phone-data.sh
Normal file
19
scripts/collect-phone-data.sh
Normal 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)"
|
||||
21
scripts/com.openclaw.mac-guard.plist
Normal file
21
scripts/com.openclaw.mac-guard.plist
Normal 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
114
scripts/dd-reinstall.sh
Normal 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
|
||||
70
scripts/emby-scrape-check.mjs
Normal file
70
scripts/emby-scrape-check.mjs
Normal 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
15
scripts/fix-telegram-menu.sh
Executable 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": "帮助"}
|
||||
]
|
||||
}'
|
||||
1
scripts/gitea-backup/dd-reinstall
Submodule
1
scripts/gitea-backup/dd-reinstall
Submodule
Submodule scripts/gitea-backup/dd-reinstall added at 726430ccc6
1
scripts/gitea-backup/oc-monitor
Submodule
1
scripts/gitea-backup/oc-monitor
Submodule
Submodule scripts/gitea-backup/oc-monitor added at 2e6b86381b
1
scripts/gitea-backup/ss-rust
Submodule
1
scripts/gitea-backup/ss-rust
Submodule
Submodule scripts/gitea-backup/ss-rust added at 0a158bbcae
1
scripts/gitea-backup/sub-bot
Submodule
1
scripts/gitea-backup/sub-bot
Submodule
Submodule scripts/gitea-backup/sub-bot added at ea233c31b6
1
scripts/gitea-backup/tcp-bbr
Submodule
1
scripts/gitea-backup/tcp-bbr
Submodule
Submodule scripts/gitea-backup/tcp-bbr added at b7fb2756a5
1
scripts/gitea-backup/tg-user-monitor
Submodule
1
scripts/gitea-backup/tg-user-monitor
Submodule
Submodule scripts/gitea-backup/tg-user-monitor added at 8b185c799a
1
scripts/gitea-backup/vps-management-bot
Submodule
1
scripts/gitea-backup/vps-management-bot
Submodule
Submodule scripts/gitea-backup/vps-management-bot added at 76a263d0f9
1
scripts/gitea-backup/vps-snapshot
Submodule
1
scripts/gitea-backup/vps-snapshot
Submodule
Submodule scripts/gitea-backup/vps-snapshot added at ef303c7857
112
scripts/mac-guard.js
Normal file
112
scripts/mac-guard.js
Normal 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
73
scripts/mteam-batch-dl.sh
Normal 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
36
scripts/mteam-guide.md
Normal 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(被屏蔽)
|
||||
104
scripts/nodeseek-checkin-browser.py
Normal file
104
scripts/nodeseek-checkin-browser.py
Normal 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()
|
||||
48
scripts/nodeseek-checkin-cron.mjs
Normal file
48
scripts/nodeseek-checkin-cron.mjs
Normal 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);
|
||||
});
|
||||
15
scripts/nodeseek-checkin-state.json
Normal file
15
scripts/nodeseek-checkin-state.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
200
scripts/nodeseek-checkin-vp404.mjs
Normal file
200
scripts/nodeseek-checkin-vp404.mjs
Normal 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();
|
||||
28
scripts/nodeseek-checkin-vp404.sh
Executable file
28
scripts/nodeseek-checkin-vp404.sh
Executable 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"
|
||||
123
scripts/nodeseek-checkin.cjs
Normal file
123
scripts/nodeseek-checkin.cjs
Normal 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);
|
||||
});
|
||||
152
scripts/nodeseek-checkin.cron.mjs
Normal file
152
scripts/nodeseek-checkin.cron.mjs
Normal 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
159
scripts/nodeseek-checkin.js
Normal 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();
|
||||
206
scripts/nodeseek-checkin.mjs
Normal file
206
scripts/nodeseek-checkin.mjs
Normal 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
95
scripts/nodeseek-checkin.py
Executable 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
79
scripts/nodeseek-checkin.sh
Executable 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"
|
||||
206
scripts/nodeseek-vp404-checkin.mjs
Normal file
206
scripts/nodeseek-vp404-checkin.mjs
Normal 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
120
scripts/oc-status.sh
Executable 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
186
scripts/peekabo-monitor.js
Normal 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);
|
||||
7
scripts/pf-bruteforce.conf
Normal file
7
scripts/pf-bruteforce.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
# Mac Guard 防火墙规则
|
||||
# 封禁爆破 IP
|
||||
|
||||
table <bruteforce> persist
|
||||
|
||||
# 封禁 bruteforce 表中的 IP
|
||||
block drop quick from <bruteforce>
|
||||
13
scripts/pixel6-autostart.sh
Normal file
13
scripts/pixel6-autostart.sh
Normal 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
|
||||
58
scripts/pixel6-chromium-cdp.sh
Normal file
58
scripts/pixel6-chromium-cdp.sh
Normal 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 工具控制它"
|
||||
40
scripts/pixel6-install-firefox.sh
Normal file
40
scripts/pixel6-install-firefox.sh
Normal 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 屏幕上"
|
||||
55
scripts/pixel6-reinstall.sh
Normal file
55
scripts/pixel6-reinstall.sh
Normal 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 "=== 安装完成 ==="
|
||||
31
scripts/pixel6-screen-reader.sh
Normal file
31
scripts/pixel6-screen-reader.sh
Normal 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
|
||||
25
scripts/pixel6-watchdog.sh
Normal file
25
scripts/pixel6-watchdog.sh
Normal 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
614
scripts/ss-rust.sh
Normal 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
203
scripts/tmdb_emby_sync.py
Normal 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
43
scripts/vp404-checkin.mjs
Normal 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
1341
scripts/vps-snapshot.sh
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user