Initial clean project import
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 账号管理 IPC 处理器
|
||||
* 提供平台账号的增删改查功能
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import { Log } from '../log/main';
|
||||
import DbMain from '../db/main';
|
||||
import { PlatformAccount } from './types';
|
||||
|
||||
/**
|
||||
* 支持的平台列表
|
||||
*/
|
||||
const SUPPORTED_PLATFORMS = [
|
||||
{ key: 'douyin', name: '抖音' },
|
||||
{ key: 'kuaishou', name: '快手' },
|
||||
{ key: 'shipin', name: '视频号' },
|
||||
{ key: 'xiaohongshu', name: '小红书' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取支持的平台列表
|
||||
*/
|
||||
ipcMain.handle('account:getSupportedPlatforms', async () => {
|
||||
return { success: true, data: SUPPORTED_PLATFORMS };
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取所有账号列表
|
||||
*/
|
||||
ipcMain.handle('account:list', async (event, params?: { platform?: string }) => {
|
||||
try {
|
||||
let sql = 'SELECT * FROM platform_accounts';
|
||||
const sqlParams: any[] = [];
|
||||
|
||||
if (params?.platform) {
|
||||
sql += ' WHERE platform = ?';
|
||||
sqlParams.push(params.platform);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY updatedAt DESC';
|
||||
|
||||
const accounts = await DbMain.select(sql, sqlParams);
|
||||
|
||||
// 不返回 cookies 原文(太长),只返回是否有 cookies
|
||||
const safeAccounts = accounts.map((a: any) => ({
|
||||
...a,
|
||||
hasCookies: !!a.cookies && a.cookies.length > 10,
|
||||
cookies: undefined, // 移除 cookies 原文
|
||||
tokens: undefined, // 移除 tokens 原文
|
||||
}));
|
||||
|
||||
return { success: true, data: safeAccounts };
|
||||
} catch (error: any) {
|
||||
Log.error('account:list error', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 添加新账号
|
||||
*/
|
||||
ipcMain.handle('account:add', async (event, params: { platform: string; nickname?: string }) => {
|
||||
try {
|
||||
const { platform, nickname } = params;
|
||||
|
||||
// 验证平台
|
||||
if (!SUPPORTED_PLATFORMS.find(p => p.key === platform)) {
|
||||
return { success: false, message: `不支持的平台: ${platform}` };
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const accountId = await DbMain.insert(
|
||||
`INSERT INTO platform_accounts (platform, nickname, loginStatus, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[platform, nickname || '未登录', 'pending', now, now]
|
||||
);
|
||||
|
||||
Log.info(`新账号已创建: platform=${platform}, id=${accountId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { id: accountId, platform, nickname: nickname || '未登录', loginStatus: 'pending' }
|
||||
};
|
||||
} catch (error: any) {
|
||||
Log.error('account:add error', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
*/
|
||||
ipcMain.handle('account:delete', 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: '账号不存在' };
|
||||
}
|
||||
|
||||
await DbMain.execute('DELETE FROM platform_accounts WHERE id = ?', [accountId]);
|
||||
Log.info(`账号已删除: id=${accountId}, platform=${account.platform}`);
|
||||
|
||||
return { success: true, message: '账号已删除' };
|
||||
} catch (error: any) {
|
||||
Log.error('account:delete error', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新账号信息
|
||||
*/
|
||||
ipcMain.handle('account:update', async (event, params: { accountId: number; nickname?: string }) => {
|
||||
try {
|
||||
const { accountId, nickname } = params;
|
||||
const now = Date.now();
|
||||
|
||||
const updates: string[] = ['updatedAt = ?'];
|
||||
const values: any[] = [now];
|
||||
|
||||
if (nickname !== undefined) {
|
||||
updates.push('nickname = ?');
|
||||
values.push(nickname);
|
||||
}
|
||||
|
||||
values.push(accountId);
|
||||
|
||||
await DbMain.execute(
|
||||
`UPDATE platform_accounts SET ${updates.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
|
||||
return { success: true, message: '账号已更新' };
|
||||
} catch (error: any) {
|
||||
Log.error('account:update error', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取单个账号详情
|
||||
*/
|
||||
ipcMain.handle('account:get', async (event, params: { accountId: number }) => {
|
||||
try {
|
||||
const account = await DbMain.first(
|
||||
'SELECT * FROM platform_accounts WHERE id = ?',
|
||||
[params.accountId]
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
return { success: false, message: '账号不存在' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...account,
|
||||
hasCookies: !!account.cookies && account.cookies.length > 10,
|
||||
cookies: undefined,
|
||||
tokens: undefined,
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
Log.error('account:get error', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
import sharp from 'sharp';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PlatformCoverRequirements, CoverAdaptResult } from './types';
|
||||
import {Log} from '../log/main';
|
||||
|
||||
/**
|
||||
* 各平台封面要求
|
||||
*/
|
||||
const PLATFORM_REQUIREMENTS: Record<string, PlatformCoverRequirements> = {
|
||||
douyin: {
|
||||
width: 1080,
|
||||
height: 1920,
|
||||
ratio: '9:16',
|
||||
maxSize: 5000, // 5MB
|
||||
format: 'jpg'
|
||||
},
|
||||
kuaishou: {
|
||||
width: 1080,
|
||||
height: 1440,
|
||||
ratio: '3:4',
|
||||
maxSize: 3000,
|
||||
format: 'jpg'
|
||||
},
|
||||
shipin: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
ratio: '16:9',
|
||||
maxSize: 2000,
|
||||
format: 'jpg'
|
||||
},
|
||||
xiaohongshu: {
|
||||
width: 1080,
|
||||
height: 1440,
|
||||
ratio: '3:4',
|
||||
maxSize: 5000, // 5MB
|
||||
format: 'jpg'
|
||||
}
|
||||
};
|
||||
|
||||
export class CoverAdapter {
|
||||
/**
|
||||
* 智能适配封面到平台要求
|
||||
*/
|
||||
static async adaptCover(params: {
|
||||
coverPath: string;
|
||||
platform: string;
|
||||
outputDir: string;
|
||||
}): Promise<CoverAdaptResult> {
|
||||
const { coverPath, platform, outputDir } = params;
|
||||
const requirements = PLATFORM_REQUIREMENTS[platform];
|
||||
|
||||
if (!requirements) {
|
||||
throw new Error(`不支持的平台: ${platform}`);
|
||||
}
|
||||
|
||||
// 检查输入文件是否存在
|
||||
if (!fs.existsSync(coverPath)) {
|
||||
throw new Error(`封面文件不存在: ${coverPath}`);
|
||||
}
|
||||
|
||||
const adjustments: string[] = [];
|
||||
|
||||
try {
|
||||
// 1. 读取原始图片信息
|
||||
const image = sharp(coverPath);
|
||||
const metadata = await image.metadata();
|
||||
|
||||
if (!metadata.width || !metadata.height) {
|
||||
throw new Error('无法读取图片尺寸信息');
|
||||
}
|
||||
|
||||
const originalSize = {
|
||||
width: metadata.width,
|
||||
height: metadata.height
|
||||
};
|
||||
|
||||
Log.info(`原始封面尺寸: ${originalSize.width}x${originalSize.height}`, { platform });
|
||||
|
||||
// 2. 计算目标尺寸和裁剪策略
|
||||
const targetRatio = requirements.width / requirements.height;
|
||||
const currentRatio = originalSize.width / originalSize.height;
|
||||
|
||||
let processedImage = sharp(coverPath);
|
||||
|
||||
// 3. 处理宽高比不匹配的情况
|
||||
// 视频号和抖音都使用contain(保留完整内容),其他平台使用cover(裁剪)
|
||||
const usePadding = platform === 'shipin' || platform === 'douyin'; // 视频号和抖音使用填充而不是裁剪
|
||||
|
||||
if (Math.abs(currentRatio - targetRatio) > 0.01) {
|
||||
adjustments.push(`调整宽高比从${currentRatio.toFixed(2)}到${targetRatio.toFixed(2)}`);
|
||||
|
||||
if (!usePadding) {
|
||||
// 其他平台:裁剪策略
|
||||
if (currentRatio > targetRatio) {
|
||||
// 原图更宽,需要裁剪左右
|
||||
const newWidth = Math.round(originalSize.height * targetRatio);
|
||||
const left = Math.round((originalSize.width - newWidth) / 2);
|
||||
|
||||
processedImage = processedImage.extract({
|
||||
left: left,
|
||||
top: 0,
|
||||
width: newWidth,
|
||||
height: originalSize.height
|
||||
});
|
||||
|
||||
adjustments.push(`裁剪宽度: ${originalSize.width} -> ${newWidth} (居中裁剪)`);
|
||||
} else {
|
||||
// 原图更高,需要裁剪上下
|
||||
const newHeight = Math.round(originalSize.width / targetRatio);
|
||||
const top = Math.round((originalSize.height - newHeight) / 2);
|
||||
|
||||
processedImage = processedImage.extract({
|
||||
left: 0,
|
||||
top: top,
|
||||
width: originalSize.width,
|
||||
height: newHeight
|
||||
});
|
||||
|
||||
adjustments.push(`裁剪高度: ${originalSize.height} -> ${newHeight} (居中裁剪)`);
|
||||
}
|
||||
} else {
|
||||
// 视频号和抖音:使用填充(添加黑边保留完整内容)
|
||||
adjustments.push(`使用等比例缩放+黑边填充(保留完整图片)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调整到目标分辨率
|
||||
processedImage = processedImage.resize(requirements.width, requirements.height, {
|
||||
fit: usePadding ? 'contain' : 'cover', // 抖音和视频号使用contain保留完整内容,其他使用cover裁剪
|
||||
position: 'center',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 1 } // 黑色背景
|
||||
});
|
||||
|
||||
if (usePadding) {
|
||||
adjustments.push(`等比例缩放至: ${requirements.width}x${requirements.height} (添加黑边)`);
|
||||
} else {
|
||||
adjustments.push(`缩放至: ${requirements.width}x${requirements.height}`);
|
||||
}
|
||||
|
||||
// 5. 转换格式并调整质量
|
||||
let pipeline: sharp.Sharp;
|
||||
if (requirements.format === 'jpg') {
|
||||
pipeline = processedImage.jpeg({ quality: 90 });
|
||||
adjustments.push('转换为JPEG格式(质量90%)');
|
||||
} else if (requirements.format === 'png') {
|
||||
pipeline = processedImage.png({ compressionLevel: 9 });
|
||||
adjustments.push('转换为PNG格式');
|
||||
} else {
|
||||
pipeline = processedImage.webp({ quality: 90 });
|
||||
adjustments.push('转换为WebP格式(质量90%)');
|
||||
}
|
||||
|
||||
// 6. 创建输出目录(如果不存在)
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 7. 保存处理后的图片
|
||||
const timestamp = Date.now();
|
||||
const adaptedPath = path.join(outputDir, `cover_${platform}_${timestamp}.${requirements.format}`);
|
||||
await pipeline.toFile(adaptedPath);
|
||||
|
||||
// 8. 检查文件大小,如果超过限制则降低质量
|
||||
let fileSize = fs.statSync(adaptedPath).size / 1024; // KB
|
||||
|
||||
if (fileSize > requirements.maxSize) {
|
||||
adjustments.push(`文件过大(${fileSize.toFixed(0)}KB),压缩至${requirements.maxSize}KB以内`);
|
||||
|
||||
let quality = 80;
|
||||
while (fileSize > requirements.maxSize && quality > 50) {
|
||||
quality -= 10;
|
||||
|
||||
await sharp(coverPath)
|
||||
.resize(requirements.width, requirements.height, {
|
||||
fit: usePadding ? 'contain' : 'cover',
|
||||
position: 'center',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 1 }
|
||||
})
|
||||
.jpeg({ quality })
|
||||
.toFile(adaptedPath);
|
||||
|
||||
fileSize = fs.statSync(adaptedPath).size / 1024;
|
||||
}
|
||||
|
||||
adjustments.push(`最终质量: ${quality}%,文件大小: ${fileSize.toFixed(0)}KB`);
|
||||
}
|
||||
|
||||
Log.info(`封面适配完成: ${adaptedPath}`, { platform, adjustments });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
adaptedPath,
|
||||
originalSize,
|
||||
adaptedSize: {
|
||||
width: requirements.width,
|
||||
height: requirements.height
|
||||
},
|
||||
adjustments
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
Log.error(`封面适配失败`, { platform, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台预设要求
|
||||
*/
|
||||
static getPlatformRequirements(platform: string): PlatformCoverRequirements | null {
|
||||
return PLATFORM_REQUIREMENTS[platform] || null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 创建发布任务
|
||||
*/
|
||||
createTask: async (params: any) => {
|
||||
return await ipcRenderer.invoke('publish:createTask', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行发布
|
||||
*/
|
||||
execute: async (params: any) => {
|
||||
return await ipcRenderer.invoke('publish:execute', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取发布进度
|
||||
*/
|
||||
getProgress: async (params: { taskId: number }) => {
|
||||
return await ipcRenderer.invoke('publish:getProgress', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消发布任务
|
||||
*/
|
||||
cancel: async (params: { taskId: number }) => {
|
||||
return await ipcRenderer.invoke('publish:cancel', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询发布历史
|
||||
*/
|
||||
getHistory: async (params?: any) => {
|
||||
return await ipcRenderer.invoke('publish:getHistory', params || {});
|
||||
},
|
||||
|
||||
/**
|
||||
* 平台登录
|
||||
*/
|
||||
login: async (params: { platform: string; accountId?: number }) => {
|
||||
return await ipcRenderer.invoke('publish:login', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查平台登录状态
|
||||
*/
|
||||
checkLoginStatus: async (params: { platform: string; accountId?: number; checkBrowser?: boolean }) => {
|
||||
return await ipcRenderer.invoke('publish:checkLoginStatus', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭指定平台的浏览器
|
||||
* @param params.platform - 平台名称
|
||||
*/
|
||||
closeBrowser: async (params: { platform: string }) => {
|
||||
return await ipcRenderer.invoke('publish:closeBrowser', params);
|
||||
},
|
||||
|
||||
// ==================== 账号管理 ====================
|
||||
|
||||
/**
|
||||
* 获取支持的平台列表
|
||||
*/
|
||||
getSupportedPlatforms: async () => {
|
||||
return await ipcRenderer.invoke('account:getSupportedPlatforms');
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取账号列表
|
||||
* @param params.platform - 可选,按平台筛选
|
||||
*/
|
||||
listAccounts: async (params?: { platform?: string }) => {
|
||||
return await ipcRenderer.invoke('account:list', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加新账号
|
||||
*/
|
||||
addAccount: async (params: { platform: string; nickname?: string }) => {
|
||||
return await ipcRenderer.invoke('account:add', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除账号
|
||||
*/
|
||||
deleteAccount: async (params: { accountId: number }) => {
|
||||
return await ipcRenderer.invoke('account:delete', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新账号信息
|
||||
*/
|
||||
updateAccount: async (params: { accountId: number; nickname?: string }) => {
|
||||
return await ipcRenderer.invoke('account:update', params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取单个账号详情
|
||||
*/
|
||||
getAccount: async (params: { accountId: number }) => {
|
||||
return await ipcRenderer.invoke('account:get', params);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 发布任务配置
|
||||
*/
|
||||
export interface PublishTask {
|
||||
id: number;
|
||||
title: string;
|
||||
videoPath: string;
|
||||
coverPath: string;
|
||||
platforms: string[];
|
||||
publishConfig: {
|
||||
[platform: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
location?: string;
|
||||
visibility?: 'public' | 'private' | 'friends';
|
||||
allowComment?: boolean;
|
||||
allowDuet?: boolean;
|
||||
}
|
||||
};
|
||||
scheduledTime: number | null;
|
||||
status: 'pending' | 'uploading' | 'processing' | 'published' | 'failed';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布历史记录
|
||||
*/
|
||||
export interface PublishHistory {
|
||||
id: number;
|
||||
taskId: number;
|
||||
platform: string;
|
||||
accountId: number;
|
||||
videoUrl: string;
|
||||
videoId: string;
|
||||
status: 'success' | 'failed' | 'verification_required' | 'manual_pending';
|
||||
errorMessage: string;
|
||||
publishedAt: number;
|
||||
viewCount: number;
|
||||
likeCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封面适配要求
|
||||
*/
|
||||
export interface PlatformCoverRequirements {
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: string;
|
||||
maxSize: number; // KB
|
||||
format: 'jpg' | 'png' | 'webp';
|
||||
}
|
||||
|
||||
/**
|
||||
* 封面适配结果
|
||||
*/
|
||||
export interface CoverAdaptResult {
|
||||
success: boolean;
|
||||
adaptedPath: string;
|
||||
originalSize: { width: number; height: number };
|
||||
adaptedSize: { width: number; height: number };
|
||||
adjustments: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行发布的参数
|
||||
*/
|
||||
export interface ExecutePublishParams {
|
||||
taskId: number;
|
||||
accountId?: number;
|
||||
platform?: string;
|
||||
videoPath: string;
|
||||
coverPath: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
autoPublish?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台账号
|
||||
*/
|
||||
export interface PlatformAccount {
|
||||
id: number;
|
||||
platform: string;
|
||||
nickname: string;
|
||||
uid: string;
|
||||
avatar: string;
|
||||
cookies: string;
|
||||
tokens: string;
|
||||
loginStatus: 'pending' | 'active' | 'expired' | 'failed';
|
||||
expiresAt: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
Reference in New Issue
Block a user