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",
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZG91eWluIiwianRpIjoiYzQ2ODc3MzMtY2I3OS00OWNmLTg2ODMtODc1MGJjMzRmYzYzIiwiZXhwIjoxNzg0ODkwMjExLCJpc3MiOiIyMDc4MDY5NzQ5MTQ5MTkyMTkyIiwiYXVkIjoiMjA3ODA2OTc0OTE0OTE5MjE5MyJ9.86oluoimnUJEUBwCfKog4hLNYgrzgdwcqAVphRCbgik",
"userName": "3js",
"id": "2004162624794681344",
"savePath": "/app/collect",
"favSavePath": "/app/favorite",
"upSavePath": "/app/uper",
"imgSavePath": "/app/images",
"status": 1
"apiBase": "http://dsv6.honor3.com:10101",
"username": "douyin",
"password": "douyin2025"
}

View File

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

View File

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

View File

@ -10,7 +10,117 @@ import { fileURLToPath } from "url";
import { syncToDouxiaoyun } from "./sync-douxiaoyun.js";
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
@ -32,22 +142,11 @@ function saveConfig(cookieString, secUserId) {
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 4), "utf8");
console.log(`💾 配置已保存到: ${CONFIG_PATH}`);
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) {
return new Promise((resolve) => {
const checkInterval = setInterval(async () => {
@ -61,25 +160,19 @@ async function waitForLogin(context) {
});
}
// 从页面中提取 sec_user_id
async function extractSecUserIdFromPage(page) {
// 方法1: 从页面 URL 提取
const url = page.url();
const urlMatch = url.match(/user\/([^?]+)/);
if (urlMatch && urlMatch[1] !== "self") {
return urlMatch[1];
}
// 方法2: 从页面 HTML 中的 __INITIAL_STATE__ 提取
try {
const html = await page.content();
const match = html.match(/"sec_uid":"([^"]+)"/);
if (match) return match[1];
} catch (e) {
// 忽略
}
} catch (e) {}
// 方法3: 从 window.__INITIAL_STATE__ 提取
try {
const secUid = await page.evaluate(() => {
const state = window.__INITIAL_STATE__;
@ -87,16 +180,17 @@ async function extractSecUserIdFromPage(page) {
return null;
});
if (secUid) return secUid;
} catch (e) {
// 忽略
}
} catch (e) {}
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);
process.exit(1);
});
export { main };
});
}

View File

@ -2,7 +2,7 @@
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
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;
async function getCollection() {

View File

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