Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+174
View File
@@ -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 };
}
});
+214
View File
@@ -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
+104
View File
@@ -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);
},
};
+96
View File
@@ -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;
}