save
This commit is contained in:
parent
a15b98cc3e
commit
375f0e50fa
@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
package.json
16
package.json
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { main };
|
||||||
|
|
||||||
|
const isMain = process.argv[1] && process.argv[1].endsWith("get-cookie.js");
|
||||||
|
if (isMain) {
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error("❌ 脚本执行出错:", err.message);
|
console.error("❌ 脚本执行出错:", err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
export { main };
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user