Przeglądaj źródła

feat: 实现短信验证码前端交互与AI辅助处理

- 新增 Python 发布服务调用前端验证码输入接口,支持短信验证码交互
- 在 CaptchaDialog 组件中添加 message 属性,支持自定义提示信息
- 实现抖音平台短信验证码自动检测、发送按钮点击与AI状态分析
- 扩展发布服务传递用户ID、任务ID等上下文信息至Python服务
- 新增内部API端点处理Python服务的验证码请求,通过WebSocket转发至前端
- 优化有头浏览器重试逻辑,支持短信验证码自动处理流程
Ethanfly 17 godzin temu
rodzic
commit
ee1fd72d8c

+ 2 - 1
client/src/components/CaptchaDialog.vue

@@ -16,7 +16,7 @@
           <span class="phone">{{ phone || '***' }}</span> 
           收到的短信验证码
         </p>
-        <p class="captcha-tip">验证码已发送,请注意查收短信</p>
+        <p class="captcha-tip">{{ message || '验证码已发送,请注意查收短信' }}</p>
       </template>
       
       <!-- 图形验证码 -->
@@ -92,6 +92,7 @@ const props = defineProps<{
   type?: 'sms' | 'image';
   phone?: string;
   imageBase64?: string;
+  message?: string;
 }>();
 
 const emit = defineEmits<{

+ 1 - 0
client/src/layouts/MainLayout.vue

@@ -279,6 +279,7 @@
       :type="taskStore.captchaType"
       :phone="taskStore.captchaPhone"
       :image-base64="taskStore.captchaImageBase64"
+      :message="taskStore.captchaMessage"
       @submit="taskStore.submitCaptcha"
       @cancel="taskStore.cancelCaptcha"
     />

+ 4 - 0
client/src/stores/taskQueue.ts

@@ -26,6 +26,7 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
   const captchaType = ref<'sms' | 'image'>('sms');
   const captchaPhone = ref('');
   const captchaImageBase64 = ref('');
+  const captchaMessage = ref('');
 
   // 计算属性
   const activeTasks = computed(() => 
@@ -301,12 +302,14 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
     type?: 'sms' | 'image';
     phone?: string;
     imageBase64?: string;
+    message?: string;
   }) {
     console.log('[TaskQueue] Captcha required:', payload);
     captchaTaskId.value = payload.captchaTaskId || '';
     captchaType.value = payload.type || 'sms';
     captchaPhone.value = payload.phone || '';
     captchaImageBase64.value = payload.imageBase64 || '';
+    captchaMessage.value = payload.message || '';
     showCaptchaDialog.value = true;
   }
   
@@ -515,6 +518,7 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
     captchaType,
     captchaPhone,
     captchaImageBase64,
+    captchaMessage,
     // 计算属性
     activeTasks,
     runningTasks,

BIN
server/python/__pycache__/app.cpython-313.pyc


+ 12 - 0
server/python/app.py

@@ -474,6 +474,18 @@ def publish_ai_assisted():
         # 获取对应平台的发布器
         PublisherClass = get_publisher(platform)
         publisher = PublisherClass(headless=headless)  # 使用请求参数中的 headless 值
+        try:
+            publisher.user_id = int(data.get("user_id")) if data.get("user_id") is not None else None
+        except Exception:
+            publisher.user_id = None
+        try:
+            publisher.publish_task_id = int(data.get("publish_task_id")) if data.get("publish_task_id") is not None else None
+        except Exception:
+            publisher.publish_task_id = None
+        try:
+            publisher.publish_account_id = int(data.get("publish_account_id")) if data.get("publish_account_id") is not None else None
+        except Exception:
+            publisher.publish_account_id = None
         
         # 执行发布
         result = asyncio.run(publisher.run(cookie_str, params))

BIN
server/python/platforms/__pycache__/base.cpython-313.pyc


BIN
server/python/platforms/__pycache__/douyin.cpython-313.pyc


+ 189 - 0
server/python/platforms/base.py

@@ -7,6 +7,7 @@
 import asyncio
 import json
 import os
+import uuid
 from abc import ABC, abstractmethod
 from dataclasses import dataclass, field
 from datetime import datetime
@@ -173,6 +174,9 @@ class BasePublisher(ABC):
         self.context: Optional[BrowserContext] = None
         self.page: Optional[Page] = None
         self.on_progress: Optional[Callable[[int, str], None]] = None
+        self.user_id: Optional[int] = None
+        self.publish_task_id: Optional[int] = None
+        self.publish_account_id: Optional[int] = None
     
     def set_progress_callback(self, callback: Callable[[int, str], None]):
         """设置进度回调"""
@@ -263,6 +267,191 @@ class BasePublisher(ABC):
             print(f"[{self.platform_name}] 截图失败: {e}")
             return ""
 
+    async def request_sms_code_from_frontend(self, phone: str = "", timeout_seconds: int = 120, message: str = "") -> str:
+        node_api_url = os.environ.get('NODEJS_API_URL', 'http://localhost:3000').rstrip('/')
+        internal_api_key = os.environ.get('INTERNAL_API_KEY', 'internal-api-key-default')
+
+        if not self.user_id:
+            raise Exception("缺少 user_id,无法请求前端输入验证码")
+
+        captcha_task_id = f"py_{self.platform_name}_{uuid.uuid4().hex}"
+
+        payload = {
+            "user_id": self.user_id,
+            "captcha_task_id": captcha_task_id,
+            "type": "sms",
+            "phone": phone or "",
+            "message": message or "请输入短信验证码",
+            "timeout_seconds": timeout_seconds,
+            "publish_task_id": self.publish_task_id,
+            "publish_account_id": self.publish_account_id,
+        }
+
+        import requests
+        try:
+            resp = requests.post(
+                f"{node_api_url}/api/internal/captcha/request",
+                headers={
+                    "Content-Type": "application/json",
+                    "X-Internal-API-Key": internal_api_key,
+                },
+                json=payload,
+                timeout=timeout_seconds + 30,
+            )
+        except Exception as e:
+            raise Exception(f"请求前端验证码失败: {e}")
+
+        try:
+            data = resp.json()
+        except Exception:
+            raise Exception(f"请求前端验证码失败: HTTP {resp.status_code}")
+
+        if resp.status_code >= 400 or not data.get("success"):
+            raise Exception(data.get("error") or data.get("message") or f"请求前端验证码失败: HTTP {resp.status_code}")
+
+        code = data.get("code") or ""
+        if not code:
+            raise Exception("未收到验证码")
+        return str(code)
+
+    async def ai_analyze_sms_send_state(self, screenshot_base64: str = None) -> dict:
+        import os
+        import requests
+        import json
+        import re
+
+        try:
+            if not screenshot_base64:
+                screenshot_base64 = await self.capture_screenshot()
+
+            if not screenshot_base64:
+                return {
+                    "has_sms_modal": False,
+                    "send_button_state": "unknown",
+                    "sent_likely": False,
+                    "block_reason": "unknown",
+                    "suggested_action": "manual_send",
+                    "confidence": 0,
+                    "notes": "无法获取截图",
+                }
+
+            ai_api_key = os.environ.get('DASHSCOPE_API_KEY', '')
+            ai_base_url = os.environ.get('DASHSCOPE_BASE_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
+            ai_vision_model = os.environ.get('AI_VISION_MODEL', 'qwen-vl-plus')
+
+            if not ai_api_key:
+                return {
+                    "has_sms_modal": True,
+                    "send_button_state": "unknown",
+                    "sent_likely": False,
+                    "block_reason": "no_ai_key",
+                    "suggested_action": "manual_send",
+                    "confidence": 0,
+                    "notes": "未配置 AI API Key",
+                }
+
+            prompt = """请分析这张网页截图,判断是否处于“短信验证码”验证弹窗/页面,并判断“发送验证码/获取验证码”是否已经触发成功。
+
+你需要重点识别:
+1) 是否存在短信验证码弹窗(包含“请输入验证码/短信验证码/手机号验证/获取验证码/发送验证码”等)
+2) 发送按钮状态:enabled / disabled / countdown(出现xx秒) / hidden / unknown
+3) 是否已发送成功:例如出现倒计时、按钮禁用、出现“已发送/重新发送/xx秒后重试”等
+4) 是否被阻塞:例如出现滑块/人机验证、频繁发送、风控提示、网络异常等
+
+请以 JSON 返回:
+```json
+{
+  "has_sms_modal": true,
+  "send_button_state": "enabled|disabled|countdown|hidden|unknown",
+  "sent_likely": true,
+  "block_reason": "none|need_click_send|slider|risk|rate_limit|network|unknown",
+  "suggested_action": "wait|click_send|solve_slider|manual_send",
+  "confidence": 0-100,
+  "notes": "一句话说明你看到的证据"
+}
+```"""
+
+            headers = {
+                'Authorization': f'Bearer {ai_api_key}',
+                'Content-Type': 'application/json'
+            }
+
+            payload = {
+                "model": ai_vision_model,
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": [
+                            {
+                                "type": "image_url",
+                                "image_url": {
+                                    "url": f"data:image/jpeg;base64,{screenshot_base64}"
+                                }
+                            },
+                            {
+                                "type": "text",
+                                "text": prompt
+                            }
+                        ]
+                    }
+                ],
+                "max_tokens": 500
+            }
+
+            response = requests.post(
+                f"{ai_base_url}/chat/completions",
+                headers=headers,
+                json=payload,
+                timeout=30
+            )
+
+            if response.status_code != 200:
+                return {
+                    "has_sms_modal": True,
+                    "send_button_state": "unknown",
+                    "sent_likely": False,
+                    "block_reason": "network",
+                    "suggested_action": "manual_send",
+                    "confidence": 0,
+                    "notes": f"AI API 返回错误 {response.status_code}",
+                }
+
+            result = response.json()
+            ai_response = result.get('choices', [{}])[0].get('message', {}).get('content', '')
+
+            json_match = re.search(r'```json\\s*([\\s\\S]*?)\\s*```', ai_response)
+            if json_match:
+                json_str = json_match.group(1)
+            else:
+                json_match = re.search(r'\\{[\\s\\S]*\\}', ai_response)
+                json_str = json_match.group(0) if json_match else '{}'
+
+            try:
+                data = json.loads(json_str)
+            except Exception:
+                data = {}
+
+            return {
+                "has_sms_modal": bool(data.get("has_sms_modal", True)),
+                "send_button_state": data.get("send_button_state", "unknown"),
+                "sent_likely": bool(data.get("sent_likely", False)),
+                "block_reason": data.get("block_reason", "unknown"),
+                "suggested_action": data.get("suggested_action", "manual_send"),
+                "confidence": int(data.get("confidence", 0) or 0),
+                "notes": data.get("notes", ""),
+            }
+
+        except Exception as e:
+            return {
+                "has_sms_modal": True,
+                "send_button_state": "unknown",
+                "sent_likely": False,
+                "block_reason": "unknown",
+                "suggested_action": "manual_send",
+                "confidence": 0,
+                "notes": f"AI 分析异常: {e}",
+            }
+
     async def ai_check_captcha(self, screenshot_base64: str = None) -> dict:
         """
         使用 AI 分析截图检测验证码

+ 191 - 12
server/python/platforms/douyin.py

@@ -7,6 +7,7 @@
 import asyncio
 import os
 import json
+import re
 from datetime import datetime
 from typing import List
 from .base import (
@@ -131,6 +132,163 @@ class DouyinPublisher(BasePublisher):
             print(f"[{self.platform_name}] 验证码检测异常: {e}", flush=True)
         
         return {'need_captcha': False, 'captcha_type': ''}
+
+    async def handle_phone_captcha(self) -> bool:
+        if not self.page:
+            return False
+
+        try:
+            body_text = ""
+            try:
+                body_text = await self.page.inner_text("body")
+            except:
+                body_text = ""
+
+            phone_match = re.search(r"(1\d{2}\*{4}\d{4})", body_text or "")
+            masked_phone = phone_match.group(1) if phone_match else ""
+
+            async def _get_send_button():
+                candidates = [
+                    self.page.get_by_role("button", name="获取验证码"),
+                    self.page.get_by_role("button", name="发送验证码"),
+                    self.page.locator('button:has-text("获取验证码")'),
+                    self.page.locator('button:has-text("发送验证码")'),
+                    self.page.locator('[role="button"]:has-text("获取验证码")'),
+                    self.page.locator('[role="button"]:has-text("发送验证码")'),
+                ]
+                for c in candidates:
+                    try:
+                        if await c.count() > 0 and await c.first.is_visible():
+                            return c.first
+                    except:
+                        continue
+                return None
+
+            async def _confirm_sent() -> bool:
+                try:
+                    txt = ""
+                    try:
+                        txt = await self.page.inner_text("body")
+                    except:
+                        txt = ""
+                    if re.search(r"(\d+\s*秒)|(\d+\s*s)|后可重试|重新发送|已发送", txt or ""):
+                        return True
+                except:
+                    pass
+
+                try:
+                    btn = await _get_send_button()
+                    if btn:
+                        disabled = await btn.is_disabled()
+                        if disabled:
+                            return True
+                        label = (await btn.inner_text()) if btn else ""
+                        if re.search(r"(\d+\s*秒)|(\d+\s*s)|后可重试|重新发送|已发送", label or ""):
+                            return True
+                except:
+                    pass
+
+                return False
+
+            did_click_send = False
+            btn = await _get_send_button()
+            if btn:
+                try:
+                    if await btn.is_enabled():
+                        await btn.click(timeout=5000)
+                        did_click_send = True
+                        print(f"[{self.platform_name}] 已点击发送短信验证码", flush=True)
+                except Exception as e:
+                    print(f"[{self.platform_name}] 点击发送验证码按钮失败: {e}", flush=True)
+
+            if did_click_send:
+                try:
+                    await self.page.wait_for_timeout(800)
+                except:
+                    pass
+
+            sent_confirmed = await _confirm_sent() if did_click_send else False
+            ai_state = await self.ai_analyze_sms_send_state()
+            try:
+                if ai_state.get("sent_likely"):
+                    sent_confirmed = True
+            except:
+                pass
+
+            if (not did_click_send or not sent_confirmed) and ai_state.get("suggested_action") == "click_send":
+                btn2 = await _get_send_button()
+                if btn2:
+                    try:
+                        if await btn2.is_enabled():
+                            await btn2.click(timeout=5000)
+                            did_click_send = True
+                            await self.page.wait_for_timeout(800)
+                            sent_confirmed = await _confirm_sent()
+                            ai_state = await self.ai_analyze_sms_send_state()
+                            if ai_state.get("sent_likely"):
+                                sent_confirmed = True
+                    except:
+                        pass
+
+            code_hint = "请输入短信验证码。"
+            if ai_state.get("block_reason") == "slider":
+                code_hint = "检测到滑块/人机验证阻塞,请先在浏览器窗口完成验证后再发送短信验证码。"
+            elif ai_state.get("block_reason") in ["rate_limit", "risk"]:
+                code_hint = f"页面提示可能被限制/风控({ai_state.get('notes','') or '请稍后重试'})。可稍等后重新发送验证码。"
+            elif not did_click_send:
+                code_hint = "未找到或无法点击“发送验证码”按钮,请在弹出的浏览器页面手动点击发送后再输入验证码。"
+            elif sent_confirmed:
+                code_hint = f"已检测到短信验证码已发送({ai_state.get('notes','') or '请查收短信'})。"
+            else:
+                code_hint = f"已尝试点击发送验证码,但未确认发送成功({ai_state.get('notes','') or '请查看是否出现倒计时/重新发送'})。"
+
+            code = await self.request_sms_code_from_frontend(masked_phone, message=code_hint)
+
+            input_selectors = [
+                'input[placeholder*="验证码"]',
+                'input[placeholder*="短信"]',
+                'input[type="tel"]',
+                'input[type="text"]',
+            ]
+            filled = False
+            for selector in input_selectors:
+                try:
+                    el = self.page.locator(selector).first
+                    if await el.count() > 0:
+                        await el.fill(code)
+                        filled = True
+                        break
+                except:
+                    continue
+            if not filled:
+                raise Exception("未找到验证码输入框")
+
+            submit_selectors = [
+                'button:has-text("确定")',
+                'button:has-text("确认")',
+                'button:has-text("提交")',
+                'button:has-text("完成")',
+            ]
+            for selector in submit_selectors:
+                try:
+                    btn = self.page.locator(selector).first
+                    if await btn.count() > 0:
+                        await btn.click()
+                        break
+                except:
+                    continue
+
+            try:
+                await self.page.wait_for_timeout(1000)
+                await self.page.wait_for_selector('text="请输入验证码"', state="hidden", timeout=15000)
+            except:
+                pass
+
+            print(f"[{self.platform_name}] 短信验证码已提交,继续执行发布流程", flush=True)
+            return True
+        except Exception as e:
+            print(f"[{self.platform_name}] 处理短信验证码失败: {e}", flush=True)
+            return False
     
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
         """发布视频到抖音 - 参考 matrix/douyin_uploader/main.py"""
@@ -212,17 +370,34 @@ class DouyinPublisher(BasePublisher):
         captcha_result = await self.check_captcha()
         if captcha_result['need_captcha']:
             print(f"[{self.platform_name}] 传统方式检测到验证码: {captcha_result['captcha_type']}", flush=True)
-            screenshot_base64 = await self.capture_screenshot()
-            return PublishResult(
-                success=False,
-                platform=self.platform_name,
-                error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
-                need_captcha=True,
-                captcha_type=captcha_result['captcha_type'],
-                screenshot_base64=screenshot_base64,
-                page_url=current_url,
-                status='need_captcha'
-            )
+            if captcha_result['captcha_type'] == 'phone':
+                handled = await self.handle_phone_captcha()
+                if handled:
+                    self.report_progress(12, "短信验证码已处理,继续发布...")
+                else:
+                    screenshot_base64 = await self.capture_screenshot()
+                    return PublishResult(
+                        success=False,
+                        platform=self.platform_name,
+                        error="检测到手机验证码,但自动处理失败",
+                        need_captcha=True,
+                        captcha_type='phone',
+                        screenshot_base64=screenshot_base64,
+                        page_url=current_url,
+                        status='need_captcha'
+                    )
+            else:
+                screenshot_base64 = await self.capture_screenshot()
+                return PublishResult(
+                    success=False,
+                    platform=self.platform_name,
+                    error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
+                    need_captcha=True,
+                    captcha_type=captcha_result['captcha_type'],
+                    screenshot_base64=screenshot_base64,
+                    page_url=current_url,
+                    status='need_captcha'
+                )
         
         self.report_progress(15, "正在选择视频文件...")
         
@@ -379,6 +554,10 @@ class DouyinPublisher(BasePublisher):
                     ai_captcha = await self.ai_check_captcha()
                     if ai_captcha['has_captcha']:
                         print(f"[{self.platform_name}] AI检测到发布过程中需要验证码: {ai_captcha['captcha_type']}", flush=True)
+                        if ai_captcha['captcha_type'] == 'phone':
+                            handled = await self.handle_phone_captcha()
+                            if handled:
+                                continue
                         screenshot_base64 = await self.capture_screenshot()
                         page_url = await self.get_page_url()
                         return PublishResult(
@@ -890,4 +1069,4 @@ class DouyinPublisher(BasePublisher):
             'platform': self.platform_name,
             'work_comments': all_work_comments,
             'total': len(all_work_comments)
-        }
+        }

BIN
server/python/weixin_private_msg_349564.png


BIN
server/python/weixin_private_msg_355035.png


+ 4 - 0
server/src/automation/platforms/douyin.ts

@@ -912,6 +912,7 @@ export class DouyinAdapter extends BasePlatformAdapter {
         : undefined;
 
       // 使用 AI 辅助发布接口
+      const extra = (params.extra || {}) as Record<string, unknown>;
       const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
@@ -920,6 +921,9 @@ export class DouyinAdapter extends BasePlatformAdapter {
         body: JSON.stringify({
           platform: 'douyin',
           cookie: cookies,
+          user_id: extra.userId,
+          publish_task_id: extra.publishTaskId,
+          publish_account_id: extra.publishAccountId,
           title: params.title,
           description: params.description || params.title,
           video_path: absoluteVideoPath,

+ 51 - 0
server/src/routes/internal.ts

@@ -9,6 +9,8 @@ import { WorkDayStatisticsService } from '../services/WorkDayStatisticsService.j
 import { asyncHandler } from '../middleware/error.js';
 import { validateRequest } from '../middleware/validate.js';
 import { config } from '../config/index.js';
+import { wsManager } from '../websocket/index.js';
+import { WS_EVENTS } from '@media-manager/shared';
 
 const router = Router();
 const workDayStatisticsService = new WorkDayStatisticsService();
@@ -174,4 +176,53 @@ router.get(
   })
 );
 
+/**
+ * POST /api/internal/captcha/request
+ * Python 发布服务请求前端输入验证码(短信/图形)
+ */
+router.post(
+  '/captcha/request',
+  [
+    body('user_id').isInt().withMessage('user_id 必须是整数'),
+    body('captcha_task_id').notEmpty().withMessage('captcha_task_id 不能为空'),
+    body('type').isIn(['sms', 'image']).withMessage('type 必须是 sms 或 image'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const userId = Number(req.body.user_id);
+    const captchaTaskId = String(req.body.captcha_task_id);
+    const type = req.body.type as 'sms' | 'image';
+    const phone = (req.body.phone ? String(req.body.phone) : '') || '';
+    const imageBase64 = (req.body.image_base64 ? String(req.body.image_base64) : '') || '';
+    const message = (req.body.message ? String(req.body.message) : '') || '';
+
+    const code = await new Promise<string>((resolve, reject) => {
+      wsManager.sendToUser(userId, WS_EVENTS.CAPTCHA_REQUIRED, {
+        captchaTaskId,
+        type,
+        phone,
+        imageBase64,
+        message,
+      });
+
+      const timeout = setTimeout(() => {
+        wsManager.removeCaptchaListener(captchaTaskId);
+        reject(new Error('验证码输入超时'));
+      }, 120000);
+
+      wsManager.onCaptchaSubmit(captchaTaskId, (captchaCode: string) => {
+        clearTimeout(timeout);
+        wsManager.removeCaptchaListener(captchaTaskId);
+        if (!captchaCode) {
+          reject(new Error('验证码已取消'));
+          return;
+        }
+        resolve(captchaCode);
+      });
+    });
+
+    res.json({ success: true, code });
+  })
+);
+
 export default router;

+ 65 - 57
server/src/services/PublishService.ts

@@ -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);
   }