|
|
@@ -32,10 +32,10 @@ export class PublishService {
|
|
|
private taskRepository = AppDataSource.getRepository(PublishTask);
|
|
|
private resultRepository = AppDataSource.getRepository(PublishResult);
|
|
|
private accountRepository = AppDataSource.getRepository(PlatformAccount);
|
|
|
-
|
|
|
+
|
|
|
// 平台适配器映射
|
|
|
private adapters: Map<PlatformType, BasePlatformAdapter> = new Map();
|
|
|
-
|
|
|
+
|
|
|
constructor() {
|
|
|
// 初始化平台适配器
|
|
|
this.adapters.set('douyin', new DouyinAdapter());
|
|
|
@@ -45,7 +45,7 @@ export class PublishService {
|
|
|
this.adapters.set('bilibili', new BilibiliAdapter());
|
|
|
this.adapters.set('baijiahao', new BaijiahaoAdapter());
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 获取平台适配器
|
|
|
*/
|
|
|
@@ -99,10 +99,10 @@ export class PublishService {
|
|
|
// 验证目标账号是否存在
|
|
|
const validAccountIds: number[] = [];
|
|
|
const invalidAccountIds: number[] = [];
|
|
|
-
|
|
|
+
|
|
|
for (const accountId of data.targetAccounts) {
|
|
|
- const account = await this.accountRepository.findOne({
|
|
|
- where: { id: accountId, userId }
|
|
|
+ const account = await this.accountRepository.findOne({
|
|
|
+ where: { id: accountId, userId }
|
|
|
});
|
|
|
if (account) {
|
|
|
validAccountIds.push(accountId);
|
|
|
@@ -111,15 +111,15 @@ export class PublishService {
|
|
|
logger.warn(`[PublishService] Account ${accountId} not found or not owned by user ${userId}, skipping`);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (validAccountIds.length === 0) {
|
|
|
throw new AppError('所选账号不存在或已被删除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (invalidAccountIds.length > 0) {
|
|
|
logger.warn(`[PublishService] ${invalidAccountIds.length} invalid accounts skipped: ${invalidAccountIds.join(', ')}`);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const task = this.taskRepository.create({
|
|
|
userId,
|
|
|
videoPath: data.videoPath,
|
|
|
@@ -153,12 +153,12 @@ export class PublishService {
|
|
|
|
|
|
return this.formatTask(task);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 带进度回调的发布任务执行
|
|
|
*/
|
|
|
async executePublishTaskWithProgress(
|
|
|
- taskId: number,
|
|
|
+ taskId: number,
|
|
|
userId: number,
|
|
|
onProgress?: (progress: number, message: string) => void
|
|
|
): Promise<void> {
|
|
|
@@ -166,32 +166,32 @@ export class PublishService {
|
|
|
where: { id: taskId },
|
|
|
relations: ['results'],
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
if (!task) {
|
|
|
throw new Error(`Task ${taskId} not found`);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新任务状态为处理中
|
|
|
await this.taskRepository.update(taskId, { status: 'processing' });
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.TASK_STATUS_CHANGED, {
|
|
|
taskId,
|
|
|
status: 'processing',
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
const results = task.results || [];
|
|
|
let successCount = 0;
|
|
|
let failCount = 0;
|
|
|
const totalAccounts = results.length;
|
|
|
-
|
|
|
+
|
|
|
// 构建视频文件的完整路径
|
|
|
let videoPath = task.videoPath || '';
|
|
|
-
|
|
|
+
|
|
|
// 处理各种路径格式
|
|
|
if (videoPath) {
|
|
|
// 如果路径以 /uploads/ 开头,提取相对路径部分
|
|
|
if (videoPath.startsWith('/uploads/')) {
|
|
|
videoPath = path.join(config.upload.path, videoPath.replace('/uploads/', ''));
|
|
|
- }
|
|
|
+ }
|
|
|
// 如果是相对路径(不是绝对路径),拼接上传目录
|
|
|
else if (!path.isAbsolute(videoPath)) {
|
|
|
// 移除可能的重复 uploads 前缀
|
|
|
@@ -200,21 +200,21 @@ export class PublishService {
|
|
|
videoPath = path.join(config.upload.path, videoPath);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
logger.info(`Publishing video: ${videoPath}`);
|
|
|
onProgress?.(5, `准备发布到 ${totalAccounts} 个账号...`);
|
|
|
-
|
|
|
+
|
|
|
// 遍历所有目标账号,逐个发布
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
|
const result = results[i];
|
|
|
const accountProgress = Math.floor((i / totalAccounts) * 80) + 10;
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
// 获取账号信息
|
|
|
const account = await this.accountRepository.findOne({
|
|
|
where: { id: result.accountId },
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
if (!account) {
|
|
|
logger.warn(`Account ${result.accountId} not found`);
|
|
|
await this.resultRepository.update(result.id, {
|
|
|
@@ -224,7 +224,7 @@ export class PublishService {
|
|
|
failCount++;
|
|
|
continue;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (!account.cookieData) {
|
|
|
logger.warn(`Account ${result.accountId} has no cookies`);
|
|
|
await this.resultRepository.update(result.id, {
|
|
|
@@ -234,7 +234,7 @@ export class PublishService {
|
|
|
failCount++;
|
|
|
continue;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 解密 Cookie
|
|
|
let decryptedCookies: string;
|
|
|
try {
|
|
|
@@ -243,18 +243,18 @@ export class PublishService {
|
|
|
// 如果解密失败,可能是未加密的 Cookie
|
|
|
decryptedCookies = account.cookieData;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新发布结果的平台信息
|
|
|
await this.resultRepository.update(result.id, {
|
|
|
platform: account.platform,
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// 获取适配器
|
|
|
const adapter = this.getAdapter(account.platform as PlatformType);
|
|
|
-
|
|
|
+
|
|
|
logger.info(`Publishing to account ${account.accountName} (${account.platform})`);
|
|
|
onProgress?.(accountProgress, `正在发布到 ${account.accountName}...`);
|
|
|
-
|
|
|
+
|
|
|
// 发送进度通知
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
|
|
|
taskId,
|
|
|
@@ -264,22 +264,22 @@ export class PublishService {
|
|
|
progress: 0,
|
|
|
message: '开始发布...',
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// 验证码处理回调(支持短信验证码和图形验证码)
|
|
|
- const onCaptchaRequired = async (captchaInfo: {
|
|
|
- taskId: string;
|
|
|
+ const onCaptchaRequired = async (captchaInfo: {
|
|
|
+ taskId: string;
|
|
|
type: 'sms' | 'image';
|
|
|
phone?: string;
|
|
|
imageBase64?: string;
|
|
|
}): Promise<string> => {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const captchaTaskId = captchaInfo.taskId;
|
|
|
-
|
|
|
+
|
|
|
// 发送验证码请求到前端
|
|
|
- const message = captchaInfo.type === 'sms'
|
|
|
- ? '请输入短信验证码'
|
|
|
+ const message = captchaInfo.type === 'sms'
|
|
|
+ ? '请输入短信验证码'
|
|
|
: '请输入图片中的验证码';
|
|
|
-
|
|
|
+
|
|
|
logger.info(`[Publish] Requesting ${captchaInfo.type} captcha, taskId: ${captchaTaskId}, phone: ${captchaInfo.phone}`);
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.CAPTCHA_REQUIRED, {
|
|
|
taskId,
|
|
|
@@ -289,13 +289,13 @@ export class PublishService {
|
|
|
imageBase64: captchaInfo.imageBase64 || '',
|
|
|
message,
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
// 设置超时(2分钟)
|
|
|
const timeout = setTimeout(() => {
|
|
|
wsManager.removeCaptchaListener(captchaTaskId);
|
|
|
reject(new Error('验证码输入超时'));
|
|
|
}, 120000);
|
|
|
-
|
|
|
+
|
|
|
// 注册验证码监听
|
|
|
wsManager.onCaptchaSubmit(captchaTaskId, (code: string) => {
|
|
|
clearTimeout(timeout);
|
|
|
@@ -305,7 +305,7 @@ export class PublishService {
|
|
|
});
|
|
|
});
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
// 执行发布
|
|
|
const publishResult = await adapter.publishVideo(
|
|
|
decryptedCookies,
|
|
|
@@ -315,6 +315,11 @@ export class PublishService {
|
|
|
description: task.description || undefined,
|
|
|
coverPath: task.coverPath || undefined,
|
|
|
tags: task.tags || undefined,
|
|
|
+ extra: {
|
|
|
+ userId,
|
|
|
+ publishTaskId: taskId,
|
|
|
+ publishAccountId: account.id,
|
|
|
+ },
|
|
|
},
|
|
|
(progress, message) => {
|
|
|
// 发送进度更新
|
|
|
@@ -329,7 +334,7 @@ export class PublishService {
|
|
|
},
|
|
|
onCaptchaRequired
|
|
|
);
|
|
|
-
|
|
|
+
|
|
|
if (publishResult.success) {
|
|
|
await this.resultRepository.update(result.id, {
|
|
|
status: 'success',
|
|
|
@@ -338,7 +343,7 @@ export class PublishService {
|
|
|
publishedAt: new Date(),
|
|
|
});
|
|
|
successCount++;
|
|
|
-
|
|
|
+
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
|
|
|
taskId,
|
|
|
accountId: account.id,
|
|
|
@@ -353,7 +358,7 @@ export class PublishService {
|
|
|
errorMessage: publishResult.errorMessage || '发布失败',
|
|
|
});
|
|
|
failCount++;
|
|
|
-
|
|
|
+
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
|
|
|
taskId,
|
|
|
accountId: account.id,
|
|
|
@@ -363,10 +368,10 @@ export class PublishService {
|
|
|
message: publishResult.errorMessage || '发布失败',
|
|
|
});
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 每个账号发布后等待一段时间,避免过于频繁
|
|
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
|
-
|
|
|
+
|
|
|
} catch (error) {
|
|
|
logger.error(`Failed to publish to account ${result.accountId}:`, error);
|
|
|
await this.resultRepository.update(result.id, {
|
|
|
@@ -376,24 +381,24 @@ export class PublishService {
|
|
|
failCount++;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新任务状态
|
|
|
const finalStatus = failCount === 0 ? 'completed' : (successCount === 0 ? 'failed' : 'completed');
|
|
|
await this.taskRepository.update(taskId, {
|
|
|
status: finalStatus,
|
|
|
publishedAt: new Date(),
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
wsManager.sendToUser(userId, WS_EVENTS.TASK_STATUS_CHANGED, {
|
|
|
taskId,
|
|
|
status: finalStatus,
|
|
|
successCount,
|
|
|
failCount,
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
onProgress?.(100, `发布完成: ${successCount} 成功, ${failCount} 失败`);
|
|
|
logger.info(`Task ${taskId} completed: ${successCount} success, ${failCount} failed`);
|
|
|
-
|
|
|
+
|
|
|
// 发布成功后,自动创建同步作品任务
|
|
|
if (successCount > 0) {
|
|
|
// 收集成功发布的账号ID
|
|
|
@@ -403,7 +408,7 @@ export class PublishService {
|
|
|
successAccountIds.add(result.accountId);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 为每个成功的账号创建同步任务
|
|
|
for (const accountId of successAccountIds) {
|
|
|
const account = await this.accountRepository.findOne({ where: { id: accountId } });
|
|
|
@@ -418,7 +423,7 @@ export class PublishService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async cancelTask(userId: number, taskId: number): Promise<void> {
|
|
|
const task = await this.taskRepository.findOne({
|
|
|
where: { id: taskId, userId },
|
|
|
@@ -474,8 +479,8 @@ export class PublishService {
|
|
|
* 调用 Python API 以有头浏览器模式执行发布
|
|
|
*/
|
|
|
async retryAccountWithHeadfulBrowser(
|
|
|
- userId: number,
|
|
|
- taskId: number,
|
|
|
+ userId: number,
|
|
|
+ taskId: number,
|
|
|
accountId: number
|
|
|
): Promise<{ success: boolean; message: string; error?: string }> {
|
|
|
// 1. 验证任务存在
|
|
|
@@ -523,12 +528,12 @@ export class PublishService {
|
|
|
|
|
|
// 6. 调用 Python API(有头浏览器模式)
|
|
|
const PYTHON_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || 'http://localhost:5005';
|
|
|
-
|
|
|
+
|
|
|
logger.info(`[Headful Publish] Starting headful browser publish for account ${account.accountName} (${account.platform})`);
|
|
|
-
|
|
|
+
|
|
|
// 更新状态为处理中
|
|
|
await this.resultRepository.update(publishResult.id, {
|
|
|
- status: 'pending',
|
|
|
+ status: null,
|
|
|
errorMessage: null,
|
|
|
});
|
|
|
|
|
|
@@ -543,8 +548,8 @@ export class PublishService {
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
- const absoluteVideoPath = path.isAbsolute(videoPath)
|
|
|
- ? videoPath
|
|
|
+ const absoluteVideoPath = path.isAbsolute(videoPath)
|
|
|
+ ? videoPath
|
|
|
: path.resolve(process.cwd(), videoPath);
|
|
|
|
|
|
const response = await fetch(`${PYTHON_SERVICE_URL}/publish/ai-assisted`, {
|
|
|
@@ -553,6 +558,9 @@ export class PublishService {
|
|
|
body: JSON.stringify({
|
|
|
platform: account.platform,
|
|
|
cookie: decryptedCookies,
|
|
|
+ user_id: userId,
|
|
|
+ publish_task_id: taskId,
|
|
|
+ publish_account_id: accountId,
|
|
|
title: task.title,
|
|
|
description: task.description || task.title,
|
|
|
video_path: absoluteVideoPath,
|
|
|
@@ -634,7 +642,7 @@ export class PublishService {
|
|
|
if (!task) {
|
|
|
throw new AppError('任务不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 不能删除正在执行的任务
|
|
|
if (task.status === 'processing') {
|
|
|
throw new AppError('不能删除正在执行的任务', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
|
|
|
@@ -642,7 +650,7 @@ export class PublishService {
|
|
|
|
|
|
// 先删除关联的发布结果
|
|
|
await this.resultRepository.delete({ taskId });
|
|
|
-
|
|
|
+
|
|
|
// 再删除任务
|
|
|
await this.taskRepository.delete(taskId);
|
|
|
}
|