This commit is contained in:
cheney 2026-07-17 20:55:47 +08:00
parent a15b98cc3e
commit 375f0e50fa
6 changed files with 158 additions and 70 deletions

View File

@ -1,11 +1,5 @@
{ {
"apiUrl": "http://dsv6.honor3.com:10101/api/config/update", "apiBase": "http://dsv6.honor3.com:10101",
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZG91eWluIiwianRpIjoiYzQ2ODc3MzMtY2I3OS00OWNmLTg2ODMtODc1MGJjMzRmYzYzIiwiZXhwIjoxNzg0ODkwMjExLCJpc3MiOiIyMDc4MDY5NzQ5MTQ5MTkyMTkyIiwiYXVkIjoiMjA3ODA2OTc0OTE0OTE5MjE5MyJ9.86oluoimnUJEUBwCfKog4hLNYgrzgdwcqAVphRCbgik", "username": "douyin",
"userName": "3js", "password": "douyin2025"
"id": "2004162624794681344",
"savePath": "/app/collect",
"favSavePath": "/app/favorite",
"upSavePath": "/app/uper",
"imgSavePath": "/app/images",
"status": 1
} }

View File

@ -2,18 +2,18 @@
"name": "p20260129-douxiaoyun", "name": "p20260129-douxiaoyun",
"version": "1.0.0", "version": "1.0.0",
"description": "抖小云插件", "description": "抖小云插件",
"main": "index.js", "main": "src/index.js",
"type": "module",
"scripts": { "scripts": {
"loop": "node cli.js loop", "cookies": "node src/cli.js cookies",
"unfavorite": "node cli.js unfavorite", "sync": "node src/cli.js sync",
"sync": "node cli.js sync", "unfavorite": "node src/cli.js unfavorite",
"uncollect": "node cli.js uncollect", "uncollect": "node src/cli.js uncollect",
"get-cookie": "node cli.js get-cookie" "loop": "node src/cli.js loop"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"playwright": "^1.61.1" "playwright": "^1.61.1"
}, }
"type": "module"
} }

View File

@ -1,6 +1,6 @@
// 抖音小云 CLI 工具 // 抖音小云 CLI 工具
// 用法: // 用法:
// node cli.js get-cookie 获取抖音 cookies 并同步到 douxiaoyun // node cli.js cookies 获取抖音 cookies 并同步到 douxiaoyun
// node cli.js sync 将本地 cookies 同步到 douxiaoyun // node cli.js sync 将本地 cookies 同步到 douxiaoyun
// node cli.js unfavorite 取消点赞(单次执行) // node cli.js unfavorite 取消点赞(单次执行)
// node cli.js uncollect 取消收藏(单次执行) // node cli.js uncollect 取消收藏(单次执行)
@ -11,7 +11,7 @@ const validCommands = ["cookies", "sync", "unfavorite", "uncollect", "loop"];
if (!cmd || !validCommands.includes(cmd)) { if (!cmd || !validCommands.includes(cmd)) {
console.log("抖音小云 CLI 工具\n"); console.log("抖音小云 CLI 工具\n");
console.log("用法: node cli.js <命令>\n"); console.log("用法: node src/cli.js <命令>\n");
console.log("可用命令:"); console.log("可用命令:");
console.log(" cookies 获取抖音 cookies 并同步到 douxiaoyun"); console.log(" cookies 获取抖音 cookies 并同步到 douxiaoyun");
console.log(" sync 将本地 cookies 同步到 douxiaoyun"); console.log(" sync 将本地 cookies 同步到 douxiaoyun");
@ -22,7 +22,6 @@ if (!cmd || !validCommands.includes(cmd)) {
} }
async function main() { async function main() {
console.log("cmd=" + cmd)
switch (cmd) { switch (cmd) {
case "cookies": case "cookies":
console.log("🔑 获取抖音 cookies..."); console.log("🔑 获取抖音 cookies...");
@ -34,10 +33,10 @@ async function main() {
console.log("🔄 同步 cookies 到 douxiaoyun..."); console.log("🔄 同步 cookies 到 douxiaoyun...");
const { syncToDouxiaoyun } = await import("./sync-douxiaoyun.js"); const { syncToDouxiaoyun } = await import("./sync-douxiaoyun.js");
const { readFileSync } = await import("fs"); const { readFileSync } = await import("fs");
const { resolve, dirname } = await import("path"); const { resolve: pathResolve, dirname } = await import("path");
const { fileURLToPath } = await import("url"); const { fileURLToPath } = await import("url");
const __dir = dirname(fileURLToPath(import.meta.url)); const __dir = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(readFileSync(resolve(__dir, "src", "config.json"), "utf8")); const config = JSON.parse(readFileSync(pathResolve(__dir, "..", "config", "config.json"), "utf8"));
await syncToDouxiaoyun(config.cookie, config.sec_user_id); await syncToDouxiaoyun(config.cookie, config.sec_user_id);
break; break;

View File

@ -10,7 +10,117 @@ import { fileURLToPath } from "url";
import { syncToDouxiaoyun } from "./sync-douxiaoyun.js"; import { syncToDouxiaoyun } from "./sync-douxiaoyun.js";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = resolve(__dirname, "config.json"); const CONFIG_PATH = resolve(__dirname, "..", "config", "config.json");
async function main() {
console.log("🚀 启动浏览器...");
const browser = await chromium.launch({
channel: "chrome",
headless: false,
args: ["--disable-blink-features=AutomationControlled"],
});
const context = await browser.newContext({
viewport: { width: 1366, height: 768 },
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
});
const page = await context.newPage();
let capturedSecUserId = null;
page.on("request", (request) => {
const url = request.url();
if (url.includes("follow") || url.includes("aweme")) {
try {
const postData = request.postData();
if (postData && postData.includes("sec_user_id")) {
const parsed = JSON.parse(postData);
if (parsed.sec_user_id) {
capturedSecUserId = parsed.sec_user_id;
console.log("✅ 从请求中捕获到 sec_user_id:", capturedSecUserId);
}
}
} catch (e) {}
}
});
page.on("response", async (response) => {
const url = response.url();
if (url.includes("user/self") || url.includes("aweme")) {
try {
const body = await response.text();
const match = body.match(/"sec_uid":"([^"]+)"/);
if (match && !capturedSecUserId) {
capturedSecUserId = match[1];
console.log("✅ 从响应中捕获到 sec_user_id:", capturedSecUserId);
}
} catch (e) {}
}
});
console.log("📱 正在打开抖音登录页...");
await page.goto("https://www.douyin.com/", {
waitUntil: "domcontentloaded",
timeout: 30000,
});
console.log("🔑 请在浏览器中扫码登录抖音...");
console.log(" (等待登录完成,检测到登录后会自动继续)");
await waitForLogin(context);
console.log("✅ 登录成功!");
console.log("📋 正在访问个人主页获取 sec_user_id...");
await page.goto("https://www.douyin.com/user/self?from_tab_name=main", {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForTimeout(3000);
if (!capturedSecUserId) {
capturedSecUserId = await extractSecUserIdFromPage(page);
}
if (!capturedSecUserId) {
console.log("🖱️ 尝试触发关注列表请求以获取 sec_user_id...");
try {
const followBtn = page.locator('[data-e2e="follow-count"]').first();
if (await followBtn.isVisible({ timeout: 5000 })) {
await followBtn.click();
await page.waitForTimeout(3000);
}
} catch (e) {
console.log(" 未能找到关注按钮,尝试其他方式...");
}
if (!capturedSecUserId) {
capturedSecUserId = await extractSecUserIdFromPage(page);
}
}
const cookies = await context.cookies();
const cookieString = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
console.log("\n========================================");
console.log("📋 获取结果:");
console.log("========================================");
console.log("Cookie:", cookieString.substring(0, 200) + "...");
console.log("sec_user_id:", capturedSecUserId || "❌ 未获取到");
console.log("========================================\n");
await browser.close();
console.log("👋 浏览器已关闭");
saveConfig(cookieString, capturedSecUserId);
if (capturedSecUserId) {
await syncToDouxiaoyun(cookieString, capturedSecUserId);
} else {
console.log("\n⚠ 未获取到 sec_user_id跳过服务器上传");
}
}
// ============================================================ // ============================================================
// 保存配置到本地 config.json // 保存配置到本地 config.json
@ -32,22 +142,11 @@ function saveConfig(cookieString, secUserId) {
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 4), "utf8"); writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 4), "utf8");
console.log(`💾 配置已保存到: ${CONFIG_PATH}`); console.log(`💾 配置已保存到: ${CONFIG_PATH}`);
console.log(` updatedAt: ${config.updatedAt}`); console.log(` updatedAt: ${config.updatedAt}`);
if (!secUserId) {
console.log("\n⚠ 未能自动获取 sec_user_id");
console.log(" 请参考 README 手动获取:");
console.log(" 1. 按 F12 打开开发者工具 -> Network");
console.log(" 2. 筛选 'follow'");
console.log(" 3. 点击关注按钮,在请求 Payload 中找到 sec_user_id");
console.log(" 4. 手动填入 config.json");
}
} }
// ============================================================ // ============================================================
// 辅助函数 // 辅助函数
// ============================================================ // ============================================================
// 等待用户登录成功
async function waitForLogin(context) { async function waitForLogin(context) {
return new Promise((resolve) => { return new Promise((resolve) => {
const checkInterval = setInterval(async () => { const checkInterval = setInterval(async () => {
@ -61,25 +160,19 @@ async function waitForLogin(context) {
}); });
} }
// 从页面中提取 sec_user_id
async function extractSecUserIdFromPage(page) { async function extractSecUserIdFromPage(page) {
// 方法1: 从页面 URL 提取
const url = page.url(); const url = page.url();
const urlMatch = url.match(/user\/([^?]+)/); const urlMatch = url.match(/user\/([^?]+)/);
if (urlMatch && urlMatch[1] !== "self") { if (urlMatch && urlMatch[1] !== "self") {
return urlMatch[1]; return urlMatch[1];
} }
// 方法2: 从页面 HTML 中的 __INITIAL_STATE__ 提取
try { try {
const html = await page.content(); const html = await page.content();
const match = html.match(/"sec_uid":"([^"]+)"/); const match = html.match(/"sec_uid":"([^"]+)"/);
if (match) return match[1]; if (match) return match[1];
} catch (e) { } catch (e) {}
// 忽略
}
// 方法3: 从 window.__INITIAL_STATE__ 提取
try { try {
const secUid = await page.evaluate(() => { const secUid = await page.evaluate(() => {
const state = window.__INITIAL_STATE__; const state = window.__INITIAL_STATE__;
@ -87,16 +180,17 @@ async function extractSecUserIdFromPage(page) {
return null; return null;
}); });
if (secUid) return secUid; if (secUid) return secUid;
} catch (e) { } catch (e) {}
// 忽略
}
return null; return null;
} }
main().catch((err) => { export { main };
const isMain = process.argv[1] && process.argv[1].endsWith("get-cookie.js");
if (isMain) {
main().catch((err) => {
console.error("❌ 脚本执行出错:", err.message); console.error("❌ 脚本执行出错:", err.message);
process.exit(1); process.exit(1);
}); });
}
export { main };

View File

@ -2,7 +2,7 @@
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname, resolve } from "path"; import { dirname, resolve } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(readFileSync(resolve(__dirname, "config.json"), "utf8")); const config = JSON.parse(readFileSync(resolve(__dirname, "..", "config", "config.json"), "utf8"));
const { cookie, sec_user_id } = config; const { cookie, sec_user_id } = config;
async function getCollection() { async function getCollection() {

View File

@ -4,17 +4,27 @@ import { fileURLToPath } from "url";
import { dirname, resolve } from "path"; import { dirname, resolve } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const DOUXIAOYUN_BASE = "http://dsv6.honor3.com:10101"; const SERVER_CONFIG_PATH = resolve(__dirname, "..", "config", "server-config.json");
const DOUXIAOYUN_USER = "douyin";
const DOUXIAOYUN_PASS = "douyin2025"; // 读取服务器配置
function getServerConfig() {
const raw = JSON.parse(readFileSync(SERVER_CONFIG_PATH, "utf8"));
return {
apiBase: raw.apiBase || "http://dsv6.honor3.com:10101",
username: raw.username || "douyin",
password: raw.password || "douyin2025",
};
}
async function syncToDouxiaoyun(cookieString, secUserId) { async function syncToDouxiaoyun(cookieString, secUserId) {
console.log("\n🔄 正在同步 cookies 到 douxiaoyun 服务..."); console.log("\n🔄 正在同步 cookies 到 douxiaoyun 服务...");
const { apiBase, username, password } = getServerConfig();
try { try {
// 1. 登录 douxiaoyun获取 JWT token // 1. 登录 douxiaoyun获取 JWT token
console.log(" 1/3 登录 douxiaoyun..."); console.log(` 1/3 登录 douxiaoyun (${username})...`);
const loginResp = await fetch(`${DOUXIAOYUN_BASE}/api/auth/login`, { const loginResp = await fetch(`${apiBase}/api/auth/login`, {
method: "POST", method: "POST",
headers: { headers: {
"accept": "application/json, text/plain, */*", "accept": "application/json, text/plain, */*",
@ -22,10 +32,9 @@ async function syncToDouxiaoyun(cookieString, secUserId) {
"cache-control": "no-cache", "cache-control": "no-cache",
"content-type": "application/json", "content-type": "application/json",
"pragma": "no-cache", "pragma": "no-cache",
"Referer": `${DOUXIAOYUN_BASE}/`, "Referer": `${apiBase}/`,
"Referrer-Policy": "no-referrer-when-downgrade",
}, },
body: JSON.stringify({ username: DOUXIAOYUN_USER, password: DOUXIAOYUN_PASS }), body: JSON.stringify({ username, password }),
}); });
if (!loginResp.ok) throw new Error(`登录失败: HTTP ${loginResp.status}`); if (!loginResp.ok) throw new Error(`登录失败: HTTP ${loginResp.status}`);
@ -39,17 +48,13 @@ async function syncToDouxiaoyun(cookieString, secUserId) {
// 2. 查询现有配置 // 2. 查询现有配置
console.log(" 2/3 查询现有配置..."); console.log(" 2/3 查询现有配置...");
const configResp = await fetch(`${DOUXIAOYUN_BASE}/api/config/paged`, { const configResp = await fetch(`${apiBase}/api/config/paged`, {
method: "POST", method: "POST",
headers: { headers: {
"accept": "application/json, text/plain, */*", "accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"authorization": `Bearer ${token}`, "authorization": `Bearer ${token}`,
"cache-control": "no-cache",
"content-type": "application/json", "content-type": "application/json",
"pragma": "no-cache", "Referer": `${apiBase}/`,
"Referer": `${DOUXIAOYUN_BASE}/`,
"Referrer-Policy": "no-referrer-when-downgrade",
}, },
body: JSON.stringify({ pageIndex: 1, pageSize: 10 }), body: JSON.stringify({ pageIndex: 1, pageSize: 10 }),
}); });
@ -73,17 +78,13 @@ async function syncToDouxiaoyun(cookieString, secUserId) {
secUserId: secUserId || existingConfig.secUserId || "", secUserId: secUserId || existingConfig.secUserId || "",
}; };
const updateResp = await fetch(`${DOUXIAOYUN_BASE}/api/config/update`, { const updateResp = await fetch(`${apiBase}/api/config/update`, {
method: "POST", method: "POST",
headers: { headers: {
"accept": "application/json, text/plain, */*", "accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"authorization": `Bearer ${token}`, "authorization": `Bearer ${token}`,
"cache-control": "no-cache",
"content-type": "application/json", "content-type": "application/json",
"pragma": "no-cache", "Referer": `${apiBase}/`,
"Referer": `${DOUXIAOYUN_BASE}/`,
"Referrer-Policy": "no-referrer-when-downgrade",
}, },
body: JSON.stringify(updateBody), body: JSON.stringify(updateBody),
}); });