Ethanfly hai 10 horas
pai
achega
d40761c9d9

+ 1 - 0
client/src/auto-imports.d.ts

@@ -6,6 +6,7 @@
 export {}
 declare global {
   const EffectScope: typeof import('vue')['EffectScope']
+  const ElLoading: typeof import('element-plus/es')['ElLoading']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']

+ 51 - 19
client/src/views/Publish/index.vue

@@ -344,7 +344,7 @@
 <script setup lang="ts">
 import { ref, reactive, computed, onMounted, watch } from 'vue';
 import { Plus, Refresh, Search, Edit } from '@element-plus/icons-vue';
-import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus';
+import { ElMessage, ElMessageBox, ElLoading, type UploadFile } from 'element-plus';
 import { accountsApi } from '@/api/accounts';
 import request from '@/api/request';
 import { PLATFORMS } from '@media-manager/shared';
@@ -439,31 +439,63 @@ function isCaptchaError(errorMessage: string | null | undefined): boolean {
   return !!errorMessage && errorMessage.includes('CAPTCHA_REQUIRED');
 }
 
-// 打开浏览器标签页让用户完成验证码验证
-function openBrowserForCaptcha(accountId: number, platform: string) {
-  // 找到对应的账号
+// 使用有头浏览器重新执行发布流程(用于验证码场景)
+async function openBrowserForCaptcha(accountId: number, platform: string) {
+  if (!currentTask.value) {
+    ElMessage.error('任务信息不存在');
+    return;
+  }
+  
   const account = accounts.value.find(a => a.id === accountId);
   const accountName = account?.accountName || `账号${accountId}`;
+  const taskId = currentTask.value.id;
   
-  // 根据平台获取登录URL
-  const platformUrls: Record<string, string> = {
-    douyin: 'https://creator.douyin.com/',
-    bilibili: 'https://member.bilibili.com/',
-    kuaishou: 'https://cp.kuaishou.com/',
-    xiaohongshu: 'https://creator.xiaohongshu.com/',
-  };
+  // 确认操作
+  try {
+    await ElMessageBox.confirm(
+      `即将使用有头浏览器重新发布到 ${accountName}。\n\n浏览器会自动执行发布流程,遇到验证码时会暂停等待您手动操作。\n\n确认继续吗?`,
+      '有头浏览器发布',
+      {
+        confirmButtonText: '开始发布',
+        cancelButtonText: '取消',
+        type: 'info',
+      }
+    );
+  } catch {
+    // 用户取消
+    return;
+  }
   
-  const url = platformUrls[platform] || 'https://creator.douyin.com/';
+  // 关闭详情对话框
+  showDetailDialog.value = false;
   
-  // 打开浏览器标签页
-  tabsStore.openBrowserTab({
-    platform: platform as PlatformType,
-    url,
-    title: `验证码验证 - ${accountName}`,
+  // 显示正在执行的提示
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: `正在使用有头浏览器发布到 ${accountName},请在弹出的浏览器窗口中完成验证码验证...`,
+    background: 'rgba(0, 0, 0, 0.7)',
   });
   
-  ElMessage.info('请在浏览器标签页中完成验证码验证,完成后重试发布');
-  showDetailDialog.value = false;
+  try {
+    // 调用后端 API 执行有头浏览器发布
+    const result = await request.post(`/api/publish/${taskId}/retry-headful/${accountId}`);
+    
+    loadingInstance.close();
+    
+    if (result.success) {
+      ElMessage.success(`${accountName} 发布成功!`);
+    } else {
+      ElMessage.error(result.error || `${accountName} 发布失败`);
+    }
+    
+    // 刷新任务列表
+    await loadTasks();
+    
+  } catch (error: unknown) {
+    loadingInstance.close();
+    const errorMsg = error instanceof Error ? error.message : '发布失败';
+    ElMessage.error(errorMsg);
+  }
 }
 
 async function loadTasks() {

+ 26 - 2
server/python/app.py

@@ -16,6 +16,17 @@ import asyncio
 import os
 import sys
 import argparse
+
+# 禁用输出缓冲,确保 print 立即输出
+os.environ['PYTHONUNBUFFERED'] = '1'
+
+# 修复 Windows 终端中文输出乱码
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True)
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True)
+    # 设置环境变量
+    os.environ['PYTHONIOENCODING'] = 'utf-8'
 import traceback
 from datetime import datetime
 from pathlib import Path
@@ -108,6 +119,13 @@ def validate_video_file(video_path: str) -> bool:
 app = Flask(__name__)
 CORS(app)
 
+# 配置日志以显示所有 HTTP 请求
+import logging
+logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
+# 让 werkzeug 日志显示
+werkzeug_logger = logging.getLogger('werkzeug')
+werkzeug_logger.setLevel(logging.INFO)
+
 # 全局配置
 HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
 print(f"[Config] HEADLESS env value: '{os.environ.get('HEADLESS', 'NOT SET')}'", flush=True)
@@ -312,6 +330,10 @@ def publish_ai_assisted():
         post_time = data.get("post_time")
         location = data.get("location", "重庆市")
         return_screenshot = data.get("return_screenshot", True)
+        # 支持请求级别的 headless 参数,用于验证码场景下的有头浏览器模式
+        headless = data.get("headless", HEADLESS_MODE)
+        if isinstance(headless, str):
+            headless = headless.lower() == 'true'
         
         # 参数验证
         if not platform:
@@ -343,11 +365,12 @@ def publish_ai_assisted():
         print(f"[AI Publish] 平台: {platform}")
         print(f"[AI Publish] 标题: {title}")
         print(f"[AI Publish] 视频: {video_path}")
+        print(f"[AI Publish] Headless: {headless}")
         print("=" * 60)
         
         # 获取对应平台的发布器
         PublisherClass = get_publisher(platform)
-        publisher = PublisherClass(headless=HEADLESS_MODE)
+        publisher = PublisherClass(headless=headless)  # 使用请求参数中的 headless 值
         
         # 执行发布
         result = asyncio.run(publisher.run(cookie_str, params))
@@ -1393,7 +1416,8 @@ def main():
     print(f"启动服务: http://{args.host}:{args.port}")
     print("=" * 60)
     
-    app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
+    # 启用 debug 模式以获取详细日志,使用 use_reloader=False 避免重复启动
+    app.run(host=args.host, port=args.port, debug=True, threaded=True, use_reloader=False)
 
 
 if __name__ == '__main__':

BIN=BIN
server/python/debug_publish_failed_xiaohongshu.png


BIN=BIN
server/python/platforms/__pycache__/baijiahao.cpython-313.pyc


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


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


BIN=BIN
server/python/platforms/__pycache__/kuaishou.cpython-313.pyc


BIN=BIN
server/python/platforms/__pycache__/weixin.cpython-313.pyc


BIN=BIN
server/python/platforms/__pycache__/xiaohongshu.cpython-313.pyc


+ 133 - 38
server/python/platforms/baijiahao.py

@@ -226,14 +226,30 @@ class BaijiahaoPublisher(BasePublisher):
                     status='need_captcha'
                 )
         
-        # 检查验证码
+        # 使用 AI 检查验证码
+        ai_captcha = await self.ai_check_captcha()
+        if ai_captcha['has_captcha']:
+            print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=f"检测到{ai_captcha['captcha_type']}验证码,需要使用有头浏览器完成验证",
+                need_captcha=True,
+                captcha_type=ai_captcha['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        # 传统方式检查验证码
         captcha_result = await self.check_captcha()
         if captcha_result['need_captcha']:
             screenshot_base64 = await self.capture_screenshot()
             return PublishResult(
                 success=False,
                 platform=self.platform_name,
-                error=f"需要{captcha_result['captcha_type']}验证码",
+                error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
                 need_captcha=True,
                 captcha_type=captcha_result['captcha_type'],
                 screenshot_base64=screenshot_base64,
@@ -466,18 +482,29 @@ class BaijiahaoPublisher(BasePublisher):
         
         self.report_progress(80, "等待发布完成...")
         
-        # 等待发布完成(最多2分钟)
-        publish_timeout = 120
+        # 记录点击发布前的 URL
+        publish_page_url = self.page.url
+        print(f"[{self.platform_name}] 发布前 URL: {publish_page_url}")
+        
+        # 等待发布完成(最多3分钟)
+        publish_timeout = 180
         start_time = asyncio.get_event_loop().time()
+        last_url = publish_page_url
         
         while asyncio.get_event_loop().time() - start_time < publish_timeout:
             await asyncio.sleep(3)
             current_url = self.page.url
             
-            # 检查是否跳转到成功页面
-            if 'success' in current_url or 'content' in current_url or 'manage' in current_url:
+            # 检测 URL 是否发生变化
+            if current_url != last_url:
+                print(f"[{self.platform_name}] URL 变化: {last_url} -> {current_url}")
+                last_url = current_url
+            
+            # 检查是否跳转到内容管理页面(真正的成功标志)
+            # 百家号发布成功后会跳转到 /builder/rc/content 页面
+            if '/builder/rc/content' in current_url and 'edit' not in current_url:
                 self.report_progress(100, "发布成功!")
-                print(f"[{self.platform_name}] 发布成功,跳转到: {current_url}")
+                print(f"[{self.platform_name}] 发布成功,跳转到内容管理页: {current_url}")
                 screenshot_base64 = await self.capture_screenshot()
                 return PublishResult(
                     success=True,
@@ -488,38 +515,55 @@ class BaijiahaoPublisher(BasePublisher):
                     status='success'
                 )
             
-            # 检查是否有成功提示
+            # 检查是否有明确的成功提示弹窗
             try:
-                success_indicators = [
-                    'text="发布成功"',
-                    'text="提交成功"',
-                    '[class*="success"]',
-                ]
-                for indicator in success_indicators:
-                    if await self.page.locator(indicator).count() > 0:
-                        self.report_progress(100, "发布成功!")
-                        print(f"[{self.platform_name}] 检测到成功提示")
-                        screenshot_base64 = await self.capture_screenshot()
-                        return PublishResult(
-                            success=True,
-                            platform=self.platform_name,
-                            message="发布成功",
-                            screenshot_base64=screenshot_base64,
-                            page_url=current_url,
-                            status='success'
-                        )
-            except:
-                pass
+                # 百家号发布成功会显示"发布成功"弹窗
+                success_modal = self.page.locator('div:has-text("发布成功"), div:has-text("提交成功"), div:has-text("视频发布成功")').first
+                if await success_modal.count() > 0 and await success_modal.is_visible():
+                    self.report_progress(100, "发布成功!")
+                    print(f"[{self.platform_name}] 检测到发布成功弹窗")
+                    screenshot_base64 = await self.capture_screenshot()
+                    
+                    # 等待一下看是否会跳转
+                    await asyncio.sleep(3)
+                    
+                    return PublishResult(
+                        success=True,
+                        platform=self.platform_name,
+                        message="发布成功",
+                        screenshot_base64=screenshot_base64,
+                        page_url=self.page.url,
+                        status='success'
+                    )
+            except Exception as e:
+                print(f"[{self.platform_name}] 检测成功提示异常: {e}")
             
             # 检查是否有错误提示
             try:
-                error_el = self.page.locator('[class*="error"], [class*="fail"]').first
-                if await error_el.count() > 0:
-                    error_text = await error_el.text_content()
-                    if error_text and ('失败' in error_text or '错误' in error_text):
-                        raise Exception(f"发布失败: {error_text}")
-            except:
-                pass
+                error_selectors = [
+                    'div.error-tip',
+                    'div[class*="error-msg"]',
+                    'span[class*="error"]',
+                    'div:has-text("发布失败")',
+                    'div:has-text("提交失败")',
+                ]
+                for error_selector in error_selectors:
+                    error_el = self.page.locator(error_selector).first
+                    if await error_el.count() > 0 and await error_el.is_visible():
+                        error_text = await error_el.text_content()
+                        if error_text and error_text.strip():
+                            print(f"[{self.platform_name}] 检测到错误: {error_text}")
+                            screenshot_base64 = await self.capture_screenshot()
+                            return PublishResult(
+                                success=False,
+                                platform=self.platform_name,
+                                error=f"发布失败: {error_text.strip()}",
+                                screenshot_base64=screenshot_base64,
+                                page_url=current_url,
+                                status='failed'
+                            )
+            except Exception as e:
+                print(f"[{self.platform_name}] 检测错误提示异常: {e}")
             
             # 检查验证码
             captcha_result = await self.check_captcha()
@@ -535,15 +579,66 @@ class BaijiahaoPublisher(BasePublisher):
                     page_url=current_url,
                     status='need_captcha'
                 )
+            
+            # 检查发布按钮状态(如果还在编辑页面)
+            if 'edit' in current_url:
+                try:
+                    # 检查是否正在上传/处理中
+                    processing_indicators = [
+                        '[class*="loading"]',
+                        '[class*="uploading"]',
+                        '[class*="processing"]',
+                        'div:has-text("正在上传")',
+                        'div:has-text("正在处理")',
+                    ]
+                    is_processing = False
+                    for indicator in processing_indicators:
+                        if await self.page.locator(indicator).count() > 0:
+                            is_processing = True
+                            print(f"[{self.platform_name}] 正在处理中...")
+                            break
+                    
+                    if not is_processing:
+                        # 如果不是在处理中,可能需要重新点击发布按钮
+                        elapsed = asyncio.get_event_loop().time() - start_time
+                        if elapsed > 30:  # 30秒后还在编辑页且不在处理中,可能发布没生效
+                            print(f"[{self.platform_name}] 发布似乎未生效,尝试重新点击发布按钮...")
+                            for selector in publish_selectors:
+                                try:
+                                    btn = self.page.locator(selector).first
+                                    if await btn.count() > 0 and await btn.is_visible():
+                                        is_disabled = await btn.get_attribute('disabled')
+                                        if not is_disabled:
+                                            await btn.click()
+                                            print(f"[{self.platform_name}] 重新点击发布按钮")
+                                            break
+                                except:
+                                    pass
+                except Exception as e:
+                    print(f"[{self.platform_name}] 检查处理状态异常: {e}")
         
-        # 超时,返回截图供分析
+        # 超时,获取截图分析最终状态
+        print(f"[{self.platform_name}] 发布超时,最终 URL: {self.page.url}")
         screenshot_base64 = await self.capture_screenshot()
+        
+        # 最后一次检查是否在内容管理页
+        final_url = self.page.url
+        if '/builder/rc/content' in final_url and 'edit' not in final_url:
+            return PublishResult(
+                success=True,
+                platform=self.platform_name,
+                message="发布成功(延迟确认)",
+                screenshot_base64=screenshot_base64,
+                page_url=final_url,
+                status='success'
+            )
+        
         return PublishResult(
             success=False,
             platform=self.platform_name,
-            error="发布超时,请检查发布状态",
+            error="发布超时,请手动检查发布状态",
             screenshot_base64=screenshot_base64,
-            page_url=await self.get_page_url(),
+            page_url=final_url,
             status='need_action'
         )
     

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

@@ -261,6 +261,233 @@ class BasePublisher(ABC):
             print(f"[{self.platform_name}] 截图失败: {e}")
             return ""
 
+    async def ai_check_captcha(self, screenshot_base64: str = None) -> dict:
+        """
+        使用 AI 分析截图检测验证码
+        
+        Args:
+            screenshot_base64: 截图的 Base64 编码,如果为空则自动获取当前页面截图
+            
+        Returns:
+            dict: {
+                "has_captcha": bool,        # 是否有验证码
+                "captcha_type": str,        # 验证码类型: slider, image, phone, rotate, puzzle
+                "captcha_description": str, # 验证码描述
+                "confidence": float,        # 置信度 0-100
+                "need_headful": bool        # 是否需要切换到有头浏览器
+            }
+        """
+        import os
+        import requests
+        
+        try:
+            # 获取截图
+            if not screenshot_base64:
+                screenshot_base64 = await self.capture_screenshot()
+            
+            if not screenshot_base64:
+                print(f"[{self.platform_name}] AI验证码检测: 无法获取截图")
+                return {
+                    "has_captcha": False,
+                    "captcha_type": "",
+                    "captcha_description": "",
+                    "confidence": 0,
+                    "need_headful": False
+                }
+            
+            # 获取 AI 配置
+            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:
+                print(f"[{self.platform_name}] AI验证码检测: 未配置 AI API Key,使用传统方式检测")
+                return await self._traditional_captcha_check()
+            
+            # 构建 AI 请求
+            prompt = """请分析这张网页截图,判断页面上是否存在验证码。
+
+请检查以下类型的验证码:
+1. 滑块验证码(需要滑动滑块到指定位置)
+2. 图片验证码(需要选择正确的图片、点击图片上的文字等)
+3. 旋转验证码(需要旋转图片到正确角度)
+4. 拼图验证码(需要拖动拼图块到正确位置)
+5. 手机验证码(需要输入手机收到的验证码)
+6. 计算验证码(需要输入计算结果)
+
+请以 JSON 格式返回结果:
+```json
+{
+    "has_captcha": true/false,
+    "captcha_type": "slider/image/phone/rotate/puzzle/calculate/none",
+    "captcha_description": "验证码的具体描述",
+    "confidence": 0-100
+}
+```
+
+注意:
+- 如果页面有明显的验证码弹窗或验证区域,has_captcha 为 true
+- 如果只是普通的登录页面或表单,没有特殊的验证步骤,has_captcha 为 false
+- confidence 表示你对判断结果的信心,100 表示非常确定"""
+
+            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
+            }
+            
+            print(f"[{self.platform_name}] AI验证码检测: 正在分析截图...")
+            
+            response = requests.post(
+                f"{ai_base_url}/chat/completions",
+                headers=headers,
+                json=payload,
+                timeout=30
+            )
+            
+            if response.status_code != 200:
+                print(f"[{self.platform_name}] AI验证码检测: API 返回错误 {response.status_code}")
+                return await self._traditional_captcha_check()
+            
+            result = response.json()
+            ai_response = result.get('choices', [{}])[0].get('message', {}).get('content', '')
+            
+            print(f"[{self.platform_name}] AI验证码检测响应: {ai_response[:200]}...")
+            
+            # 解析 AI 响应
+            import re
+            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)
+                if json_match:
+                    json_str = json_match.group(0)
+                else:
+                    json_str = '{}'
+            
+            try:
+                ai_result = json.loads(json_str)
+            except:
+                ai_result = {}
+            
+            has_captcha = ai_result.get('has_captcha', False)
+            captcha_type = ai_result.get('captcha_type', '')
+            captcha_description = ai_result.get('captcha_description', '')
+            confidence = ai_result.get('confidence', 0)
+            
+            # 如果检测到验证码,需要切换到有头浏览器
+            need_headful = has_captcha and captcha_type not in ['none', '']
+            
+            print(f"[{self.platform_name}] AI验证码检测结果: has_captcha={has_captcha}, type={captcha_type}, confidence={confidence}")
+            
+            return {
+                "has_captcha": has_captcha,
+                "captcha_type": captcha_type if captcha_type != 'none' else '',
+                "captcha_description": captcha_description,
+                "confidence": confidence,
+                "need_headful": need_headful
+            }
+            
+        except Exception as e:
+            print(f"[{self.platform_name}] AI验证码检测异常: {e}")
+            import traceback
+            traceback.print_exc()
+            return await self._traditional_captcha_check()
+    
+    async def _traditional_captcha_check(self) -> dict:
+        """传统方式检测验证码(基于 DOM 元素)"""
+        if not self.page:
+            return {
+                "has_captcha": False,
+                "captcha_type": "",
+                "captcha_description": "",
+                "confidence": 0,
+                "need_headful": False
+            }
+        
+        try:
+            # 检查常见的验证码选择器
+            captcha_selectors = [
+                # 滑块验证码
+                ('[class*="slider"]', 'slider', '滑块验证码'),
+                ('[class*="slide-verify"]', 'slider', '滑块验证码'),
+                ('text="滑动"', 'slider', '滑块验证码'),
+                ('text="拖动"', 'slider', '滑块验证码'),
+                
+                # 图片验证码
+                ('[class*="captcha"]', 'image', '图片验证码'),
+                ('[class*="verify-img"]', 'image', '图片验证码'),
+                ('text="点击"', 'image', '图片验证码'),
+                ('text="选择"', 'image', '图片验证码'),
+                
+                # 手机验证码
+                ('text="验证码"', 'phone', '手机验证码'),
+                ('text="获取验证码"', 'phone', '手机验证码'),
+                ('[class*="sms-code"]', 'phone', '手机验证码'),
+                
+                # 旋转验证码
+                ('text="旋转"', 'rotate', '旋转验证码'),
+                ('[class*="rotate"]', 'rotate', '旋转验证码'),
+            ]
+            
+            for selector, captcha_type, description in captcha_selectors:
+                try:
+                    count = await self.page.locator(selector).count()
+                    if count > 0:
+                        # 检查是否可见
+                        element = self.page.locator(selector).first
+                        if await element.is_visible():
+                            print(f"[{self.platform_name}] 传统检测: 发现验证码 - {selector}")
+                            return {
+                                "has_captcha": True,
+                                "captcha_type": captcha_type,
+                                "captcha_description": description,
+                                "confidence": 80,
+                                "need_headful": True
+                            }
+                except:
+                    pass
+            
+            return {
+                "has_captcha": False,
+                "captcha_type": "",
+                "captcha_description": "",
+                "confidence": 80,
+                "need_headful": False
+            }
+        except Exception as e:
+            print(f"[{self.platform_name}] 传统验证码检测异常: {e}")
+            return {
+                "has_captcha": False,
+                "captcha_type": "",
+                "captcha_description": "",
+                "confidence": 0,
+                "need_headful": False
+            }
+
     async def get_page_url(self) -> str:
         """获取当前页面 URL"""
         if not self.page:

+ 144 - 94
server/python/platforms/douyin.py

@@ -133,7 +133,7 @@ class DouyinPublisher(BasePublisher):
         return {'need_captcha': False, 'captcha_type': ''}
     
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
-        """发布视频到抖音"""
+        """发布视频到抖音 - 参考 matrix/douyin_uploader/main.py"""
         print(f"\n{'='*60}")
         print(f"[{self.platform_name}] 开始发布视频")
         print(f"[{self.platform_name}] 视频路径: {params.video_path}")
@@ -163,114 +163,194 @@ class DouyinPublisher(BasePublisher):
         
         self.report_progress(10, "正在打开上传页面...")
         
-        # 访问上传页面
-        await self.page.goto(self.publish_url)
-        await self.page.wait_for_url(self.publish_url, timeout=30000)
+        # 访问上传页面 - 参考 matrix
+        await self.page.goto("https://creator.douyin.com/creator-micro/content/upload")
+        print(f"[{self.platform_name}] 等待页面加载...")
         
-        # 等待页面加载,检查验证码
-        await asyncio.sleep(2)
+        try:
+            await self.page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=30000)
+        except:
+            pass
+        
+        await asyncio.sleep(3)
+        
+        # 检查当前 URL 和页面状态
+        current_url = self.page.url
+        print(f"[{self.platform_name}] 当前 URL: {current_url}")
+        
+        # 检查是否在登录页面或需要登录
+        if "login" in current_url or "passport" in current_url:
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="Cookie 已过期,需要重新登录",
+                need_captcha=True,
+                captcha_type='login',
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        # 使用 AI 检测验证码
+        ai_captcha_result = await self.ai_check_captcha()
+        if ai_captcha_result['has_captcha']:
+            print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha_result['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=f"检测到{ai_captcha_result['captcha_type']}验证码,需要使用有头浏览器完成验证",
+                need_captcha=True,
+                captcha_type=ai_captcha_result['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        # 传统方式检测验证码
         captcha_result = await self.check_captcha()
         if captcha_result['need_captcha']:
-            print(f"[{self.platform_name}] 检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
+            print(f"[{self.platform_name}] 传统方式检测到验证码: {captcha_result['captcha_type']}", flush=True)
             screenshot_base64 = await self.capture_screenshot()
-            page_url = await self.get_page_url()
             return PublishResult(
                 success=False,
                 platform=self.platform_name,
-                error=f"需要{captcha_result['captcha_type']}验证码",
+                error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
                 need_captcha=True,
                 captcha_type=captcha_result['captcha_type'],
                 screenshot_base64=screenshot_base64,
-                page_url=page_url,
+                page_url=current_url,
                 status='need_captcha'
             )
         
         self.report_progress(15, "正在选择视频文件...")
         
-        # 点击上传区域
-        upload_div = self.page.locator("div[class*='container-drag']").first
-        async with self.page.expect_file_chooser() as fc_info:
-            await upload_div.click()
-        file_chooser = await fc_info.value
-        await file_chooser.set_files(params.video_path)
+        # 点击上传区域 - 参考 matrix: div.container-drag-info-Tl0RGH 或带 container-drag 的 div
+        upload_selectors = [
+            "div[class*='container-drag-info']",
+            "div[class*='container-drag']",
+            "div.upload-btn",
+            "div[class*='upload']",
+        ]
+        
+        upload_success = False
+        for selector in upload_selectors:
+            try:
+                upload_div = self.page.locator(selector).first
+                if await upload_div.count() > 0:
+                    print(f"[{self.platform_name}] 找到上传区域: {selector}")
+                    async with self.page.expect_file_chooser(timeout=10000) as fc_info:
+                        await upload_div.click()
+                    file_chooser = await fc_info.value
+                    await file_chooser.set_files(params.video_path)
+                    upload_success = True
+                    print(f"[{self.platform_name}] 视频文件已选择")
+                    break
+            except Exception as e:
+                print(f"[{self.platform_name}] 选择器 {selector} 失败: {e}")
+        
+        if not upload_success:
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="未找到上传入口",
+                screenshot_base64=screenshot_base64,
+                page_url=await self.get_page_url(),
+                status='failed'
+            )
         
-        # 等待跳转到发布页面
+        # 等待跳转到发布页面 - 参考 matrix
         self.report_progress(20, "等待进入发布页面...")
-        for _ in range(60):
+        for i in range(60):
             try:
+                # matrix 等待的 URL: https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page
                 await self.page.wait_for_url(
                     "https://creator.douyin.com/creator-micro/content/post/video*",
                     timeout=2000
                 )
+                print(f"[{self.platform_name}] 已进入发布页面")
                 break
             except:
+                print(f"[{self.platform_name}] 等待进入发布页面... {i+1}/60")
                 await asyncio.sleep(1)
         
         await asyncio.sleep(2)
         self.report_progress(30, "正在填充标题和话题...")
         
-        # 填写标题
+        # 填写标题 - 参考 matrix
         title_input = self.page.get_by_text('作品标题').locator("..").locator(
             "xpath=following-sibling::div[1]").locator("input")
         if await title_input.count():
             await title_input.fill(params.title[:30])
+            print(f"[{self.platform_name}] 标题已填写")
         else:
-            # 备用方式
+            # 备用方式 - 参考 matrix
             title_container = self.page.locator(".notranslate")
             await title_container.click()
+            await self.page.keyboard.press("Backspace")
             await self.page.keyboard.press("Control+KeyA")
             await self.page.keyboard.press("Delete")
             await self.page.keyboard.type(params.title)
             await self.page.keyboard.press("Enter")
+            print(f"[{self.platform_name}] 标题已填写(备用方式)")
         
-        # 添加话题标签
+        # 添加话题标签 - 参考 matrix
         if params.tags:
             css_selector = ".zone-container"
-            for tag in params.tags:
-                print(f"[{self.platform_name}] 添加话题: #{tag}")
+            for index, tag in enumerate(params.tags, start=1):
+                print(f"[{self.platform_name}] 正在添加第{index}个话题: #{tag}")
                 await self.page.type(css_selector, "#" + tag)
                 await self.page.press(css_selector, "Space")
         
         self.report_progress(40, "等待视频上传完成...")
         
-        # 等待视频上传完成
-        for _ in range(120):
+        # 等待视频上传完成 - 参考 matrix: 检测"重新上传"按钮
+        for i in range(120):
             try:
                 count = await self.page.locator("div").filter(has_text="重新上传").count()
                 if count > 0:
                     print(f"[{self.platform_name}] 视频上传完毕")
                     break
-                
-                # 检查上传错误
-                if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
-                    await self.handle_upload_error(params.video_path)
-                
-                await asyncio.sleep(3)
+                else:
+                    print(f"[{self.platform_name}] 正在上传视频中... {i+1}/120")
+                    
+                    # 检查上传错误
+                    if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
+                        print(f"[{self.platform_name}] 发现上传出错了,重新上传...")
+                        await self.handle_upload_error(params.video_path)
+                    
+                    await asyncio.sleep(3)
             except:
+                print(f"[{self.platform_name}] 正在上传视频中...")
                 await asyncio.sleep(3)
         
         self.report_progress(60, "处理视频设置...")
         
-        # 关闭弹窗
-        known_btn = self.page.get_by_role("button", name="我知道了")
-        if await known_btn.count() > 0:
-            await known_btn.first.click()
+        # 点击"我知道了"弹窗 - 参考 matrix
+        known_count = await self.page.get_by_role("button", name="我知道了").count()
+        if known_count > 0:
+            await self.page.get_by_role("button", name="我知道了").nth(0).click()
+            print(f"[{self.platform_name}] 关闭弹窗")
         
-        await asyncio.sleep(2)
+        await asyncio.sleep(5)
         
-        # 设置位置
+        # 设置位置 - 参考 matrix
         try:
             await self.page.locator('div.semi-select span:has-text("输入地理位置")').click()
             await asyncio.sleep(1)
+            await self.page.keyboard.press("Backspace")
             await self.page.keyboard.press("Control+KeyA")
             await self.page.keyboard.press("Delete")
             await self.page.keyboard.type(params.location)
             await asyncio.sleep(1)
             await self.page.locator('div[role="listbox"] [role="option"]').first.click()
+            print(f"[{self.platform_name}] 位置设置成功: {params.location}")
         except Exception as e:
             print(f"[{self.platform_name}] 设置位置失败: {e}")
         
-        # 开启头条/西瓜同步
+        # 开启头条/西瓜同步 - 参考 matrix
         try:
             third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
             if await self.page.locator(third_part_element).count():
@@ -279,6 +359,7 @@ class DouyinPublisher(BasePublisher):
                 if 'semi-switch-checked' not in class_name:
                     await self.page.locator(third_part_element).locator(
                         'input.semi-switch-native-control').click()
+                    print(f"[{self.platform_name}] 已开启头条/西瓜同步")
         except:
             pass
         
@@ -290,61 +371,41 @@ class DouyinPublisher(BasePublisher):
         self.report_progress(80, "正在发布...")
         print(f"[{self.platform_name}] 查找发布按钮...")
         
-        # 点击发布
-        publish_clicked = False
+        # 点击发布 - 参考 matrix
         for i in range(30):
             try:
-                # 每次循环都检查验证码
-                captcha_result = await self.check_captcha()
-                if captcha_result['need_captcha']:
-                    print(f"[{self.platform_name}] 发布过程中检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
-                    # 获取截图供 AI 分析
-                    screenshot_base64 = await self.capture_screenshot()
-                    page_url = await self.get_page_url()
-                    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=page_url,
-                        status='need_captcha'
-                    )
-                
-                publish_btn = self.page.get_by_role('button', name="发布", exact=True)
-                btn_count = await publish_btn.count()
-                print(f"[{self.platform_name}] 发布按钮数量: {btn_count}")
-                
-                if btn_count > 0:
-                    print(f"[{self.platform_name}] 点击发布按钮...")
-                    await publish_btn.click()
-                    publish_clicked = True
-                    
-                    # 点击后等待并检查验证码
-                    await asyncio.sleep(2)
-                    captcha_result = await self.check_captcha()
-                    if captcha_result['need_captcha']:
-                        print(f"[{self.platform_name}] 点击发布后需要验证码: {captcha_result['captcha_type']}", flush=True)
+                # 检查验证码(不要在每次循环都调 AI,太慢)
+                if i % 5 == 0:
+                    ai_captcha = await self.ai_check_captcha()
+                    if ai_captcha['has_captcha']:
+                        print(f"[{self.platform_name}] AI检测到发布过程中需要验证码: {ai_captcha['captcha_type']}", flush=True)
                         screenshot_base64 = await self.capture_screenshot()
                         page_url = await self.get_page_url()
                         return PublishResult(
                             success=False,
                             platform=self.platform_name,
-                            error=f"发布需要{captcha_result['captcha_type']}验证码",
+                            error=f"发布过程中需要{ai_captcha['captcha_type']}验证码,请使用有头浏览器完成验证",
                             need_captcha=True,
-                            captcha_type=captcha_result['captcha_type'],
+                            captcha_type=ai_captcha['captcha_type'],
                             screenshot_base64=screenshot_base64,
                             page_url=page_url,
                             status='need_captcha'
                         )
                 
+                publish_btn = self.page.get_by_role('button', name="发布", exact=True)
+                btn_count = await publish_btn.count()
+                
+                if btn_count > 0:
+                    print(f"[{self.platform_name}] 点击发布按钮...")
+                    await publish_btn.click()
+                
+                # 等待跳转到内容管理页面 - 参考 matrix
                 await self.page.wait_for_url(
                     "https://creator.douyin.com/creator-micro/content/manage",
                     timeout=5000
                 )
                 self.report_progress(100, "发布成功")
-                print(f"[{self.platform_name}] 发布成功! 已跳转到内容管理页面")
+                print(f"[{self.platform_name}] 视频发布成功!")
                 screenshot_base64 = await self.capture_screenshot()
                 page_url = await self.get_page_url()
                 return PublishResult(
@@ -357,11 +418,10 @@ class DouyinPublisher(BasePublisher):
                 )
             except Exception as e:
                 current_url = self.page.url
-                print(f"[{self.platform_name}] 尝试 {i+1}/30, 当前URL: {current_url}")
-                
-                if "content/manage" in current_url:
+                # 检查是否已经在管理页面
+                if "https://creator.douyin.com/creator-micro/content/manage" in current_url:
                     self.report_progress(100, "发布成功")
-                    print(f"[{self.platform_name}] 发布成功! 已在内容管理页面")
+                    print(f"[{self.platform_name}] 视频发布成功!")
                     screenshot_base64 = await self.capture_screenshot()
                     return PublishResult(
                         success=True,
@@ -371,22 +431,12 @@ class DouyinPublisher(BasePublisher):
                         page_url=current_url,
                         status='success'
                     )
-                
-                # 检查是否有错误提示
-                try:
-                    error_toast = self.page.locator('[class*="toast"][class*="error"], [class*="error-tip"]')
-                    if await error_toast.count() > 0:
-                        error_text = await error_toast.first.text_content()
-                        if error_text:
-                            print(f"[{self.platform_name}] 检测到错误提示: {error_text}")
-                            raise Exception(f"发布失败: {error_text}")
-                except:
-                    pass
-                
-                await asyncio.sleep(1)
+                else:
+                    print(f"[{self.platform_name}] 视频正在发布中... {i+1}/30, URL: {current_url}")
+                    await asyncio.sleep(1)
         
-        # 发布超时,返回截图供 AI 分析
-        print(f"[{self.platform_name}] 发布超时,获取截图供 AI 分析...")
+        # 发布超时
+        print(f"[{self.platform_name}] 发布超时,获取截图...")
         screenshot_base64 = await self.capture_screenshot()
         page_url = await self.get_page_url()
         

+ 103 - 23
server/python/platforms/kuaishou.py

@@ -67,14 +67,23 @@ class KuaishouPublisher(BasePublisher):
             print(f"[{self.platform_name}] 封面上传失败: {e}")
     
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
-        """发布视频到快手"""
+        """发布视频到快手 - 参考 matrix/ks_uploader/main.py"""
+        print(f"\n{'='*60}")
+        print(f"[{self.platform_name}] 开始发布视频")
+        print(f"[{self.platform_name}] 视频路径: {params.video_path}")
+        print(f"[{self.platform_name}] 标题: {params.title}")
+        print(f"[{self.platform_name}] Headless: {self.headless}")
+        print(f"{'='*60}")
+        
         self.report_progress(5, "正在初始化浏览器...")
         
         # 初始化浏览器
         await self.init_browser()
+        print(f"[{self.platform_name}] 浏览器初始化完成")
         
         # 解析并设置 cookies
         cookie_list = self.parse_cookies(cookies)
+        print(f"[{self.platform_name}] 解析到 {len(cookie_list)} 个 cookies")
         await self.set_cookies(cookie_list)
         
         if not self.page:
@@ -84,89 +93,160 @@ class KuaishouPublisher(BasePublisher):
         if not os.path.exists(params.video_path):
             raise Exception(f"视频文件不存在: {params.video_path}")
         
+        print(f"[{self.platform_name}] 视频文件存在,大小: {os.path.getsize(params.video_path)} bytes")
+        
         self.report_progress(10, "正在打开上传页面...")
         
-        # 访问上传页面
-        await self.page.goto(self.publish_url)
-        await self.page.wait_for_url(self.publish_url, timeout=30000)
+        # 访问上传页面 - 参考 matrix
+        await self.page.goto("https://cp.kuaishou.com/article/publish/video")
+        print(f"[{self.platform_name}] 等待页面加载...")
+        
+        try:
+            await self.page.wait_for_url("https://cp.kuaishou.com/article/publish/video", timeout=30000)
+        except:
+            pass
+        
+        await asyncio.sleep(3)
+        
+        # 检查是否跳转到登录页
+        current_url = self.page.url
+        print(f"[{self.platform_name}] 当前 URL: {current_url}")
+        
+        if "passport" in current_url or "login" in current_url:
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="Cookie 已过期,需要重新登录",
+                need_captcha=True,
+                captcha_type='login',
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        # 使用 AI 检查验证码
+        ai_captcha = await self.ai_check_captcha()
+        if ai_captcha['has_captcha']:
+            print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=f"检测到{ai_captcha['captcha_type']}验证码,需要使用有头浏览器完成验证",
+                need_captcha=True,
+                captcha_type=ai_captcha['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
         
         self.report_progress(15, "正在选择视频文件...")
         
-        # 点击上传按钮
+        # 点击上传按钮 - 参考 matrix: page.get_by_role("button", name="上传视频")
         upload_btn = self.page.get_by_role("button", name="上传视频")
-        async with self.page.expect_file_chooser() as fc_info:
+        async with self.page.expect_file_chooser(timeout=10000) as fc_info:
             await upload_btn.click()
         file_chooser = await fc_info.value
         await file_chooser.set_files(params.video_path)
+        print(f"[{self.platform_name}] 视频文件已选择")
         
         await asyncio.sleep(1)
         
-        # 关闭可能的弹窗
+        # 关闭可能的弹窗 - 参考 matrix
         known_btn = self.page.get_by_role("button", name="我知道了")
         if await known_btn.count():
             await known_btn.click()
+            print(f"[{self.platform_name}] 关闭弹窗")
         
         self.report_progress(20, "正在填充标题...")
         
-        # 填写标题
+        # 填写标题 - 参考 matrix
         await asyncio.sleep(1)
         title_input = self.page.get_by_placeholder('添加合适的话题和描述,作品能获得更多推荐~')
         if await title_input.count():
             await title_input.click()
             await title_input.fill(params.title[:30])
+            print(f"[{self.platform_name}] 标题已填写")
         
         self.report_progress(30, "等待视频上传完成...")
         
-        # 等待上传完成
-        for _ in range(120):
+        # 等待上传完成 - 参考 matrix: span:has-text("上传成功")
+        for i in range(120):
             try:
                 count = await self.page.locator('span:has-text("上传成功")').count()
                 if count > 0:
                     print(f"[{self.platform_name}] 视频上传完毕")
                     break
-                await asyncio.sleep(3)
+                else:
+                    print(f"[{self.platform_name}] 正在上传视频中... {i+1}/120")
+                    await asyncio.sleep(3)
             except:
+                print(f"[{self.platform_name}] 正在上传视频中...")
                 await asyncio.sleep(3)
         
         self.report_progress(50, "正在上传封面...")
         
-        # 上传封面
+        # 上传封面 - 参考 matrix
         await self.upload_cover(params.cover_path)
         
-        # 定时发布(快手暂不支持或选择器有变化)
-        # if params.publish_date:
-        #     await self.set_schedule_time(params.publish_date)
+        await asyncio.sleep(5)
         
         self.report_progress(80, "正在发布...")
         
-        # 点击发布
-        for _ in range(30):
+        # 点击发布 - 参考 matrix
+        for i in range(30):
             try:
                 publish_btn = self.page.get_by_role('button', name="发布", exact=True)
                 if await publish_btn.count():
+                    print(f"[{self.platform_name}] 点击发布按钮...")
                     await publish_btn.click()
+                
+                # 等待跳转到管理页面 - 参考 matrix: https://cp.kuaishou.com/article/manage/video?status=2&from=publish
                 await self.page.wait_for_url(
                     "https://cp.kuaishou.com/article/manage/video*",
-                    timeout=5000
+                    timeout=1500
                 )
                 self.report_progress(100, "发布成功")
+                print(f"[{self.platform_name}] 视频发布成功!")
+                screenshot_base64 = await self.capture_screenshot()
                 return PublishResult(
                     success=True,
                     platform=self.platform_name,
-                    message="发布成功"
+                    message="发布成功",
+                    screenshot_base64=screenshot_base64,
+                    page_url=self.page.url,
+                    status='success'
                 )
-            except:
+            except Exception as e:
                 current_url = self.page.url
                 if "manage/video" in current_url:
                     self.report_progress(100, "发布成功")
+                    print(f"[{self.platform_name}] 视频发布成功!")
+                    screenshot_base64 = await self.capture_screenshot()
                     return PublishResult(
                         success=True,
                         platform=self.platform_name,
-                        message="发布成功"
+                        message="发布成功",
+                        screenshot_base64=screenshot_base64,
+                        page_url=current_url,
+                        status='success'
                     )
-                await asyncio.sleep(1)
+                else:
+                    print(f"[{self.platform_name}] 视频正在发布中... {i+1}/30")
+                    await asyncio.sleep(0.5)
         
-        raise Exception("发布超时")
+        # 发布超时
+        screenshot_base64 = await self.capture_screenshot()
+        page_url = await self.get_page_url()
+        return PublishResult(
+            success=False,
+            platform=self.platform_name,
+            error="发布超时,请检查发布状态",
+            screenshot_base64=screenshot_base64,
+            page_url=page_url,
+            status='need_action'
+        )
 
     async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
         """获取快手作品列表"""

+ 92 - 35
server/python/platforms/weixin.py

@@ -50,15 +50,22 @@ class WeixinPublisher(BasePublisher):
     cookie_domain = ".weixin.qq.com"
     
     async def init_browser(self, storage_state: str = None):
-        """初始化浏览器 - 使用 Chrome 浏览器"""
+        """初始化浏览器 - 参考 matrix 使用 channel=chrome 避免 H264 编码错误"""
         from playwright.async_api import async_playwright
         
         playwright = await async_playwright().start()
-        # 使用 Chrome 浏览器,避免 H264 编码问题
-        self.browser = await playwright.chromium.launch(
-            headless=self.headless, 
-            channel="chrome"
-        )
+        
+        # 参考 matrix: 使用系统内的 Chrome 浏览器,避免 H264 编码错误
+        # 如果没有安装 Chrome,则使用默认 Chromium
+        try:
+            self.browser = await playwright.chromium.launch(
+                headless=self.headless,
+                channel="chrome"  # 使用系统 Chrome
+            )
+            print(f"[{self.platform_name}] 使用系统 Chrome 浏览器")
+        except Exception as e:
+            print(f"[{self.platform_name}] Chrome 不可用,使用 Chromium: {e}")
+            self.browser = await playwright.chromium.launch(headless=self.headless)
         
         if storage_state and os.path.exists(storage_state):
             self.context = await self.browser.new_context(storage_state=storage_state)
@@ -276,14 +283,30 @@ class WeixinPublisher(BasePublisher):
                 status='need_captcha'
             )
         
-        # 检查验证码
+        # 使用 AI 检查验证码
+        ai_captcha = await self.ai_check_captcha()
+        if ai_captcha['has_captcha']:
+            print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=f"检测到{ai_captcha['captcha_type']}验证码,需要使用有头浏览器完成验证",
+                need_captcha=True,
+                captcha_type=ai_captcha['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        # 传统方式检查验证码
         captcha_result = await self.check_captcha()
         if captcha_result['need_captcha']:
             screenshot_base64 = await self.capture_screenshot()
             return PublishResult(
                 success=False,
                 platform=self.platform_name,
-                error=f"需要{captcha_result['captcha_type']}验证码",
+                error=f"需要{captcha_result['captcha_type']}验证码,请使用有头浏览器完成验证",
                 need_captcha=True,
                 captcha_type=captcha_result['captcha_type'],
                 screenshot_base64=screenshot_base64,
@@ -293,33 +316,30 @@ class WeixinPublisher(BasePublisher):
         
         self.report_progress(15, "正在选择视频文件...")
         
-        # 上传视频 - 尝试多种方式
+        # 上传视频 - 参考 matrix/tencent_uploader/main.py
+        # matrix 使用: div.upload-content 点击后触发文件选择器
         upload_success = False
         
-        # 方法1: 直接通过 file input 上传
+        # 方法1: 参考 matrix - 点击 div.upload-content
         try:
-            file_inputs = await self.page.query_selector_all('input[type="file"]')
-            print(f"[{self.platform_name}] 找到 {len(file_inputs)} 个文件输入")
-            
-            for file_input in file_inputs:
-                try:
-                    await file_input.set_input_files(params.video_path)
-                    upload_success = True
-                    print(f"[{self.platform_name}] 通过 file input 上传成功")
-                    break
-                except Exception as e:
-                    print(f"[{self.platform_name}] file input 上传失败: {e}")
+            upload_div = self.page.locator("div.upload-content")
+            if await upload_div.count() > 0:
+                print(f"[{self.platform_name}] 找到 upload-content 上传区域")
+                async with self.page.expect_file_chooser(timeout=10000) as fc_info:
+                    await upload_div.click()
+                file_chooser = await fc_info.value
+                await file_chooser.set_files(params.video_path)
+                upload_success = True
+                print(f"[{self.platform_name}] 通过 upload-content 上传成功")
         except Exception as e:
-            print(f"[{self.platform_name}] 查找 file input 失败: {e}")
+            print(f"[{self.platform_name}] upload-content 上传失败: {e}")
         
-        # 方法2: 点击上传区域
+        # 方法2: 尝试其他选择器
         if not upload_success:
             upload_selectors = [
-                'div.upload-content',
-                'div[class*="upload"]',
+                'div[class*="upload-area"]',
                 'div[class*="drag-upload"]',
                 'div.add-wrap',
-                'div:has-text("上传视频")',
                 '[class*="uploader"]',
             ]
             
@@ -340,6 +360,17 @@ class WeixinPublisher(BasePublisher):
                 except Exception as e:
                     print(f"[{self.platform_name}] 选择器 {selector} 失败: {e}")
         
+        # 方法3: 直接设置 file input
+        if not upload_success:
+            try:
+                file_input = self.page.locator('input[type="file"]')
+                if await file_input.count() > 0:
+                    await file_input.first.set_input_files(params.video_path)
+                    upload_success = True
+                    print(f"[{self.platform_name}] 通过 file input 上传成功")
+            except Exception as e:
+                print(f"[{self.platform_name}] file input 上传失败: {e}")
+        
         if not upload_success:
             screenshot_base64 = await self.capture_screenshot()
             return PublishResult(
@@ -398,34 +429,60 @@ class WeixinPublisher(BasePublisher):
         
         self.report_progress(80, "正在发布...")
         
-        # 点击发布
-        for _ in range(30):
+        # 点击发布 - 参考 matrix
+        for i in range(30):
             try:
+                # 参考 matrix: div.form-btns button:has-text("发表")
                 publish_btn = self.page.locator('div.form-btns button:has-text("发表")')
                 if await publish_btn.count():
+                    print(f"[{self.platform_name}] 点击发布按钮...")
                     await publish_btn.click()
+                
+                # 等待跳转到作品列表页面 - 参考 matrix
                 await self.page.wait_for_url(
                     "https://channels.weixin.qq.com/platform/post/list",
                     timeout=10000
                 )
                 self.report_progress(100, "发布成功")
+                print(f"[{self.platform_name}] 视频发布成功!")
+                screenshot_base64 = await self.capture_screenshot()
                 return PublishResult(
                     success=True,
                     platform=self.platform_name,
-                    message="发布成功"
+                    message="发布成功",
+                    screenshot_base64=screenshot_base64,
+                    page_url=self.page.url,
+                    status='success'
                 )
-            except:
+            except Exception as e:
                 current_url = self.page.url
-                if "post/list" in current_url:
+                if "https://channels.weixin.qq.com/platform/post/list" in current_url:
                     self.report_progress(100, "发布成功")
+                    print(f"[{self.platform_name}] 视频发布成功!")
+                    screenshot_base64 = await self.capture_screenshot()
                     return PublishResult(
                         success=True,
                         platform=self.platform_name,
-                        message="发布成功"
+                        message="发布成功",
+                        screenshot_base64=screenshot_base64,
+                        page_url=current_url,
+                        status='success'
                     )
-                await asyncio.sleep(1)
-        
-        raise Exception("发布超时")
+                else:
+                    print(f"[{self.platform_name}] 视频正在发布中... {i+1}/30, URL: {current_url}")
+                    await asyncio.sleep(1)
+        
+        # 发布超时
+        screenshot_base64 = await self.capture_screenshot()
+        page_url = await self.get_page_url()
+        return PublishResult(
+            success=False,
+            platform=self.platform_name,
+            error="发布超时,请检查发布状态",
+            screenshot_base64=screenshot_base64,
+            page_url=page_url,
+            status='need_action'
+        )
 
     async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
         """获取视频号作品列表"""

+ 42 - 15
server/python/platforms/xiaohongshu.py

@@ -157,11 +157,12 @@ class XiaohongshuPublisher(BasePublisher):
         )
     
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
-        """发布视频到小红书"""
+        """发布视频到小红书 - 参考 matrix/xhs_uploader/main.py"""
         print(f"\n{'='*60}")
         print(f"[{self.platform_name}] 开始发布视频")
         print(f"[{self.platform_name}] 视频路径: {params.video_path}")
         print(f"[{self.platform_name}] 标题: {params.title}")
+        print(f"[{self.platform_name}] Headless: {self.headless}")
         print(f"[{self.platform_name}] XHS SDK 可用: {XHS_SDK_AVAILABLE}")
         print(f"{'='*60}")
         
@@ -173,20 +174,30 @@ class XiaohongshuPublisher(BasePublisher):
         
         self.report_progress(5, "正在准备发布...")
         
-        # 临时禁用 API 方式,直接使用 Playwright(更稳定)
-        # TODO: 后续优化 API 方式的返回值验证
-        # if XHS_SDK_AVAILABLE:
-        #     try:
-        #         result = await self.publish_via_api(cookies, params)
-        #         print(f"[{self.platform_name}] API 发布完成: {result}")
-        #         return result
-        #     except Exception as e:
-        #         import traceback
-        #         traceback.print_exc()
-        #         print(f"[{self.platform_name}] API 发布失败: {e}")
-        #         print(f"[{self.platform_name}] 尝试使用 Playwright 方式...")
-        
-        # 使用 Playwright 方式发布(更可靠)
+        # 参考 matrix: 优先使用 XHS SDK API 方式发布(更稳定)
+        if XHS_SDK_AVAILABLE:
+            try:
+                print(f"[{self.platform_name}] 尝试使用 XHS SDK API 发布...")
+                result = await self.publish_via_api(cookies, params)
+                print(f"[{self.platform_name}] API 发布完成: success={result.success}")
+                
+                # 如果 API 返回成功,直接返回
+                if result.success:
+                    return result
+                
+                # 如果 API 返回失败但有具体错误,也返回
+                if result.error and "请刷新" not in result.error:
+                    return result
+                    
+                # 其他情况尝试 Playwright 方式
+                print(f"[{self.platform_name}] API 方式未成功,尝试 Playwright...")
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                print(f"[{self.platform_name}] API 发布失败: {e}")
+                print(f"[{self.platform_name}] 尝试使用 Playwright 方式...")
+        
+        # 使用 Playwright 方式发布
         print(f"[{self.platform_name}] 使用 Playwright 方式发布...")
         return await self.publish_via_playwright(cookies, params)
     
@@ -229,6 +240,22 @@ class XiaohongshuPublisher(BasePublisher):
                 captcha_type='login'
             )
         
+        # 使用 AI 检查验证码
+        ai_captcha = await self.ai_check_captcha()
+        if ai_captcha['has_captcha']:
+            print(f"[{self.platform_name}] AI检测到验证码: {ai_captcha['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=f"检测到{ai_captcha['captcha_type']}验证码,需要使用有头浏览器完成验证",
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha',
+                need_captcha=True,
+                captcha_type=ai_captcha['captcha_type']
+            )
+        
         self.report_progress(20, "正在上传视频...")
         
         # 等待页面加载

+ 43 - 11
server/src/ai/index.ts

@@ -981,17 +981,49 @@ ${platformHint}
 
       const jsonMatch = response.match(/\{[\s\S]*\}/);
       if (jsonMatch) {
-        const result = JSON.parse(jsonMatch[0]);
-        return {
-          found: Boolean(result.found),
-          accountName: result.accountName || undefined,
-          accountId: result.accountId || undefined,
-          avatarDescription: result.avatarDescription || undefined,
-          fansCount: result.fansCount || undefined,
-          worksCount: result.worksCount || undefined,
-          otherInfo: result.otherInfo || undefined,
-          navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
-        };
+        let jsonStr = jsonMatch[0];
+
+        // 尝试修复常见的 JSON 格式问题
+        try {
+          // 1. 尝试直接解析
+          const result = JSON.parse(jsonStr);
+          return {
+            found: Boolean(result.found),
+            accountName: result.accountName || undefined,
+            accountId: result.accountId || undefined,
+            avatarDescription: result.avatarDescription || undefined,
+            fansCount: result.fansCount || undefined,
+            worksCount: result.worksCount || undefined,
+            otherInfo: result.otherInfo || undefined,
+            navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
+          };
+        } catch {
+          // 2. 修复单引号问题:将单引号替换为双引号(注意处理值中的单引号)
+          // 先替换属性名的单引号
+          jsonStr = jsonStr.replace(/'([^']+)':/g, '"$1":');
+          // 替换值的单引号(排除已经是双引号的)
+          jsonStr = jsonStr.replace(/:\s*'([^']*)'/g, ': "$1"');
+          // 移除末尾多余的逗号
+          jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1');
+
+          try {
+            const result = JSON.parse(jsonStr);
+            return {
+              found: Boolean(result.found),
+              accountName: result.accountName || undefined,
+              accountId: result.accountId || undefined,
+              avatarDescription: result.avatarDescription || undefined,
+              fansCount: result.fansCount || undefined,
+              worksCount: result.worksCount || undefined,
+              otherInfo: result.otherInfo || undefined,
+              navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
+            };
+          } catch (innerError) {
+            logger.error('extractAccountInfo JSON parse failed after fix attempt:', innerError);
+            logger.debug('Original JSON:', jsonMatch[0]);
+            logger.debug('Fixed JSON:', jsonStr);
+          }
+        }
       }
 
       return {

+ 30 - 16
server/src/automation/platforms/baijiahao.ts

@@ -324,26 +324,39 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    // 优先尝试使用 Python 服务
+    // 只使用 Python 服务发布
     const pythonAvailable = await this.checkPythonServiceAvailable();
-    if (pythonAvailable) {
-      logger.info('[Baijiahao] Python service available, using Python method');
-      try {
-        return await this.publishVideoViaPython(cookies, params, onProgress);
-      } catch (pythonError) {
-        logger.warn('[Baijiahao] Python publish failed:', pythonError);
-        // Python 服务可用但发布失败,可能是功能未实现
-        if (pythonError instanceof Error && pythonError.message.includes('暂未实现')) {
-          return {
-            success: false,
-            errorMessage: '百家号发布功能正在开发中,敬请期待',
-          };
-        }
-        onProgress?.(0, 'Python 服务发布失败,正在尝试浏览器模式...');
+    if (!pythonAvailable) {
+      logger.error('[Baijiahao] Python service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
+    }
+
+    logger.info('[Baijiahao] Using Python service for publishing');
+    try {
+      const result = await this.publishVideoViaPython(cookies, params, onProgress);
+      
+      // 检查是否需要验证码
+      if (!result.success && result.errorMessage?.includes('验证码')) {
+        logger.info('[Baijiahao] Python detected captcha, need headful browser');
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
+        };
       }
+      
+      return result;
+    } catch (pythonError) {
+      logger.error('[Baijiahao] Python publish failed:', pythonError);
+      return {
+        success: false,
+        errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     const useHeadless = options?.headless ?? true;
 
     try {
@@ -752,6 +765,7 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
   
   async getComments(cookies: string, videoId: string): Promise<CommentData[]> {

+ 30 - 11
server/src/automation/platforms/bilibili.ts

@@ -218,21 +218,39 @@ export class BilibiliAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    // 优先尝试使用 Python 服务
+    // 只使用 Python 服务发布
     const pythonAvailable = await this.checkPythonServiceAvailable();
-    if (pythonAvailable) {
-      logger.info('[Bilibili] Python service available, using Python method');
-      try {
-        return await this.publishVideoViaPython(cookies, params, onProgress);
-      } catch (pythonError) {
-        logger.warn('[Bilibili] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
+    if (!pythonAvailable) {
+      logger.error('[Bilibili] Python service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
+    }
+
+    logger.info('[Bilibili] Using Python service for publishing');
+    try {
+      const result = await this.publishVideoViaPython(cookies, params, onProgress);
+      
+      // 检查是否需要验证码
+      if (!result.success && result.errorMessage?.includes('验证码')) {
+        logger.info('[Bilibili] Python detected captcha, need headful browser');
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
+        };
       }
-    } else {
-      logger.info('[Bilibili] Python service not available, using Playwright method');
+      
+      return result;
+    } catch (pythonError) {
+      logger.error('[Bilibili] Python publish failed:', pythonError);
+      return {
+        success: false,
+        errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     const useHeadless = options?.headless ?? true;
 
     try {
@@ -521,6 +539,7 @@ export class BilibiliAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
   
   async getComments(cookies: string, videoId: string): Promise<CommentData[]> {

+ 38 - 33
server/src/automation/platforms/douyin.ts

@@ -1008,8 +1008,7 @@ export class DouyinAdapter extends BasePlatformAdapter {
   /**
    * 发布视频
    * 参考 https://github.com/kebenxiaoming/matrix 项目实现
-   * 优先使用 Python 服务,如果不可用则回退到 Playwright 方式
-   * 如果 Python 服务检测到需要验证码,自动切换到 Playwright headful 模式
+   * 只使用 Python 服务发布,如果检测到验证码返回错误让前端用有头浏览器重试
    * @param onCaptchaRequired 验证码回调,返回用户输入的验证码
    * @param options.headless 是否使用无头模式,默认 true
    */
@@ -1020,42 +1019,47 @@ export class DouyinAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; phone?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    let useHeadless = options?.headless ?? true;
-    let skipPython = false;
-
-    // 优先尝试使用 Python 服务
+    // 只使用 Python 服务发布
     const pythonAvailable = await this.checkPythonServiceAvailable();
-    if (pythonAvailable) {
-      logger.info('[Douyin] Python service available, using Python method');
-      try {
-        const pythonResult = await this.publishVideoViaPython(cookies, params, onProgress);
-
-        // 检查是否需要验证码
-        if (pythonResult.needCaptcha) {
-          logger.info(`[Douyin] Python detected captcha (${pythonResult.captchaType}), switching to Playwright headful mode`);
-          onProgress?.(5, `检测到${pythonResult.captchaType}验证码,正在打开浏览器...`);
-          // 切换到 headful 模式,让用户可以在浏览器中完成验证
-          useHeadless = false;
-          skipPython = true;
-        } else if (pythonResult.success) {
-          return pythonResult;
-        } else {
-          throw new Error(pythonResult.errorMessage || '发布失败');
-        }
-      } catch (pythonError) {
-        logger.warn('[Douyin] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
-      }
-    } else {
-      logger.info('[Douyin] Python service not available, using Playwright method');
+    if (!pythonAvailable) {
+      logger.error('[Douyin] Python service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
     }
 
-    // 如果因为验证码而切换,记录日志
-    if (skipPython) {
-      logger.info('[Douyin] Skipping Python due to captcha, using Playwright headful mode');
+    logger.info('[Douyin] Using Python service for publishing');
+    try {
+      const pythonResult = await this.publishVideoViaPython(cookies, params, onProgress);
+
+      // 检查是否需要验证码 - 返回错误让前端用有头浏览器重试
+      if (pythonResult.needCaptcha) {
+        logger.info(`[Douyin] Python detected captcha (${pythonResult.captchaType}), need headful browser`);
+        onProgress?.(0, `检测到${pythonResult.captchaType}验证码,请使用有头浏览器重试...`);
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:检测到${pythonResult.captchaType}验证码,需要使用有头浏览器完成验证`,
+        };
+      }
+      
+      if (pythonResult.success) {
+        return pythonResult;
+      }
+      
+      return {
+        success: false,
+        errorMessage: pythonResult.errorMessage || '发布失败',
+      };
+    } catch (pythonError) {
+      logger.error('[Douyin] Python publish failed:', pythonError);
+      return {
+        success: false,
+        errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     try {
       await this.initBrowser({ headless: useHeadless });
       await this.setCookies(cookies);
@@ -1693,6 +1697,7 @@ export class DouyinAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
 
   /**

+ 30 - 11
server/src/automation/platforms/kuaishou.ts

@@ -207,21 +207,39 @@ export class KuaishouAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    // 优先尝试使用 Python 服务
+    // 只使用 Python 服务发布
     const pythonAvailable = await this.checkPythonServiceAvailable();
-    if (pythonAvailable) {
-      logger.info('[Kuaishou] Python service available, using Python method');
-      try {
-        return await this.publishVideoViaPython(cookies, params, onProgress);
-      } catch (pythonError) {
-        logger.warn('[Kuaishou] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
+    if (!pythonAvailable) {
+      logger.error('[Kuaishou] Python service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
+    }
+
+    logger.info('[Kuaishou] Using Python service for publishing');
+    try {
+      const result = await this.publishVideoViaPython(cookies, params, onProgress);
+      
+      // 检查是否需要验证码
+      if (!result.success && result.errorMessage?.includes('验证码')) {
+        logger.info('[Kuaishou] Python detected captcha, need headful browser');
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
+        };
       }
-    } else {
-      logger.info('[Kuaishou] Python service not available, using Playwright method');
+      
+      return result;
+    } catch (pythonError) {
+      logger.error('[Kuaishou] Python publish failed:', pythonError);
+      return {
+        success: false,
+        errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     const useHeadless = options?.headless ?? true;
 
     try {
@@ -492,6 +510,7 @@ export class KuaishouAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
   
   /**

+ 30 - 11
server/src/automation/platforms/weixin.ts

@@ -289,21 +289,39 @@ export class WeixinAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    // 优先尝试使用 Python 服务
+    // 只使用 Python 服务发布
     const pythonAvailable = await this.checkPythonServiceAvailable();
-    if (pythonAvailable) {
-      logger.info('[Weixin] Python service available, using Python method');
-      try {
-        return await this.publishVideoViaPython(cookies, params, onProgress);
-      } catch (pythonError) {
-        logger.warn('[Weixin] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
+    if (!pythonAvailable) {
+      logger.error('[Weixin] Python service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
+    }
+
+    logger.info('[Weixin] Using Python service for publishing');
+    try {
+      const result = await this.publishVideoViaPython(cookies, params, onProgress);
+      
+      // 检查是否需要验证码
+      if (!result.success && result.errorMessage?.includes('验证码')) {
+        logger.info('[Weixin] Python detected captcha, need headful browser');
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
+        };
       }
-    } else {
-      logger.info('[Weixin] Python service not available, using Playwright method');
+      
+      return result;
+    } catch (pythonError) {
+      logger.error('[Weixin] Python publish failed:', pythonError);
+      return {
+        success: false,
+        errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     const useHeadless = options?.headless ?? true;
 
     try {
@@ -827,6 +845,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
   
   /**

+ 31 - 12
server/src/automation/platforms/xiaohongshu.ts

@@ -526,7 +526,7 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
 
   /**
    * 发布视频/笔记
-   * 优先使用 Python API 服务(更稳定),如果不可用则回退到 Playwright 方式
+   * 只使用 Python API 服务发布
    */
   async publishVideo(
     cookies: string,
@@ -535,21 +535,39 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; phone?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
-    // 优先尝试使用 Python API 服务
+    // 使用 Python API 服务
     const apiAvailable = await this.checkPythonServiceAvailable();
-    if (apiAvailable) {
-      logger.info('[Xiaohongshu] Python API service available, using API method');
-      try {
-        return await this.publishVideoViaApi(cookies, params, onProgress);
-      } catch (apiError) {
-        logger.warn('[Xiaohongshu] API publish failed, falling back to Playwright:', apiError);
-        onProgress?.(0, 'API发布失败,正在切换到浏览器模式...');
+    if (!apiAvailable) {
+      logger.error('[Xiaohongshu] Python API service not available');
+      return {
+        success: false,
+        errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
+      };
+    }
+
+    logger.info('[Xiaohongshu] Using Python API service for publishing');
+    try {
+      const result = await this.publishVideoViaApi(cookies, params, onProgress);
+      
+      // 检查是否需要验证码
+      if (!result.success && result.errorMessage?.includes('验证码')) {
+        logger.info('[Xiaohongshu] Python detected captcha, need headful browser');
+        return {
+          success: false,
+          errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
+        };
       }
-    } else {
-      logger.info('[Xiaohongshu] Python API service not available, using Playwright method');
+      
+      return result;
+    } catch (apiError) {
+      logger.error('[Xiaohongshu] Python API publish failed:', apiError);
+      return {
+        success: false,
+        errorMessage: apiError instanceof Error ? apiError.message : '发布失败',
+      };
     }
 
-    // 回退到 Playwright 方式
+    /* ========== Playwright 方式已注释,只使用 Python API ==========
     const useHeadless = options?.headless ?? true;
 
     try {
@@ -1250,6 +1268,7 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
         errorMessage: error instanceof Error ? error.message : '发布失败',
       };
     }
+    ========== Playwright 方式已注释结束 ========== */
   }
 
   /**

+ 24 - 0
server/src/routes/publish.ts

@@ -126,4 +126,28 @@ router.delete(
   })
 );
 
+// 单账号有头浏览器重试发布(用于验证码场景)
+router.post(
+  '/:taskId/retry-headful/:accountId',
+  [
+    param('taskId').isInt().withMessage('任务ID无效'),
+    param('accountId').isInt().withMessage('账号ID无效'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const userId = req.user!.userId;
+    const taskId = Number(req.params.taskId);
+    const accountId = Number(req.params.accountId);
+    
+    // 调用服务层执行有头浏览器发布
+    const result = await publishService.retryAccountWithHeadfulBrowser(
+      userId, 
+      taskId, 
+      accountId
+    );
+    
+    res.json({ success: true, data: result });
+  })
+);
+
 export default router;

+ 158 - 0
server/src/services/PublishService.ts

@@ -469,6 +469,164 @@ export class PublishService {
     return this.formatTask(updated!);
   }
 
+  /**
+   * 单账号有头浏览器重试发布(用于验证码场景)
+   * 调用 Python API 以有头浏览器模式执行发布
+   */
+  async retryAccountWithHeadfulBrowser(
+    userId: number, 
+    taskId: number, 
+    accountId: number
+  ): Promise<{ success: boolean; message: string; error?: string }> {
+    // 1. 验证任务存在
+    const task = await this.taskRepository.findOne({
+      where: { id: taskId, userId },
+      relations: ['results'],
+    });
+    if (!task) {
+      throw new AppError('任务不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
+    }
+
+    // 2. 获取账号信息
+    const account = await this.accountRepository.findOne({
+      where: { id: accountId },
+    });
+    if (!account) {
+      throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
+    }
+
+    // 3. 获取发布结果记录
+    const publishResult = task.results?.find(r => r.accountId === accountId);
+    if (!publishResult) {
+      throw new AppError('发布结果不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND);
+    }
+
+    // 4. 解密 Cookie
+    let decryptedCookies: string;
+    try {
+      decryptedCookies = CookieManager.decrypt(account.cookieData || '');
+    } catch {
+      decryptedCookies = account.cookieData || '';
+    }
+
+    // 5. 构建视频文件的完整路径
+    let videoPath = task.videoPath || '';
+    if (videoPath) {
+      if (videoPath.startsWith('/uploads/')) {
+        videoPath = path.join(config.upload.path, videoPath.replace('/uploads/', ''));
+      } else if (!path.isAbsolute(videoPath)) {
+        videoPath = videoPath.replace(/^uploads[\\\/]+uploads[\\\/]+/, '');
+        videoPath = videoPath.replace(/^uploads[\\\/]+/, '');
+        videoPath = path.join(config.upload.path, videoPath);
+      }
+    }
+
+    // 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',
+      errorMessage: null,
+    });
+
+    // 发送 WebSocket 通知
+    wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
+      taskId,
+      accountId: account.id,
+      platform: account.platform,
+      status: 'processing',
+      progress: 10,
+      message: '正在启动有头浏览器发布...',
+    });
+
+    try {
+      const absoluteVideoPath = path.isAbsolute(videoPath) 
+        ? videoPath 
+        : path.resolve(process.cwd(), videoPath);
+
+      const response = await fetch(`${PYTHON_SERVICE_URL}/publish/ai-assisted`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          platform: account.platform,
+          cookie: decryptedCookies,
+          title: task.title,
+          description: task.description || task.title,
+          video_path: absoluteVideoPath,
+          cover_path: task.coverPath ? path.resolve(process.cwd(), task.coverPath) : undefined,
+          tags: task.tags || [],
+          headless: false,  // 关键:使用有头浏览器模式
+        }),
+        signal: AbortSignal.timeout(600000), // 10分钟超时
+      });
+
+      const result = await response.json();
+
+      if (result.success) {
+        // 更新发布结果为成功
+        await this.resultRepository.update(publishResult.id, {
+          status: 'success',
+          videoUrl: result.video_url || null,
+          platformVideoId: result.video_id || null,
+          publishedAt: new Date(),
+          errorMessage: null,
+        });
+
+        wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
+          taskId,
+          accountId: account.id,
+          platform: account.platform,
+          status: 'success',
+          progress: 100,
+          message: '发布成功',
+        });
+
+        logger.info(`[Headful Publish] Success for account ${account.accountName}`);
+        return { success: true, message: '发布成功' };
+      } else {
+        // 发布失败
+        const errorMsg = result.error || '发布失败';
+        await this.resultRepository.update(publishResult.id, {
+          status: 'failed',
+          errorMessage: errorMsg,
+        });
+
+        wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
+          taskId,
+          accountId: account.id,
+          platform: account.platform,
+          status: 'failed',
+          progress: 0,
+          message: errorMsg,
+        });
+
+        logger.warn(`[Headful Publish] Failed for account ${account.accountName}: ${errorMsg}`);
+        return { success: false, message: '发布失败', error: errorMsg };
+      }
+    } catch (error) {
+      const errorMsg = error instanceof Error ? error.message : '发布失败';
+      await this.resultRepository.update(publishResult.id, {
+        status: 'failed',
+        errorMessage: errorMsg,
+      });
+
+      wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
+        taskId,
+        accountId: account.id,
+        platform: account.platform,
+        status: 'failed',
+        progress: 0,
+        message: errorMsg,
+      });
+
+      logger.error(`[Headful Publish] Error for account ${account.accountName}:`, error);
+      return { success: false, message: '发布失败', error: errorMsg };
+    }
+  }
+
   async deleteTask(userId: number, taskId: number): Promise<void> {
     const task = await this.taskRepository.findOne({
       where: { id: taskId, userId },