752 lines
29 KiB
TypeScript
752 lines
29 KiB
TypeScript
import { ipcMain, BrowserWindow } from 'electron';
|
|
import { Log } from '../log/main';
|
|
import DbMain from '../db/main';
|
|
import { PlatformAccount, LoginSession } from './types';
|
|
import { chromium, Browser, BrowserContext, Page } from 'playwright';
|
|
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { getPlaywrightChromiumPath, COMMON_BROWSER_ARGS } from '../../lib/browser-path';
|
|
import { getRuntimeDataPath } from '../../lib/resource-path';
|
|
|
|
// 登录会话存储
|
|
const loginSessions: Map<string, LoginSession> = new Map();
|
|
|
|
// 浏览器实例管理
|
|
let browser: Browser | null = null;
|
|
const browserContexts: Map<string, BrowserContext> = new Map();
|
|
|
|
/**
|
|
* 初始化浏览器
|
|
*/
|
|
async function initBrowser(): Promise<Browser> {
|
|
if (!browser) {
|
|
const executablePath = getPlaywrightChromiumPath();
|
|
browser = await chromium.launch({
|
|
executablePath, // ✅ 使用打包的浏览器,支持自定义安装路径
|
|
headless: false, // 显示浏览器窗口,方便扫码
|
|
args: COMMON_BROWSER_ARGS
|
|
});
|
|
}
|
|
return browser;
|
|
}
|
|
|
|
/**
|
|
* 获取或创建浏览器上下文
|
|
*/
|
|
async function getBrowserContext(platform: string): Promise<BrowserContext> {
|
|
if (!browserContexts.has(platform)) {
|
|
const browser = await initBrowser();
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1280, height: 720 },
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
locale: 'zh-CN',
|
|
timezoneId: 'Asia/Shanghai'
|
|
});
|
|
browserContexts.set(platform, context);
|
|
}
|
|
return browserContexts.get(platform)!;
|
|
}
|
|
|
|
/**
|
|
* 生成会话ID
|
|
*/
|
|
function generateSessionId(): string {
|
|
return `session_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
}
|
|
|
|
/**
|
|
* 启动扫码登录
|
|
*/
|
|
ipcMain.handle('platform:startQRLogin', async (event, params: { platform: string }) => {
|
|
try {
|
|
const { platform } = params;
|
|
Log.info('platform.startQRLogin', { platform });
|
|
|
|
const sessionId = generateSessionId();
|
|
let qrCodeUrl = '';
|
|
const context = await getBrowserContext(platform);
|
|
const page = await context.newPage();
|
|
|
|
// 根据平台访问不同的登录页面
|
|
if (platform === 'douyin') {
|
|
await page.goto('https://creator.douyin.com/');
|
|
|
|
// 等待二维码出现
|
|
try {
|
|
await page.waitForSelector('.qrcode-img', { timeout: 10000 });
|
|
const qrImgElement = await page.$('.qrcode-img');
|
|
qrCodeUrl = await qrImgElement?.getAttribute('src') || '';
|
|
|
|
if (!qrCodeUrl) {
|
|
// 尝试其他选择器
|
|
const qrCanvas = await page.$('canvas[class*="qrcode"]');
|
|
if (qrCanvas) {
|
|
qrCodeUrl = await qrCanvas.screenshot({ type: 'png' }).then(buffer =>
|
|
`data:image/png;base64,${buffer.toString('base64')}`
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
Log.error('Failed to get QR code', error);
|
|
}
|
|
|
|
} else if (platform === 'kuaishou') {
|
|
await page.goto('https://cp.kuaishou.com/');
|
|
|
|
try {
|
|
await page.waitForSelector('.qrcode-box img', { timeout: 10000 });
|
|
const qrImgElement = await page.$('.qrcode-box img');
|
|
qrCodeUrl = await qrImgElement?.getAttribute('src') || '';
|
|
} catch (error) {
|
|
Log.error('Failed to get QR code', error);
|
|
}
|
|
|
|
} else if (platform === 'shipin') {
|
|
await page.goto('https://channels.weixin.qq.com/');
|
|
|
|
try {
|
|
await page.waitForSelector('.qrcode img', { timeout: 10000 });
|
|
const qrImgElement = await page.$('.qrcode img');
|
|
qrCodeUrl = await qrImgElement?.getAttribute('src') || '';
|
|
} catch (error) {
|
|
Log.error('Failed to get QR code', error);
|
|
}
|
|
}
|
|
|
|
// 存储登录会话
|
|
const session: LoginSession = {
|
|
sessionId,
|
|
platform,
|
|
qrCodeUrl,
|
|
status: 'pending',
|
|
createdAt: Date.now()
|
|
};
|
|
loginSessions.set(sessionId, session);
|
|
|
|
// 将page存储到context的metadata中,以便后续使用
|
|
(context as any)._loginPage = page;
|
|
|
|
return {
|
|
success: true,
|
|
sessionId,
|
|
qrCodeUrl
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error('platform.startQRLogin error', error);
|
|
return {
|
|
success: false,
|
|
message: error.message
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 检查登录状态
|
|
*/
|
|
ipcMain.handle('platform:checkLoginStatus', async (event, params: { sessionId: string }) => {
|
|
try {
|
|
const { sessionId } = params;
|
|
const session = loginSessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
return { status: 'expired' };
|
|
}
|
|
|
|
// 检查会话是否过期 (5分钟)
|
|
if (Date.now() - session.createdAt > 5 * 60 * 1000) {
|
|
session.status = 'expired';
|
|
return { status: 'expired' };
|
|
}
|
|
|
|
const context = browserContexts.get(session.platform);
|
|
if (!context) {
|
|
return { status: 'expired' };
|
|
}
|
|
|
|
const page = (context as any)._loginPage as Page;
|
|
if (!page) {
|
|
return { status: 'expired' };
|
|
}
|
|
|
|
// 检查是否登录成功(通过检查页面URL或特定元素)
|
|
const currentUrl = page.url();
|
|
let isLoggedIn = false;
|
|
|
|
if (session.platform === 'douyin') {
|
|
isLoggedIn = currentUrl.includes('/creator/') && !currentUrl.includes('/login');
|
|
} else if (session.platform === 'kuaishou') {
|
|
isLoggedIn = currentUrl.includes('/cp.kuaishou.com/article');
|
|
} else if (session.platform === 'shipin') {
|
|
isLoggedIn = currentUrl.includes('/channels.weixin.qq.com/platform');
|
|
}
|
|
|
|
if (isLoggedIn) {
|
|
// 登录成功,获取Cookie
|
|
const cookies = await context.cookies();
|
|
|
|
// 提取用户信息
|
|
let userInfo = await extractUserInfo(page, session.platform);
|
|
|
|
// 保存到数据库
|
|
const now = Date.now();
|
|
|
|
await DbMain.insert(
|
|
`INSERT INTO platform_accounts (platform, nickname, uid, avatar, cookies, tokens, loginStatus, expiresAt, createdAt, updatedAt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
session.platform,
|
|
userInfo.nickname || '',
|
|
userInfo.uid || '',
|
|
userInfo.avatar || '',
|
|
JSON.stringify(cookies),
|
|
'', // tokens
|
|
'active',
|
|
now + 30 * 24 * 60 * 60 * 1000, // 30天后过期
|
|
now,
|
|
now
|
|
]
|
|
);
|
|
|
|
session.status = 'success';
|
|
|
|
// 🔧 修改:检测到已登录后,必须关闭浏览器(不是关页签,是关闭整个浏览器)
|
|
try {
|
|
// 关闭登录页面
|
|
await page.close();
|
|
(context as any)._loginPage = null;
|
|
|
|
// 关闭浏览器上下文(这会关闭整个浏览器窗口)
|
|
Log.info(`✅ 检测到${session.platform}已登录,准备关闭浏览器`);
|
|
await context.close();
|
|
browserContexts.delete(session.platform);
|
|
Log.info(`✅ 浏览器已关闭`);
|
|
} catch (closeError: any) {
|
|
Log.error('关闭浏览器失败:', closeError.message);
|
|
}
|
|
|
|
return {
|
|
status: 'success',
|
|
userInfo
|
|
};
|
|
}
|
|
|
|
return { status: 'pending' };
|
|
|
|
} catch (error: any) {
|
|
Log.error('platform.checkLoginStatus error', error);
|
|
return {
|
|
status: 'failed',
|
|
message: error.message
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 提取用户信息
|
|
*/
|
|
async function extractUserInfo(page: Page, platform: string): Promise<any> {
|
|
try {
|
|
if (platform === 'douyin') {
|
|
// 尝试提取抖音用户信息
|
|
const nickname = await page.locator('.username, .account-name').first().textContent().catch(() => '');
|
|
const avatar = await page.locator('.avatar img').first().getAttribute('src').catch(() => '');
|
|
|
|
return { nickname: nickname?.trim() || '', avatar: avatar || '', uid: '' };
|
|
} else if (platform === 'kuaishou') {
|
|
const nickname = await page.locator('.user-name').first().textContent().catch(() => '');
|
|
const avatar = await page.locator('.user-avatar img').first().getAttribute('src').catch(() => '');
|
|
|
|
return { nickname: nickname?.trim() || '', avatar: avatar || '', uid: '' };
|
|
} else if (platform === 'shipin') {
|
|
const nickname = await page.locator('.nickname').first().textContent().catch(() => '');
|
|
const avatar = await page.locator('.avatar img').first().getAttribute('src').catch(() => '');
|
|
|
|
return { nickname: nickname?.trim() || '', avatar: avatar || '', uid: '' };
|
|
}
|
|
} catch (error) {
|
|
Log.error('extractUserInfo error', error);
|
|
}
|
|
|
|
return { nickname: '', avatar: '', uid: '' };
|
|
}
|
|
|
|
/**
|
|
* 获取账号列表
|
|
*/
|
|
ipcMain.handle('platform:getAccounts', async (event, params: { platform?: string } = {}) => {
|
|
try {
|
|
let query = 'SELECT * FROM platform_accounts WHERE loginStatus = ?';
|
|
let queryParams: any[] = ['active'];
|
|
|
|
if (params.platform) {
|
|
query += ' AND platform = ?';
|
|
queryParams.push(params.platform);
|
|
}
|
|
|
|
query += ' ORDER BY updatedAt DESC';
|
|
|
|
const accounts = await DbMain.select(query, queryParams);
|
|
|
|
// 解密cookies(这里简化处理,实际应该加密存储)
|
|
const result = accounts.map(acc => ({
|
|
...acc,
|
|
cookies: null, // 不返回敏感信息到前端
|
|
tokens: null
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
accounts: result
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error('platform.getAccounts error', error);
|
|
return {
|
|
success: false,
|
|
message: error.message,
|
|
accounts: []
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 登出账号
|
|
*/
|
|
ipcMain.handle('platform:logout', async (event, params: { accountId: number }) => {
|
|
try {
|
|
const { accountId } = params;
|
|
|
|
await DbMain.execute(
|
|
'UPDATE platform_accounts SET loginStatus = ?, updatedAt = ? WHERE id = ?',
|
|
['expired', Date.now(), accountId]
|
|
);
|
|
|
|
return { success: true };
|
|
|
|
} catch (error: any) {
|
|
Log.error('platform.logout error', error);
|
|
return {
|
|
success: false,
|
|
message: error.message
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 清除平台的浏览器数据和Cookie(完全清空登录状态)
|
|
*/
|
|
ipcMain.handle('platform:clearBrowserData', async (event, params: { platform: string }) => {
|
|
try {
|
|
const { platform } = params;
|
|
|
|
Log.info(`[clearBrowserData] 开始清除${platform}的浏览器数据`);
|
|
|
|
// 1. 关闭 Publish 模块管理的浏览器上下文 (关键步骤:释放文件锁)
|
|
try {
|
|
// 动态导入 publish 模块,避免循环依赖
|
|
const { closeBrowserContext } = await import('../publish/main');
|
|
await closeBrowserContext(platform);
|
|
Log.info(`[clearBrowserData] 已调用 publish.closeBrowserContext 关闭${platform}浏览器`);
|
|
} catch (e: any) {
|
|
Log.warn(`[clearBrowserData] 关闭Publish模块浏览器失败 (可能未打开):`, e.message);
|
|
}
|
|
|
|
// 2. 关闭 Platform 模块自己管理的浏览器上下文 (如果有)
|
|
// 注意:Platform 模块的 key 是 platform,不是 publish_${platform}
|
|
if (browserContexts.has(platform)) {
|
|
try {
|
|
const context = browserContexts.get(platform)!;
|
|
await context.close();
|
|
browserContexts.delete(platform);
|
|
Log.info(`[clearBrowserData] 已关闭并删除Platform模块的${platform}浏览器上下文`);
|
|
} catch (error: any) {
|
|
Log.warn(`[clearBrowserData] 关闭Platform模块浏览器上下文失败:`, error.message);
|
|
}
|
|
}
|
|
|
|
// 0. 🔴 终极手段:强制杀死所有 Chromium 进程,确保没有文件被锁
|
|
try {
|
|
const { execSync } = require('child_process');
|
|
Log.info('[clearBrowserData] 正在强制关闭所有浏览器进程...');
|
|
// Windows 下强制杀死 chrome.exe 和 chromium.exe
|
|
if (process.platform === 'win32') {
|
|
execSync('taskkill /F /IM chrome.exe /IM chromium.exe /T 2>nul', { stdio: 'ignore' });
|
|
} else {
|
|
execSync('pkill -9 -f chromium || true', { stdio: 'ignore' });
|
|
execSync('pkill -9 -f chrome || true', { stdio: 'ignore' });
|
|
}
|
|
Log.info('[clearBrowserData] 浏览器进程已强制清理');
|
|
} catch (e) {
|
|
Log.warn('[clearBrowserData] 强制关闭进程时出错(可忽略):', e);
|
|
}
|
|
|
|
// 等待进程完全退出释放文件锁
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// 🔥 3. 删除持久化浏览器数据目录(增强版:彻底清除)
|
|
const userDataDir = getRuntimeDataPath('browser-data', 'platform', platform);
|
|
if (fs.existsSync(userDataDir)) {
|
|
try {
|
|
// 第一步:强制关闭占用这个目录的所有进程
|
|
const { execSync } = require('child_process');
|
|
try {
|
|
Log.info(`[clearBrowserData] 尝试解除${userDataDir}上的文件锁...`);
|
|
if (process.platform === 'win32') {
|
|
// Windows 下使用 handle.exe 或直接杀 chrome 进程
|
|
execSync(`taskkill /F /IM chrome.exe /IM chromium.exe 2>nul`, { stdio: 'ignore' });
|
|
}
|
|
} catch (e) {
|
|
Log.debug(`[clearBrowserData] 解除文件锁失败(可忽略):`, e);
|
|
}
|
|
|
|
// 第二步:等待文件锁释放
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// 第三步:增加重试机制,防止文件锁释放延迟
|
|
let retries = 10; // 增加重试次数到10
|
|
while (retries > 0) {
|
|
try {
|
|
// 尝试先清空目录内容
|
|
if (fs.existsSync(userDataDir)) {
|
|
const files = fs.readdirSync(userDataDir);
|
|
for (const file of files) {
|
|
const curPath = path.join(userDataDir, file);
|
|
try {
|
|
if (fs.lstatSync(curPath).isDirectory()) {
|
|
fs.rmSync(curPath, { recursive: true, force: true });
|
|
} else {
|
|
fs.unlinkSync(curPath);
|
|
}
|
|
} catch (e) { } // 忽略单个文件删除失败
|
|
}
|
|
|
|
// 然后删除目录本身
|
|
fs.rmSync(userDataDir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 第四步:验证目录是否真的被删除了
|
|
if (fs.existsSync(userDataDir)) {
|
|
throw new Error('目录仍然存在');
|
|
}
|
|
|
|
Log.info(`[clearBrowserData] ✅ 已彻底删除浏览器数据目录: ${userDataDir}`);
|
|
break;
|
|
} catch (rmError: any) {
|
|
retries--;
|
|
if (retries === 0) {
|
|
throw new Error(`无法删除目录 (尝试${10}次后放弃): ${rmError.message}`);
|
|
}
|
|
Log.warn(`[clearBrowserData] 删除目录失败,等待后重试 (${retries} left)...`);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
Log.error(`[clearBrowserData] 删除浏览器数据目录失败:`, error.message);
|
|
// 最后手段:重命名目录
|
|
try {
|
|
const trashPath = getRuntimeDataPath('trash', `${platform}_${Date.now()}`);
|
|
fs.mkdirSync(path.dirname(trashPath), { recursive: true });
|
|
fs.renameSync(userDataDir, trashPath);
|
|
Log.warn(`[clearBrowserData] ⚠️ 无法删除,已将目录重命名为: ${trashPath}`);
|
|
} catch (mvError) {
|
|
Log.error(`[clearBrowserData] 重命名也失败:`, mvError);
|
|
throw mvError;
|
|
}
|
|
}
|
|
} else {
|
|
Log.info(`[clearBrowserData] 数据目录不存在,无需删除: ${userDataDir}`);
|
|
}
|
|
|
|
// 🔥 3.5 关键:清除全局 Chromium User Data 目录中的 cookies
|
|
// 这是真正存储所有 cookies 的地方!(所有 Chromium 实例共享)
|
|
try {
|
|
let globalChromiumDataDir: string;
|
|
if (process.platform === 'win32') {
|
|
globalChromiumDataDir = path.join(os.homedir(), 'AppData', 'Local', 'Chromium', 'User Data', 'Default');
|
|
} else if (process.platform === 'darwin') {
|
|
globalChromiumDataDir = path.join(os.homedir(), 'Library', 'Application Support', 'Chromium', 'Default');
|
|
} else {
|
|
globalChromiumDataDir = path.join(os.homedir(), '.config', 'chromium', 'Default');
|
|
}
|
|
|
|
if (fs.existsSync(globalChromiumDataDir)) {
|
|
const filesToClean = [
|
|
'Network/Cookies',
|
|
'Network/Cookies-journal',
|
|
'Network/Network Persistent State',
|
|
'Cookies',
|
|
'Cookies-journal',
|
|
];
|
|
|
|
for (const file of filesToClean) {
|
|
try {
|
|
const filePath = path.join(globalChromiumDataDir, file);
|
|
if (fs.existsSync(filePath)) {
|
|
if (fs.statSync(filePath).isDirectory()) {
|
|
fs.rmSync(filePath, { recursive: true, force: true });
|
|
} else {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
Log.info(`[clearBrowserData] 已清除全局Chromium cookies: ${file}`);
|
|
}
|
|
} catch (e: any) {
|
|
Log.warn(`[clearBrowserData] 清除全局Chromium文件失败 ${file} (可忽略):`, e.message);
|
|
}
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
Log.warn(`[clearBrowserData] 清除全局Chromium User Data失败(可忽略):`, error.message);
|
|
}
|
|
|
|
// 3.6 🔧 新增:清除 ipAgent 模块使用的 douyin-browser-data 目录
|
|
if (platform === 'douyin') {
|
|
// 先关闭 DouyinBrowserManager 管理的浏览器实例
|
|
try {
|
|
const { DouyinBrowserManager } = await import('../ipAgent/browserManager');
|
|
await DouyinBrowserManager.cleanup();
|
|
Log.info(`[clearBrowserData] 已关闭 DouyinBrowserManager 的浏览器实例`);
|
|
} catch (e: any) {
|
|
Log.warn(`[clearBrowserData] 关闭 DouyinBrowserManager 失败 (可能未初始化):`, e.message);
|
|
}
|
|
|
|
const { app } = await import('electron');
|
|
const douyinBrowserDataDir = path.join(app.getPath('userData'), 'douyin-browser-data');
|
|
if (fs.existsSync(douyinBrowserDataDir)) {
|
|
try {
|
|
let retries = 5;
|
|
while (retries > 0) {
|
|
try {
|
|
fs.rmSync(douyinBrowserDataDir, { recursive: true, force: true });
|
|
if (fs.existsSync(douyinBrowserDataDir)) throw new Error('目录仍存在');
|
|
|
|
Log.info(`[clearBrowserData] 已删除抖音浏览器数据目录: ${douyinBrowserDataDir}`);
|
|
break;
|
|
} catch (rmError: any) {
|
|
retries--;
|
|
if (retries === 0) throw rmError;
|
|
Log.warn(`[clearBrowserData] 删除抖音目录失败,等待1秒后重试 (${retries} left)...`);
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
Log.error(`[clearBrowserData] 删除抖音浏览器数据目录失败:`, error.message);
|
|
// 尝试重命名
|
|
try {
|
|
const trashPath = path.join(app.getPath('userData'), 'trash', `douyin_${Date.now()}`);
|
|
fs.mkdirSync(path.dirname(trashPath), { recursive: true });
|
|
fs.renameSync(douyinBrowserDataDir, trashPath);
|
|
Log.info(`[clearBrowserData] 删除失败,已将目录重命名为: ${trashPath}`);
|
|
} catch (mvError) { }
|
|
}
|
|
} else {
|
|
Log.info(`[clearBrowserData] 抖音数据目录不存在,无需删除: ${douyinBrowserDataDir}`);
|
|
}
|
|
}
|
|
|
|
// 4. 从数据库中删除该平台的所有账号
|
|
try {
|
|
const result = await DbMain.execute(
|
|
'DELETE FROM platform_accounts WHERE platform = ?',
|
|
[platform]
|
|
);
|
|
Log.info(`[clearBrowserData] 已从数据库删除${platform}的所有账号`);
|
|
} catch (error: any) {
|
|
Log.error(`[clearBrowserData] 删除数据库账号失败:`, error.message);
|
|
throw error;
|
|
}
|
|
|
|
// 🔥 4.5 关键步骤:清除内存中的浏览器实例和持久化上下文缓存
|
|
// 这是彻底清除cookies的关键!否则Playwright会从内存恢复旧数据
|
|
try {
|
|
// 关闭全局浏览器实例,强制释放内存中的cookies和session
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
} catch (e) {
|
|
Log.warn(`[clearBrowserData] browser.close() 失败:`, e);
|
|
}
|
|
browser = null;
|
|
}
|
|
|
|
// 清除所有浏览器上下文的缓存引用(包括该平台的)
|
|
browserContexts.clear();
|
|
|
|
Log.info(`[clearBrowserData] ✅ 已关闭全局浏览器实例和所有上下文,清除内存中的所有cookies和session数据`);
|
|
} catch (error: any) {
|
|
Log.warn(`[clearBrowserData] 清除内存缓存时出错(但可以继续):`, error.message);
|
|
}
|
|
|
|
Log.info(`[clearBrowserData] ${platform} 数据清除成功`);
|
|
return { success: true, message: `已清除${platform}的所有浏览器数据和登录信息` };
|
|
|
|
} catch (error: any) {
|
|
// 关键:确保记录完整的错误信息
|
|
Log.error('[clearBrowserData] 清除浏览器数据失败:', {
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
return {
|
|
success: false,
|
|
message: error.message || '未知错误'
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 刷新账号信息(真正打开浏览器检测登录状态)
|
|
*/
|
|
ipcMain.handle('platform:refreshAccount', async (event, params: { accountId: number }) => {
|
|
try {
|
|
const { accountId } = params;
|
|
|
|
const account = await DbMain.first(
|
|
'SELECT * FROM platform_accounts WHERE id = ?',
|
|
[accountId]
|
|
);
|
|
|
|
if (!account) {
|
|
return { success: false, message: '账号不存在' };
|
|
}
|
|
|
|
// 🔧 修改:调用 publish 模块的检查登录状态功能(这会真正打开浏览器检测)
|
|
const platform = account.platform;
|
|
Log.info(`开始刷新${platform}账号,真正检测登录状态`);
|
|
|
|
// 直接使用 publish 模块的函数来检测登录状态
|
|
const { getBrowserContext, closeBrowserContext } = await import('../publish/main');
|
|
const context = await getBrowserContext(platform);
|
|
const page = await context.newPage();
|
|
|
|
let isLoggedIn = false;
|
|
let userInfo: any = {};
|
|
|
|
try {
|
|
// 根据平台确定检查URL
|
|
let checkUrl = '';
|
|
if (platform === 'douyin') {
|
|
checkUrl = 'https://creator.douyin.com/creator-micro/content/upload';
|
|
} else if (platform === 'kuaishou') {
|
|
checkUrl = 'https://cp.kuaishou.com/article/publish';
|
|
} else if (platform === 'shipin') {
|
|
checkUrl = 'https://channels.weixin.qq.com/platform/post/create';
|
|
} else if (platform === 'xiaohongshu') {
|
|
checkUrl = 'https://creator.xiaohongshu.com/publish/publish';
|
|
}
|
|
|
|
if (checkUrl) {
|
|
await page.goto(checkUrl, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
const currentUrl = page.url();
|
|
|
|
// 检测登录状态
|
|
if (platform === 'douyin') {
|
|
const fileInputCount = await page.locator('input[type="file"]').count();
|
|
isLoggedIn = fileInputCount > 0 || (currentUrl.includes('creator.douyin.com') && !currentUrl.includes('login'));
|
|
userInfo.nickname = '用户';
|
|
} else if (platform === 'kuaishou') {
|
|
isLoggedIn = currentUrl.includes('cp.kuaishou.com') && !currentUrl.includes('login');
|
|
userInfo.nickname = '用户';
|
|
} else if (platform === 'shipin') {
|
|
const fileInputCount = await page.locator('input[type="file"]').count();
|
|
isLoggedIn = fileInputCount > 0 || (currentUrl.includes('channels.weixin.qq.com') && !currentUrl.includes('login'));
|
|
userInfo.nickname = '用户';
|
|
} else if (platform === 'xiaohongshu') {
|
|
const fileInputCount = await page.locator('input[type="file"]').count();
|
|
isLoggedIn = fileInputCount > 0 || (currentUrl.includes('creator.xiaohongshu.com') && !currentUrl.includes('login'));
|
|
userInfo.nickname = '用户';
|
|
}
|
|
|
|
// 如果已登录,获取最新的Cookie
|
|
if (isLoggedIn) {
|
|
const cookies = await context.cookies();
|
|
const now = Date.now();
|
|
|
|
// 更新数据库中的账号信息
|
|
await DbMain.execute(
|
|
'UPDATE platform_accounts SET cookies = ?, nickname = ?, updatedAt = ?, loginStatus = ? WHERE id = ?',
|
|
[JSON.stringify(cookies), userInfo.nickname || account.nickname, now, 'active', accountId]
|
|
);
|
|
|
|
Log.info(`${platform}刷新成功,已登录`);
|
|
} else {
|
|
// 未登录,更新状态为 inactive
|
|
await DbMain.execute(
|
|
'UPDATE platform_accounts SET loginStatus = ?, updatedAt = ? WHERE id = ?',
|
|
['inactive', Date.now(), accountId]
|
|
);
|
|
Log.info(`${platform}刷新完成,未登录`);
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
Log.error(`刷新${platform}账号时检测登录状态失败:`, error);
|
|
} finally {
|
|
// 关闭页面和浏览器
|
|
try {
|
|
if (page && !page.isClosed()) {
|
|
await page.close();
|
|
}
|
|
// 关闭浏览器上下文
|
|
await closeBrowserContext(platform);
|
|
} catch (closeError: any) {
|
|
Log.error('关闭浏览器失败:', closeError.message);
|
|
}
|
|
}
|
|
|
|
// 重新查询更新后的账号信息
|
|
const updatedAccount = await DbMain.first(
|
|
'SELECT * FROM platform_accounts WHERE id = ?',
|
|
[accountId]
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
isLoggedIn,
|
|
account: {
|
|
...updatedAccount,
|
|
cookies: null, // 不返回敏感信息
|
|
tokens: null
|
|
},
|
|
userInfo
|
|
};
|
|
|
|
} catch (error: any) {
|
|
Log.error('platform.refreshAccount error', error);
|
|
return {
|
|
success: false,
|
|
message: error.message
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 清理资源
|
|
*/
|
|
async function cleanup() {
|
|
for (const context of browserContexts.values()) {
|
|
await context.close();
|
|
}
|
|
browserContexts.clear();
|
|
|
|
if (browser) {
|
|
await browser.close();
|
|
browser = null;
|
|
}
|
|
|
|
loginSessions.clear();
|
|
}
|
|
|
|
// 应用退出时清理
|
|
process.on('exit', () => {
|
|
cleanup();
|
|
});
|
|
|
|
export default {
|
|
async init() {
|
|
Log.info('platform module initialized');
|
|
}
|
|
};
|