Ethanfly před 7 hodinami
rodič
revize
a9f750ea7e

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
client/dist-electron/main.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
client/dist-electron/preload.js


+ 6 - 1
client/src/components.d.ts

@@ -9,7 +9,6 @@ declare module 'vue' {
   export interface GlobalComponents {
     BrowserTab: typeof import('./components/BrowserTab.vue')['default']
     CaptchaDialog: typeof import('./components/CaptchaDialog.vue')['default']
-    Close: typeof import('@element-plus/icons-vue')['Close']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
@@ -17,10 +16,13 @@ declare module 'vue' {
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -42,11 +44,14 @@ declare module 'vue' {
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 1 - 29
server/python/app.py

@@ -17,12 +17,9 @@ import os
 import sys
 import argparse
 import traceback
-from datetime import datetime, date
+from datetime import datetime
 from pathlib import Path
 
-import pymysql
-from pymysql.cursors import DictCursor
-
 # 确保当前目录在 Python 路径中
 CURRENT_DIR = Path(__file__).parent.resolve()
 if str(CURRENT_DIR) not in sys.path:
@@ -116,31 +113,6 @@ HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
 print(f"[Config] HEADLESS env value: '{os.environ.get('HEADLESS', 'NOT SET')}'", flush=True)
 print(f"[Config] HEADLESS_MODE: {HEADLESS_MODE}", flush=True)
 
-# 数据库配置
-DB_CONFIG = {
-    'host': os.environ.get('DB_HOST', 'localhost'),
-    'port': int(os.environ.get('DB_PORT', 3306)),
-    'user': os.environ.get('DB_USERNAME', 'root'),
-    'password': os.environ.get('DB_PASSWORD', ''),
-    'database': os.environ.get('DB_DATABASE', 'media_manager'),
-    'charset': 'utf8mb4',
-    'cursorclass': DictCursor
-}
-print(f"[DB_CONFIG] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}, pwd_len={len(DB_CONFIG['password'])}", flush=True)
-
-
-def get_db_connection():
-    """获取数据库连接"""
-    print(f"[DEBUG DB] 正在连接数据库...", flush=True)
-    print(f"[DEBUG DB] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}", flush=True)
-    try:
-        conn = pymysql.connect(**DB_CONFIG)
-        print(f"[DEBUG DB] 数据库连接成功!", flush=True)
-        return conn
-    except Exception as e:
-        print(f"[DEBUG DB] 数据库连接失败: {e}", flush=True)
-        raise
-
 # ==================== 签名相关(小红书专用) ====================
 
 @app.route("/sign", methods=["POST"])

binární
server/python/platforms/__pycache__/baijiahao.cpython-313.pyc


binární
server/python/platforms/__pycache__/base.cpython-313.pyc


binární
server/python/platforms/__pycache__/douyin.cpython-313.pyc


binární
server/python/platforms/__pycache__/weixin.cpython-313.pyc


binární
server/python/platforms/__pycache__/xiaohongshu.cpython-313.pyc


+ 404 - 2
server/python/platforms/baijiahao.py

@@ -130,19 +130,421 @@ class BaijiahaoPublisher(BasePublisher):
         finally:
             await self.close_browser()
     
+    async def check_captcha(self) -> dict:
+        """检查页面是否需要验证码"""
+        if not self.page:
+            return {'need_captcha': False, 'captcha_type': ''}
+        
+        try:
+            # 检查各种验证码
+            captcha_selectors = [
+                'text="请输入验证码"',
+                'text="滑动验证"',
+                '[class*="captcha"]',
+                '[class*="verify"]',
+            ]
+            for selector in captcha_selectors:
+                try:
+                    if await self.page.locator(selector).count() > 0:
+                        print(f"[{self.platform_name}] 检测到验证码: {selector}")
+                        return {'need_captcha': True, 'captcha_type': 'image'}
+                except:
+                    pass
+            
+            # 检查登录弹窗
+            login_selectors = [
+                'text="请登录"',
+                'text="登录后继续"',
+                '[class*="login-dialog"]',
+            ]
+            for selector in login_selectors:
+                try:
+                    if await self.page.locator(selector).count() > 0:
+                        print(f"[{self.platform_name}] 检测到需要登录: {selector}")
+                        return {'need_captcha': True, 'captcha_type': 'login'}
+                except:
+                    pass
+                    
+        except Exception as e:
+            print(f"[{self.platform_name}] 验证码检测异常: {e}")
+        
+        return {'need_captcha': False, 'captcha_type': ''}
+
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
         """发布视频到百家号"""
+        import os
+        
         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}")
         
-        # TODO: 实现百家号视频发布逻辑
+        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:
+            raise Exception("Page not initialized")
+        
+        # 检查视频文件
+        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, "正在打开上传页面...")
+        
+        # 访问视频发布页面(使用新视频发布界面)
+        video_publish_url = "https://baijiahao.baidu.com/builder/rc/edit?type=videoV2&is_from_cms=1"
+        await self.page.goto(video_publish_url, wait_until="domcontentloaded", timeout=60000)
+        await asyncio.sleep(3)
+        
+        # 检查是否跳转到登录页
+        current_url = self.page.url
+        print(f"[{self.platform_name}] 当前页面: {current_url}")
+        
+        for indicator in self.login_indicators:
+            if indicator 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'
+                )
+        
+        # 检查验证码
+        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']}验证码",
+                need_captcha=True,
+                captcha_type=captcha_result['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
+        
+        self.report_progress(15, "正在选择视频文件...")
+        
+        # 等待页面加载完成
+        await asyncio.sleep(2)
+        
+        # 关闭可能的弹窗
+        try:
+            close_buttons = [
+                'button:has-text("我知道了")',
+                'button:has-text("知道了")',
+                '[class*="close"]',
+                '[class*="modal-close"]',
+            ]
+            for btn_selector in close_buttons:
+                try:
+                    btn = self.page.locator(btn_selector).first
+                    if await btn.count() > 0 and await btn.is_visible():
+                        await btn.click()
+                        await asyncio.sleep(0.5)
+                except:
+                    pass
+        except:
+            pass
+        
+        # 上传视频 - 尝试多种方式
+        upload_success = False
+        
+        # 方法1: 直接通过 file input 上传
+        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}")
+        except Exception as e:
+            print(f"[{self.platform_name}] 查找 file input 失败: {e}")
+        
+        # 方法2: 点击上传区域
+        if not upload_success:
+            upload_selectors = [
+                'div[class*="upload-box"]',
+                'div[class*="drag-upload"]',
+                'div[class*="uploader"]',
+                'div:has-text("点击上传")',
+                'div:has-text("选择文件")',
+                '[class*="upload-area"]',
+            ]
+            
+            for selector in upload_selectors:
+                if upload_success:
+                    break
+                try:
+                    upload_area = self.page.locator(selector).first
+                    if await upload_area.count() > 0:
+                        print(f"[{self.platform_name}] 尝试点击上传区域: {selector}")
+                        async with self.page.expect_file_chooser(timeout=10000) as fc_info:
+                            await upload_area.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'
+            )
+        
+        self.report_progress(20, "等待视频上传...")
+        
+        # 等待视频上传完成(最多5分钟)
+        upload_timeout = 300
+        start_time = asyncio.get_event_loop().time()
+        
+        while asyncio.get_event_loop().time() - start_time < upload_timeout:
+            # 检查上传进度
+            progress_text = ''
+            try:
+                progress_el = self.page.locator('[class*="progress"], [class*="percent"]').first
+                if await progress_el.count() > 0:
+                    progress_text = await progress_el.text_content()
+                    if progress_text:
+                        import re
+                        match = re.search(r'(\d+)%', progress_text)
+                        if match:
+                            pct = int(match.group(1))
+                            self.report_progress(20 + int(pct * 0.4), f"视频上传中 {pct}%...")
+                            if pct >= 100:
+                                print(f"[{self.platform_name}] 上传完成")
+                                break
+            except:
+                pass
+            
+            # 检查是否出现标题输入框(说明上传完成)
+            try:
+                title_input = self.page.locator('input[placeholder*="标题"], textarea[placeholder*="标题"], [class*="title-input"] input').first
+                if await title_input.count() > 0 and await title_input.is_visible():
+                    print(f"[{self.platform_name}] 检测到标题输入框,上传完成")
+                    break
+            except:
+                pass
+            
+            # 检查是否有错误提示
+            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
+            
+            await asyncio.sleep(3)
+        
+        self.report_progress(60, "正在填写标题...")
+        await asyncio.sleep(2)
+        
+        # 填写标题
+        title_filled = False
+        title_selectors = [
+            'input[placeholder*="标题"]',
+            'textarea[placeholder*="标题"]',
+            '[class*="title-input"] input',
+            '[class*="title"] input',
+            'input[maxlength]',
+        ]
+        
+        for selector in title_selectors:
+            if title_filled:
+                break
+            try:
+                title_input = self.page.locator(selector).first
+                if await title_input.count() > 0 and await title_input.is_visible():
+                    await title_input.click()
+                    await self.page.keyboard.press("Control+KeyA")
+                    await self.page.keyboard.type(params.title[:30])  # 百家号标题限制30字
+                    title_filled = True
+                    print(f"[{self.platform_name}] 标题填写成功")
+            except Exception as e:
+                print(f"[{self.platform_name}] 标题选择器 {selector} 失败: {e}")
+        
+        if not title_filled:
+            print(f"[{self.platform_name}] 警告: 未能填写标题")
+        
+        # 填写描述
+        if params.description:
+            self.report_progress(65, "正在填写描述...")
+            try:
+                desc_selectors = [
+                    'textarea[placeholder*="描述"]',
+                    'textarea[placeholder*="简介"]',
+                    '[class*="desc"] textarea',
+                    '[class*="description"] textarea',
+                ]
+                for selector in desc_selectors:
+                    try:
+                        desc_input = self.page.locator(selector).first
+                        if await desc_input.count() > 0 and await desc_input.is_visible():
+                            await desc_input.click()
+                            await self.page.keyboard.type(params.description[:200])
+                            print(f"[{self.platform_name}] 描述填写成功")
+                            break
+                    except:
+                        pass
+            except Exception as e:
+                print(f"[{self.platform_name}] 描述填写失败: {e}")
+        
+        self.report_progress(70, "正在发布...")
+        await asyncio.sleep(2)
+        
+        # 点击发布按钮
+        publish_selectors = [
+            'button:has-text("发布")',
+            'button:has-text("发表")',
+            'button:has-text("提交")',
+            '[class*="publish"] button',
+            '[class*="submit"] button',
+        ]
+        
+        publish_clicked = False
+        for selector in publish_selectors:
+            if publish_clicked:
+                break
+            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 is_disabled:
+                        print(f"[{self.platform_name}] 按钮 {selector} 被禁用")
+                        continue
+                    
+                    await btn.click()
+                    publish_clicked = True
+                    print(f"[{self.platform_name}] 点击发布按钮成功")
+            except Exception as e:
+                print(f"[{self.platform_name}] 发布按钮 {selector} 失败: {e}")
+        
+        if not publish_clicked:
+            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'
+            )
+        
+        self.report_progress(80, "等待发布完成...")
+        
+        # 等待发布完成(最多2分钟)
+        publish_timeout = 120
+        start_time = asyncio.get_event_loop().time()
+        
+        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:
+                self.report_progress(100, "发布成功!")
+                print(f"[{self.platform_name}] 发布成功,跳转到: {current_url}")
+                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'
+                )
+            
+            # 检查是否有成功提示
+            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
+            
+            # 检查是否有错误提示
+            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
+            
+            # 检查验证码
+            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']}验证码",
+                    need_captcha=True,
+                    captcha_type=captcha_result['captcha_type'],
+                    screenshot_base64=screenshot_base64,
+                    page_url=current_url,
+                    status='need_captcha'
+                )
+        
+        # 超时,返回截图供分析
+        screenshot_base64 = await self.capture_screenshot()
         return PublishResult(
             success=False,
             platform=self.platform_name,
-            error="百家号发布功能暂未实现"
+            error="发布超时,请检查发布状态",
+            screenshot_base64=screenshot_base64,
+            page_url=await self.get_page_url(),
+            status='need_action'
         )
     
     async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:

+ 142 - 8
server/python/platforms/weixin.py

@@ -184,15 +184,64 @@ class WeixinPublisher(BasePublisher):
         except Exception as e:
             print(f"[{self.platform_name}] 封面上传失败: {e}")
     
+    async def check_captcha(self) -> dict:
+        """检查页面是否需要验证码"""
+        if not self.page:
+            return {'need_captcha': False, 'captcha_type': ''}
+        
+        try:
+            # 检查各种验证码
+            captcha_selectors = [
+                'text="请输入验证码"',
+                'text="滑动验证"',
+                '[class*="captcha"]',
+                '[class*="verify"]',
+            ]
+            for selector in captcha_selectors:
+                try:
+                    if await self.page.locator(selector).count() > 0:
+                        print(f"[{self.platform_name}] 检测到验证码: {selector}")
+                        return {'need_captcha': True, 'captcha_type': 'image'}
+                except:
+                    pass
+            
+            # 检查登录弹窗
+            login_selectors = [
+                'text="请登录"',
+                'text="扫码登录"',
+                '[class*="login-dialog"]',
+            ]
+            for selector in login_selectors:
+                try:
+                    if await self.page.locator(selector).count() > 0:
+                        print(f"[{self.platform_name}] 检测到需要登录: {selector}")
+                        return {'need_captcha': True, 'captcha_type': 'login'}
+                except:
+                    pass
+                    
+        except Exception as e:
+            print(f"[{self.platform_name}] 验证码检测异常: {e}")
+        
+        return {'need_captcha': False, 'captcha_type': ''}
+
     async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
         """发布视频到视频号"""
+        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, "正在初始化浏览器...")
         
         # 初始化浏览器(使用 Chrome)
         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:
@@ -202,20 +251,105 @@ class WeixinPublisher(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)
+        await self.page.goto(self.publish_url, wait_until="domcontentloaded", timeout=60000)
+        await asyncio.sleep(3)
+        
+        # 检查是否跳转到登录页
+        current_url = self.page.url
+        print(f"[{self.platform_name}] 当前页面: {current_url}")
+        
+        if "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'
+            )
+        
+        # 检查验证码
+        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']}验证码",
+                need_captcha=True,
+                captcha_type=captcha_result['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha'
+            )
         
         self.report_progress(15, "正在选择视频文件...")
         
-        # 点击上传区域
-        upload_div = self.page.locator("div.upload-content")
-        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)
+        # 上传视频 - 尝试多种方式
+        upload_success = False
+        
+        # 方法1: 直接通过 file input 上传
+        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}")
+        except Exception as e:
+            print(f"[{self.platform_name}] 查找 file input 失败: {e}")
+        
+        # 方法2: 点击上传区域
+        if not upload_success:
+            upload_selectors = [
+                'div.upload-content',
+                'div[class*="upload"]',
+                'div[class*="drag-upload"]',
+                'div.add-wrap',
+                'div:has-text("上传视频")',
+                '[class*="uploader"]',
+            ]
+            
+            for selector in upload_selectors:
+                if upload_success:
+                    break
+                try:
+                    upload_area = self.page.locator(selector).first
+                    if await upload_area.count() > 0:
+                        print(f"[{self.platform_name}] 尝试点击上传区域: {selector}")
+                        async with self.page.expect_file_chooser(timeout=10000) as fc_info:
+                            await upload_area.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'
+            )
         
         self.report_progress(20, "正在填充标题和话题...")
         

+ 191 - 7
server/src/ai/index.ts

@@ -1019,11 +1019,34 @@ ${platformHint}
     platform: string,
     goal: string
   ): Promise<PageOperationGuide> {
+    // 根据目标添加特定的识别提示
+    let additionalHints = '';
+    if (goal.includes('上传') || goal.includes('upload')) {
+      additionalHints = `
+【重要】关于视频上传入口的识别提示:
+- 百家号:上传区域通常是一个虚线边框的大区域,包含云朵图标和"点击上传或将文件拖动入此区域"文字,整个虚线框区域都是可点击的上传入口
+- 微信视频号:
+  * 如果在首页,需要找"发表视频"按钮(通常是橙色/红色按钮)
+  * 如果在发布页面(标题显示"发表动态"),左侧会有一个带"+"号的矩形上传区域,下方有格式说明文字(如"上传时长8小时内...MP4/H.264格式"),点击这个"+"号区域即可上传
+- 小红书:上传区域通常有"上传视频"文字或拖拽区域
+- 抖音:上传区域通常是一个带有"发布视频"或上传图标的区域
+- 快手:找"上传视频"或拖拽上传区域
+- B站:找"投稿"或上传视频按钮
+
+如果页面不是发布页面,需要先找到进入发布页面的入口按钮(如"发表视频"、"发布"、"上传"、"投稿"等)。`;
+    }
+
     const prompt = `请分析这张${platform}平台的网页截图,我的目标是:${goal}
+${additionalHints}
+
+请告诉我下一步应该进行什么操作。仔细观察页面上的所有可点击元素,包括:
+- 带有虚线边框的拖拽上传区域(这是很常见的上传入口)
+- 带有"上传"、"发布"、"发表"、"投稿"等文字的按钮
+- 带有云朵、加号、上传箭头等图标的区域
 
-请告诉我下一步应该进行什么操作。如果需要点击某个元素,请尽可能提供:
+如果需要点击某个元素,请尽可能提供:
 1. 操作类型(点击、输入、滚动、等待、跳转)
-2. 目标元素的描述(文字内容、位置描述)
+2. 目标元素的描述(文字内容、位置描述、视觉特征
 3. 如果是点击操作,估计目标在截图中的大致位置(假设截图尺寸为 1920x1080,给出x,y坐标)
 4. 如果是输入操作,需要输入什么内容
 
@@ -1031,8 +1054,8 @@ ${platformHint}
 {
   "hasAction": true或false(是否需要执行操作),
   "actionType": "click"或"input"或"scroll"或"wait"或"navigate"或null,
-  "targetDescription": "目标元素的文字描述",
-  "targetSelector": "可能的CSS选择器,如果能推断的话",
+  "targetDescription": "目标元素的文字描述(如:虚线框上传区域、发表视频按钮等)",
+  "targetSelector": "可能的CSS选择器,常见的有:[class*='upload'], [class*='drag'], button:has-text('发布'), button:has-text('上传') 等",
   "targetPosition": {"x": 数字, "y": 数字} 或 null,
   "inputText": "需要输入的文字,如果不需要输入则为null",
   "explanation": "操作说明和原因"
@@ -1073,6 +1096,165 @@ ${platformHint}
   }
 
   /**
+   * 分析视频上传进度
+   * @param imageBase64 页面截图的base64
+   * @param platform 平台名称
+   * @returns 上传进度分析结果
+   */
+  async analyzeUploadProgress(
+    imageBase64: string,
+    platform: string
+  ): Promise<{
+    isUploading: boolean;
+    isComplete: boolean;
+    isFailed: boolean;
+    progress: number | null;
+    statusDescription: string;
+  }> {
+    const prompt = `请分析这张${platform}平台的网页截图,判断视频上传的状态。
+
+请仔细观察页面上是否有以下元素:
+1. 上传进度条(通常显示百分比,如 "50%"、"上传中 75%" 等)
+2. 上传完成标志(如 "上传成功"、"✓"、绿色勾选图标、"100%"、视频预览画面)
+3. 上传失败标志(如 "上传失败"、"重试"、红色错误提示)
+4. 视频处理中的提示(如 "处理中"、"转码中")
+
+请严格按照以下JSON格式返回:
+{
+  "isUploading": true或false(是否正在上传中,有进度条显示但未完成),
+  "isComplete": true或false(是否上传完成,进度达到100%或显示成功标志),
+  "isFailed": true或false(是否上传失败),
+  "progress": 数字或null(当前上传进度百分比,如果能识别到的话,范围0-100),
+  "statusDescription": "当前状态的文字描述"
+}`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 300,
+      });
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          isUploading: Boolean(result.isUploading),
+          isComplete: Boolean(result.isComplete),
+          isFailed: Boolean(result.isFailed),
+          progress: typeof result.progress === 'number' ? result.progress : null,
+          statusDescription: result.statusDescription || '未知状态',
+        };
+      }
+
+      return {
+        isUploading: false,
+        isComplete: false,
+        isFailed: false,
+        progress: null,
+        statusDescription: response,
+      };
+    } catch (error) {
+      logger.error('analyzeUploadProgress error:', error);
+      return {
+        isUploading: false,
+        isComplete: false,
+        isFailed: false,
+        progress: null,
+        statusDescription: '分析失败',
+      };
+    }
+  }
+
+  /**
+   * 分析点击发布按钮后的发布进度
+   * @param imageBase64 页面截图的base64
+   * @param platform 平台名称
+   * @returns 发布进度分析结果
+   */
+  async analyzePublishProgress(
+    imageBase64: string,
+    platform: string
+  ): Promise<{
+    isPublishing: boolean;
+    isComplete: boolean;
+    isFailed: boolean;
+    progress: number | null;
+    needAction: boolean;
+    actionDescription: string | null;
+    statusDescription: string;
+  }> {
+    const prompt = `请分析这张${platform}平台的网页截图,判断点击发布按钮后的发布状态。
+
+请仔细观察页面上是否有以下元素:
+1. 发布/上传进度条(通常显示百分比,如 "发布中 50%"、"上传中 79%"、"正在发布..."、进度圈等)
+2. 后台上传提示(如 "作品上传中,请勿关闭页面"、"上传完成后将自动发布" + 百分比进度)
+3. 发布成功标志(如 "发布成功"、"已发布"、绿色勾选图标)
+4. 发布失败标志(如 "发布失败"、"重试"、红色错误提示)
+5. 需要处理的弹窗(如确认弹窗、验证码弹窗、协议确认等)
+6. 正在处理中的提示(如 "视频处理中"、"审核中"、loading动画)
+
+【重要】特别注意:
+- 抖音等平台在点击发布后可能会跳转到作品管理页面,但页面右下角会有一个小的上传进度框显示"作品上传中,请勿关闭页面 XX%"
+- 只要页面上有任何上传/发布进度条(无论在页面哪个位置),都应该认为发布尚未完成
+- 只有当进度达到100%且没有任何进度提示时,才算发布完成
+
+请严格按照以下JSON格式返回:
+{
+  "isPublishing": true或false(是否正在发布/上传中,只要有进度条或loading显示就返回true),
+  "isComplete": true或false(是否完全完成,进度100%且无任何上传提示才返回true),
+  "isFailed": true或false(是否发布失败),
+  "progress": 数字或null(当前进度百分比,仔细查找页面上的百分比数字,范围0-100),
+  "needAction": true或false(是否需要用户处理某些操作,如确认弹窗),
+  "actionDescription": "需要执行的操作描述,如果不需要操作则为null",
+  "statusDescription": "当前状态的文字描述(包括进度信息)"
+}`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 400,
+      });
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          isPublishing: Boolean(result.isPublishing),
+          isComplete: Boolean(result.isComplete),
+          isFailed: Boolean(result.isFailed),
+          progress: typeof result.progress === 'number' ? result.progress : null,
+          needAction: Boolean(result.needAction),
+          actionDescription: result.actionDescription || null,
+          statusDescription: result.statusDescription || '未知状态',
+        };
+      }
+
+      return {
+        isPublishing: false,
+        isComplete: false,
+        isFailed: false,
+        progress: null,
+        needAction: false,
+        actionDescription: null,
+        statusDescription: response,
+      };
+    } catch (error) {
+      logger.error('analyzePublishProgress error:', error);
+      return {
+        isPublishing: false,
+        isComplete: false,
+        isFailed: false,
+        progress: null,
+        needAction: false,
+        actionDescription: null,
+        statusDescription: '分析失败',
+      };
+    }
+  }
+
+  /**
    * 通过 HTML 分析页面并返回操作指导
    * @param html 页面 HTML 内容
    * @param platform 平台名称
@@ -1114,10 +1296,12 @@ ${simplifiedHtml}
   "explanation": "操作说明和当前页面状态分析"
 }
 
-注意:
-- CSS选择器必须能够唯一定位到目标元素
-- 如果有多个相似元素,使用更具体的选择器(如 :first-child, :nth-child(n))
+【重要】CSS选择器格式要求:
+- 必须使用标准CSS选择器语法,不要使用jQuery语法
+- 不要使用 :contains()(这是jQuery语法,不是标准CSS)
+- 如果需要按文本匹配,使用 :has-text("文本") 格式,例如:button:has-text("发布")
 - 优先使用 id 选择器,其次是 data-* 属性,再次是唯一的 class
+- 如果有多个相似元素,使用更具体的选择器(如 :first-child, :nth-child(n))
 - 如果页面已完成目标(如已登录成功),返回 hasAction: false`;
 
     try {

+ 350 - 43
server/src/automation/platforms/baijiahao.ts

@@ -11,6 +11,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
+import { aiService } from '../../ai/index.js';
 
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
@@ -24,7 +25,7 @@ const SERVER_ROOT = path.resolve(process.cwd());
 export class BaijiahaoAdapter extends BasePlatformAdapter {
   readonly platform: PlatformType = 'baijiahao';
   readonly loginUrl = 'https://baijiahao.baidu.com/';
-  readonly publishUrl = 'https://baijiahao.baidu.com/builder/rc/edit?type=video';
+  readonly publishUrl = 'https://baijiahao.baidu.com/builder/rc/edit?type=videoV2&is_from_cms=1';
   
   protected getCookieDomain(): string {
     return '.baidu.com';
@@ -143,6 +144,66 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
     }
   }
   
+  /**
+   * 关闭页面上可能存在的弹窗对话框
+   */
+  private async closeModalDialogs(): Promise<boolean> {
+    if (!this.page) return false;
+    
+    let closedAny = false;
+    
+    try {
+      const modalSelectors = [
+        // 百家号常见弹窗关闭按钮
+        '.Dialog-close',
+        '.modal-close',
+        '[class*="dialog"] [class*="close"]',
+        '[class*="modal"] [class*="close"]',
+        '[role="dialog"] button[aria-label="close"]',
+        '.ant-modal-close',
+        'button:has-text("关闭")',
+        'button:has-text("取消")',
+        'button:has-text("我知道了")',
+        'button:has-text("暂不")',
+        '.close-btn',
+      ];
+      
+      for (const selector of modalSelectors) {
+        try {
+          const closeBtn = this.page.locator(selector).first();
+          if (await closeBtn.count() > 0 && await closeBtn.isVisible()) {
+            logger.info(`[Baijiahao] Found modal close button: ${selector}`);
+            await closeBtn.click({ timeout: 2000 });
+            closedAny = true;
+            await this.page.waitForTimeout(500);
+          }
+        } catch (e) {
+          // 忽略错误,继续尝试下一个选择器
+        }
+      }
+      
+      // 尝试按 ESC 键关闭弹窗
+      if (!closedAny) {
+        const hasModal = await this.page.locator('[class*="dialog"], [class*="modal"], [role="dialog"]').count();
+        if (hasModal > 0) {
+          logger.info('[Baijiahao] Trying ESC key to close modal...');
+          await this.page.keyboard.press('Escape');
+          await this.page.waitForTimeout(500);
+          closedAny = true;
+        }
+      }
+      
+      if (closedAny) {
+        logger.info('[Baijiahao] Successfully closed modal dialog');
+      }
+      
+    } catch (error) {
+      logger.warn('[Baijiahao] Error closing modal:', error);
+    }
+    
+    return closedAny;
+  }
+
   async getAccountInfo(cookies: string): Promise<AccountProfile> {
     try {
       await this.initBrowser({ headless: true });
@@ -301,6 +362,9 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
 
       await this.page.waitForTimeout(3000);
 
+      // 先关闭可能存在的弹窗
+      await this.closeModalDialogs();
+
       // 检查是否需要登录
       const currentUrl = this.page.url();
       if (currentUrl.includes('login') || currentUrl.includes('passport')) {
@@ -311,26 +375,174 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
         };
       }
 
+      // 再次关闭可能的弹窗(登录后可能出现活动弹窗)
+      await this.closeModalDialogs();
+
       onProgress?.(10, '正在上传视频...');
 
-      // 上传视频
-      const fileInput = this.page.locator('input[type="file"]').first();
-      if (await fileInput.count() > 0) {
-        await fileInput.setInputFiles(params.videoPath);
-      } else {
-        // 尝试其他上传方式
-        const uploadArea = this.page.locator('[class*="upload"], [class*="video-upload"]').first();
-        if (await uploadArea.count() > 0) {
-          const [fileChooser] = await Promise.all([
-            this.page.waitForEvent('filechooser', { timeout: 10000 }),
-            uploadArea.click(),
-          ]);
-          await fileChooser.setFiles(params.videoPath);
-        } else {
-          throw new Error('未找到上传入口');
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
+      let uploadTriggered = false;
+
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Baijiahao Publish] Using AI to find upload entry...');
+      try {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'baijiahao', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Baijiahao Publish] AI analysis result:`, guide);
+
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Baijiahao Publish] AI suggested selector: ${guide.targetSelector}`);
+          try {
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Baijiahao Publish] Upload triggered via AI selector');
+          } catch (e) {
+            logger.warn(`[Baijiahao Publish] AI selector click failed: ${e}`);
+          }
+        }
+      } catch (e) {
+        logger.warn(`[Baijiahao Publish] AI analysis failed: ${e}`);
+      }
+
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
+      if (!uploadTriggered) {
+        logger.info('[Baijiahao Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // 百家号常见上传区域选择器 - 虚线框拖拽上传区域
+          '[class*="drag"]',
+          '[class*="drop"]',
+          '[class*="upload-area"]',
+          '[class*="upload-zone"]',
+          '[class*="upload-wrapper"]',
+          '[class*="upload-box"]',
+          '[class*="upload-btn"]',
+          '[class*="upload-video"]',
+          '[class*="video-upload"]',
+          '[class*="drag-upload"]',
+          '.upload-container',
+          '.video-uploader',
+          'div[class*="uploader"]',
+          // 匹配包含"点击上传"文字的区域
+          'div:has-text("点击上传")',
+          'div:has-text("拖动入此区域")',
+          'span:has-text("点击上传")',
+          // 带虚线边框的容器(通常是拖拽上传区域)
+          '[style*="dashed"]',
+          '[class*="dashed"]',
+          '[class*="border-dashed"]',
+          // 其他常见选择器
+          'button:has-text("上传")',
+          '[class*="add-btn"]',
+          '.bjh-upload',
+          '[class*="file-select"]',
+          // 通用的上传触发器
+          '[class*="trigger"]',
+          '[class*="picker"]',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Baijiahao Publish] Trying selector: ${selector}`);
+              const [fileChooser] = await Promise.all([
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
+              ]);
+              await fileChooser.setFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info(`[Baijiahao Publish] Upload triggered via selector: ${selector}`);
+            }
+          } catch (e) {
+            // 继续尝试下一个选择器
+          }
         }
       }
 
+      // 方法3: 直接设置 file input
+      if (!uploadTriggered) {
+        logger.info('[Baijiahao Publish] Trying file input method...');
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Baijiahao Publish] Found ${fileInputs.length} file inputs`);
+        for (const fileInput of fileInputs) {
+          try {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
+              await fileInput.setInputFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info('[Baijiahao Publish] Upload triggered via file input');
+              break;
+            }
+          } catch (e) {
+            logger.warn(`[Baijiahao Publish] File input method failed: ${e}`);
+          }
+        }
+      }
+
+      // 方法4: 如果AI给出了坐标,尝试基于坐标点击
+      if (!uploadTriggered) {
+        logger.info('[Baijiahao Publish] Trying AI position-based click...');
+        try {
+          const screenshot = await this.screenshotBase64();
+          const guide = await aiService.getPageOperationGuide(screenshot, 'baijiahao', '请找到页面中央的虚线框上传区域(有"点击上传或将文件拖动入此区域"文字的区域),返回该区域的中心坐标');
+          logger.info(`[Baijiahao Publish] AI position analysis:`, guide);
+          
+          if (guide.hasAction && guide.targetPosition) {
+            const { x, y } = guide.targetPosition;
+            logger.info(`[Baijiahao Publish] Clicking at position: ${x}, ${y}`);
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.mouse.click(x, y),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Baijiahao Publish] Upload triggered via position click');
+          }
+        } catch (e) {
+          logger.warn(`[Baijiahao Publish] Position-based click failed: ${e}`);
+        }
+      }
+
+      // 方法5: 点击页面中央区域(百家号上传区域通常在中央)
+      if (!uploadTriggered) {
+        logger.info('[Baijiahao Publish] Trying center area click...');
+        try {
+          const viewport = this.page.viewportSize();
+          if (viewport) {
+            // 百家号的上传区域大约在页面中央偏上的位置
+            const centerX = viewport.width / 2;
+            const centerY = viewport.height * 0.35; // 上传区域通常在页面上半部分
+            logger.info(`[Baijiahao Publish] Clicking center area: ${centerX}, ${centerY}`);
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.mouse.click(centerX, centerY),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Baijiahao Publish] Upload triggered via center click');
+          }
+        } catch (e) {
+          logger.warn(`[Baijiahao Publish] Center click failed: ${e}`);
+        }
+      }
+
+      if (!uploadTriggered) {
+        // 截图调试
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/baijiahao_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Baijiahao Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
+        throw new Error('未找到上传入口');
+      }
+
       // 等待上传完成
       onProgress?.(30, '视频上传中...');
       await this.page.waitForTimeout(5000);
@@ -338,24 +550,62 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
       // 等待视频处理
       const maxWaitTime = 300000; // 5分钟
       const startTime = Date.now();
+      let lastAiCheckTime = 0;
+      const aiCheckInterval = 10000; // 每10秒使用AI检测一次
 
       while (Date.now() - startTime < maxWaitTime) {
-        // 检查上传进度
+        // 检查上传进度(通过DOM)
+        let progressDetected = false;
         const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
         if (progressText) {
           const match = progressText.match(/(\d+)%/);
           if (match) {
             const progress = parseInt(match[1]);
             onProgress?.(30 + Math.floor(progress * 0.3), `视频上传中: ${progress}%`);
+            progressDetected = true;
+            if (progress >= 100) {
+              logger.info('[Baijiahao Publish] Upload progress reached 100%');
+              break;
+            }
           }
         }
 
         // 检查是否上传完成
         const uploadSuccess = await this.page.locator('[class*="success"], [class*="complete"]').count();
         if (uploadSuccess > 0) {
+          logger.info('[Baijiahao Publish] Upload success indicator found');
           break;
         }
 
+        // 使用AI检测上传进度(每隔一段时间检测一次)
+        if (!progressDetected && Date.now() - lastAiCheckTime > aiCheckInterval) {
+          lastAiCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'baijiahao');
+            logger.info(`[Baijiahao Publish] AI upload status:`, uploadStatus);
+            
+            if (uploadStatus.isComplete) {
+              logger.info('[Baijiahao Publish] AI detected upload complete');
+              break;
+            }
+            
+            if (uploadStatus.isFailed) {
+              throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
+            }
+            
+            if (uploadStatus.progress !== null) {
+              onProgress?.(30 + Math.floor(uploadStatus.progress * 0.3), `视频上传中: ${uploadStatus.progress}%`);
+              if (uploadStatus.progress >= 100) {
+                logger.info('[Baijiahao Publish] AI detected progress 100%');
+                break;
+              }
+            }
+          } catch (aiError) {
+            logger.warn('[Baijiahao Publish] AI progress check failed:', aiError);
+          }
+        }
+
         await this.page.waitForTimeout(2000);
       }
 
@@ -387,47 +637,104 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
 
       // 等待发布结果
       onProgress?.(90, '等待发布完成...');
-      await this.page.waitForTimeout(5000);
+      const publishMaxWait = 120000; // 2分钟
+      const publishStartTime = Date.now();
+      let lastProgressCheckTime = 0;
+      const progressCheckInterval = 5000; // 每5秒检测一次发布进度
 
-      // AI 辅助检测发布状态
-      const aiStatus = await this.aiAnalyzePublishStatus();
-      if (aiStatus) {
-        if (aiStatus.status === 'success') {
+      while (Date.now() - publishStartTime < publishMaxWait) {
+        await this.page.waitForTimeout(3000);
+        
+        // 检查是否跳转到内容管理页面
+        const currentUrl = this.page.url();
+        if (currentUrl.includes('/content') || currentUrl.includes('/rc/home')) {
           onProgress?.(100, '发布成功!');
           await this.closeBrowser();
           return {
             success: true,
-            videoUrl: this.page.url(),
+            videoUrl: currentUrl,
           };
         }
 
-        if (aiStatus.status === 'need_captcha' && onCaptchaRequired) {
-          const imageBase64 = await this.screenshotBase64();
+        // 检查发布进度条(DOM方式)
+        const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
+        if (publishProgressText) {
+          const match = publishProgressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(90 + Math.floor(progress * 0.1), `发布中: ${progress}%`);
+            logger.info(`[Baijiahao Publish] Publish progress: ${progress}%`);
+          }
+        }
+
+        // AI检测发布进度(定期检测)
+        if (Date.now() - lastProgressCheckTime > progressCheckInterval) {
+          lastProgressCheckTime = Date.now();
           try {
-            await onCaptchaRequired({
-              taskId: `baijiahao_captcha_${Date.now()}`,
-              type: 'image',
-              imageBase64,
-            });
-          } catch {
-            logger.error('[Baijiahao] Captcha handling failed');
+            const screenshot = await this.screenshotBase64();
+            const publishStatus = await aiService.analyzePublishProgress(screenshot, 'baijiahao');
+            logger.info(`[Baijiahao Publish] AI publish status:`, publishStatus);
+            
+            if (publishStatus.isComplete) {
+              logger.info('[Baijiahao Publish] AI detected publish complete');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+            
+            if (publishStatus.isFailed) {
+              throw new Error(`发布失败: ${publishStatus.statusDescription}`);
+            }
+            
+            if (publishStatus.progress !== null) {
+              onProgress?.(90 + Math.floor(publishStatus.progress * 0.1), `发布中: ${publishStatus.progress}%`);
+            }
+            
+            // 处理需要用户操作的情况(如验证码)
+            if (publishStatus.needAction && onCaptchaRequired) {
+              logger.info(`[Baijiahao Publish] Need action: ${publishStatus.actionDescription}`);
+              const imageBase64 = await this.screenshotBase64();
+              try {
+                await onCaptchaRequired({
+                  taskId: `baijiahao_captcha_${Date.now()}`,
+                  type: 'image',
+                  imageBase64,
+                });
+              } catch {
+                logger.error('[Baijiahao] Captcha handling failed');
+              }
+            }
+            
+            if (publishStatus.isPublishing) {
+              logger.info(`[Baijiahao Publish] Still publishing: ${publishStatus.statusDescription}`);
+            }
+          } catch (aiError) {
+            logger.warn('[Baijiahao Publish] AI publish progress check failed:', aiError);
           }
         }
 
-        if (aiStatus.status === 'failed') {
-          throw new Error(aiStatus.errorMessage || '发布失败');
+        // 检查错误提示
+        const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
+          throw new Error(`发布失败: ${errorHint}`);
         }
       }
 
-      // 检查是否跳转到内容管理页面
-      const finalUrl = this.page.url();
-      if (finalUrl.includes('/content') || finalUrl.includes('/rc/home')) {
-        onProgress?.(100, '发布成功!');
-        await this.closeBrowser();
-        return {
-          success: true,
-          videoUrl: finalUrl,
-        };
+      // 最后再检查一次AI状态
+      const aiStatus = await this.aiAnalyzePublishStatus();
+      if (aiStatus) {
+        if (aiStatus.status === 'success') {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        if (aiStatus.status === 'failed') {
+          throw new Error(aiStatus.errorMessage || '发布失败');
+        }
       }
 
       // 超时

+ 57 - 5
server/src/automation/platforms/base.ts

@@ -328,6 +328,31 @@ export abstract class BasePlatformAdapter {
   }
 
   /**
+   * 将AI返回的选择器转换为Playwright兼容的格式
+   * @param selector 原始选择器
+   * @returns 转换后的选择器
+   */
+  private convertToPlaywrightSelector(selector: string): string {
+    let converted = selector;
+    
+    // 将 :contains('text') 或 :contains("text") 转换为 :has-text("text")
+    converted = converted.replace(/:contains\(['"]([^'"]+)['"]\)/g, ':has-text("$1")');
+    converted = converted.replace(/:contains\(([^)]+)\)/g, ':has-text("$1")');
+    
+    // 移除不支持的伪类选择器
+    // 如果选择器包含不支持的语法,尝试简化
+    if (converted.includes(':contains')) {
+      // 如果还有 :contains,提取文本用于 text= 选择器
+      const match = converted.match(/:contains\(['"]?([^'")\]]+)['"]?\)/);
+      if (match) {
+        return `text="${match[1]}"`;
+      }
+    }
+    
+    return converted;
+  }
+
+  /**
    * AI 辅助执行操作
    * @param guide 操作指导
    * @returns 是否执行成功
@@ -339,16 +364,43 @@ export abstract class BasePlatformAdapter {
       switch (guide.actionType) {
         case 'click':
           if (guide.targetSelector) {
-            logger.info(`[AI Publish] Clicking: ${guide.targetSelector}`);
-            await this.page.click(guide.targetSelector, { timeout: 10000 });
-            return true;
+            // 转换选择器为Playwright兼容格式
+            const selector = this.convertToPlaywrightSelector(guide.targetSelector);
+            logger.info(`[AI Publish] Clicking: ${selector} (original: ${guide.targetSelector})`);
+            
+            try {
+              await this.page.click(selector, { timeout: 10000 });
+              return true;
+            } catch (selectorError) {
+              // 如果选择器失败,尝试使用位置点击
+              if (guide.targetPosition) {
+                logger.info(`[AI Publish] Selector failed, trying position click: ${guide.targetPosition.x}, ${guide.targetPosition.y}`);
+                await this.page.mouse.click(guide.targetPosition.x, guide.targetPosition.y);
+                return true;
+              }
+              
+              // 尝试使用文本匹配
+              if (guide.targetDescription) {
+                const textSelector = `text="${guide.targetDescription}"`;
+                logger.info(`[AI Publish] Trying text selector: ${textSelector}`);
+                try {
+                  await this.page.click(textSelector, { timeout: 5000 });
+                  return true;
+                } catch {
+                  // 继续抛出原始错误
+                }
+              }
+              
+              throw selectorError;
+            }
           }
           break;
 
         case 'input':
           if (guide.targetSelector && guide.inputText) {
-            logger.info(`[AI Publish] Inputting to: ${guide.targetSelector}`);
-            await this.page.fill(guide.targetSelector, guide.inputText);
+            const selector = this.convertToPlaywrightSelector(guide.targetSelector);
+            logger.info(`[AI Publish] Inputting to: ${selector}`);
+            await this.page.fill(selector, guide.inputText);
             return true;
           }
           break;

+ 99 - 4
server/src/automation/platforms/bilibili.ts

@@ -11,6 +11,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
+import { aiService } from '../../ai/index.js';
 
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
@@ -261,11 +262,105 @@ export class BilibiliAdapter extends BasePlatformAdapter {
 
       onProgress?.(10, '正在上传视频...');
 
-      // B站上传逻辑
-      const fileInput = await this.page.$('input[type="file"]');
-      if (!fileInput) throw new Error('未找到文件上传入口');
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
+      let uploadTriggered = false;
 
-      await fileInput.setInputFiles(params.videoPath);
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Bilibili Publish] Using AI to find upload entry...');
+      try {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'bilibili', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Bilibili Publish] AI analysis result:`, guide);
+
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Bilibili Publish] AI suggested selector: ${guide.targetSelector}`);
+          try {
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Bilibili Publish] Upload triggered via AI selector');
+          } catch (e) {
+            logger.warn(`[Bilibili Publish] AI selector click failed: ${e}`);
+          }
+        }
+      } catch (e) {
+        logger.warn(`[Bilibili Publish] AI analysis failed: ${e}`);
+      }
+
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
+      if (!uploadTriggered) {
+        logger.info('[Bilibili Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // B站常见上传区域选择器
+          '[class*="upload-area"]',
+          '[class*="upload-btn"]',
+          '[class*="bcc-upload"]',
+          '[class*="video-upload"]',
+          '[class*="upload-container"]',
+          '.upload-trigger',
+          '.video-uploader',
+          'div[class*="uploader"]',
+          'button:has-text("上传")',
+          'div:has-text("上传视频"):not(:has(div))',
+          'span:has-text("点击上传")',
+          '[class*="upload-select"]',
+          '.bili-upload',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Bilibili Publish] Trying selector: ${selector}`);
+              const [fileChooser] = await Promise.all([
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
+              ]);
+              await fileChooser.setFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info(`[Bilibili Publish] Upload triggered via selector: ${selector}`);
+            }
+          } catch (e) {
+            // 继续尝试下一个选择器
+          }
+        }
+      }
+
+      // 方法3: 直接设置 file input
+      if (!uploadTriggered) {
+        logger.info('[Bilibili Publish] Trying file input method...');
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Bilibili Publish] Found ${fileInputs.length} file inputs`);
+        for (const fileInput of fileInputs) {
+          try {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
+              await fileInput.setInputFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info('[Bilibili Publish] Upload triggered via file input');
+              break;
+            }
+          } catch (e) {
+            logger.warn(`[Bilibili Publish] File input method failed: ${e}`);
+          }
+        }
+      }
+
+      if (!uploadTriggered) {
+        // 截图调试
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/bilibili_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Bilibili Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
+        throw new Error('未找到上传入口');
+      }
 
       // 等待上传完成
       onProgress?.(20, '视频上传中...');

+ 174 - 64
server/src/automation/platforms/douyin.ts

@@ -1088,90 +1088,103 @@ export class DouyinAdapter extends BasePlatformAdapter {
 
       onProgress?.(10, '正在选择视频文件...');
 
-      // 参考 matrix: 点击上传区域触发文件选择
-      // 选择器: div.container-drag-info-Tl0RGH (哈希值可能变化)
-      const uploadDivSelectors = [
-        'div[class*="container-drag-info"]',
-        'div[class*="container-drag"]',
-        'div[class*="upload-drag"]',
-        'div[class*="drag-info"]',
-        'div[class*="upload-btn"]',
-        'div[class*="drag-area"]',
-        '[class*="upload"] [class*="drag"]',
-        'div[class*="upload-area"]',
-      ];
-
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
       let uploadTriggered = false;
 
-      // 方法1: 使用 file chooser 方式(最可靠)
-      for (const selector of uploadDivSelectors) {
-        if (uploadTriggered) break;
-        try {
-          const uploadDiv = this.page.locator(selector).first();
-          const count = await uploadDiv.count();
-          logger.info(`[Douyin Publish] Checking selector ${selector}: count=${count}`);
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Douyin Publish] Using AI to find upload entry...');
+      try {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'douyin', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Douyin Publish] AI analysis result:`, guide);
 
-          if (count > 0) {
-            logger.info(`[Douyin Publish] Trying file chooser with selector: ${selector}`);
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Douyin Publish] AI suggested selector: ${guide.targetSelector}`);
+          try {
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Douyin Publish] Upload triggered via AI selector');
+          } catch (e) {
+            logger.warn(`[Douyin Publish] AI selector click failed: ${e}`);
+          }
+        }
+      } catch (e) {
+        logger.warn(`[Douyin Publish] AI analysis failed: ${e}`);
+      }
 
-            try {
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
+      if (!uploadTriggered) {
+        logger.info('[Douyin Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // 抖音常见上传区域选择器
+          'div[class*="container-drag-info"]',
+          'div[class*="container-drag"]',
+          'div[class*="upload-drag"]',
+          'div[class*="drag-info"]',
+          'div[class*="upload-btn"]',
+          'div[class*="drag-area"]',
+          '[class*="upload"] [class*="drag"]',
+          'div[class*="upload-area"]',
+          '.upload-trigger',
+          'button:has-text("上传")',
+          'div:has-text("上传视频"):not(:has(div))',
+          'span:has-text("点击上传")',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Douyin Publish] Trying selector: ${selector}`);
               const [fileChooser] = await Promise.all([
-                this.page.waitForEvent('filechooser', { timeout: 10000 }),
-                uploadDiv.click(),
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
               ]);
-
               await fileChooser.setFiles(params.videoPath);
               uploadTriggered = true;
-              logger.info(`[Douyin Publish] File selected via file chooser`);
-            } catch (fcError) {
-              logger.warn(`[Douyin Publish] File chooser failed for ${selector}:`, fcError);
+              logger.info(`[Douyin Publish] Upload triggered via selector: ${selector}`);
             }
+          } catch (e) {
+            // 继续尝试下一个选择器
           }
-        } catch (e) {
-          logger.warn(`[Douyin Publish] Failed with selector ${selector}:`, e);
         }
       }
 
-      // 方法2: 直接设置 file input(备用)
+      // 方法3: 直接设置 file input
       if (!uploadTriggered) {
-        logger.info('[Douyin Publish] Trying direct input method...');
-        try {
-          const fileInputs = await this.page.$$('input[type="file"]');
-          logger.info(`[Douyin Publish] Found ${fileInputs.length} file inputs`);
-
-          for (const fileInput of fileInputs) {
-            try {
+        logger.info('[Douyin Publish] Trying file input method...');
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Douyin Publish] Found ${fileInputs.length} file inputs`);
+        for (const fileInput of fileInputs) {
+          try {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
               await fileInput.setInputFiles(params.videoPath);
               uploadTriggered = true;
-              logger.info('[Douyin Publish] File set via input element');
+              logger.info('[Douyin Publish] Upload triggered via file input');
               break;
-            } catch (inputError) {
-              logger.warn('[Douyin Publish] Failed to set file on input:', inputError);
             }
+          } catch (e) {
+            logger.warn(`[Douyin Publish] File input method failed: ${e}`);
           }
-        } catch (e) {
-          logger.warn('[Douyin Publish] Direct input method failed:', e);
         }
       }
 
-      // 方法3: 使用 locator('input[type="file"]').setInputFiles (最后尝试)
       if (!uploadTriggered) {
-        logger.info('[Douyin Publish] Trying locator input method...');
+        // 截图调试
         try {
-          await this.page.locator('input[type="file"]').first().setInputFiles(params.videoPath);
-          uploadTriggered = true;
-          logger.info('[Douyin Publish] File set via locator input');
-        } catch (e) {
-          logger.warn('[Douyin Publish] Locator input method failed:', e);
-        }
-      }
-
-      if (!uploadTriggered) {
-        // 保存截图以便调试
-        const screenshotPath = `douyin_upload_failed_${Date.now()}.png`;
-        await this.page.screenshot({ path: screenshotPath, fullPage: true });
-        logger.error(`[Douyin Publish] Screenshot saved to ${screenshotPath}`);
-        throw new Error(`无法触发文件上传(截图: ${screenshotPath})`);
+          if (this.page) {
+            const screenshotPath = `uploads/debug/douyin_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Douyin Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
+        throw new Error('未找到上传入口');
       }
 
       onProgress?.(15, '视频上传中,等待跳转到发布页面...');
@@ -1180,6 +1193,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
       // URL: https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page
       const maxWaitTime = 180000; // 3分钟
       const startTime = Date.now();
+      let lastAiCheckTime = 0;
+      const aiCheckInterval = 10000; // 每10秒使用AI检测一次
 
       while (Date.now() - startTime < maxWaitTime) {
         await this.page.waitForTimeout(2000);
@@ -1190,13 +1205,40 @@ export class DouyinAdapter extends BasePlatformAdapter {
           break;
         }
 
-        // 检查上传进度
+        // 检查上传进度(通过DOM)
+        let progressDetected = false;
         const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
         if (progressText) {
           const match = progressText.match(/(\d+)%/);
           if (match) {
             const progress = parseInt(match[1]);
             onProgress?.(15 + Math.floor(progress * 0.3), `视频上传中: ${progress}%`);
+            progressDetected = true;
+          }
+        }
+
+        // 使用AI检测上传进度(每隔一段时间检测一次)
+        if (!progressDetected && Date.now() - lastAiCheckTime > aiCheckInterval) {
+          lastAiCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'douyin');
+            logger.info(`[Douyin Publish] AI upload status:`, uploadStatus);
+            
+            if (uploadStatus.isComplete) {
+              logger.info('[Douyin Publish] AI detected upload complete');
+              // 继续等待页面跳转
+            }
+            
+            if (uploadStatus.isFailed) {
+              throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
+            }
+            
+            if (uploadStatus.progress !== null) {
+              onProgress?.(15 + Math.floor(uploadStatus.progress * 0.3), `视频上传中: ${uploadStatus.progress}%`);
+            }
+          } catch (aiError) {
+            logger.warn('[Douyin Publish] AI progress check failed:', aiError);
           }
         }
 
@@ -1434,6 +1476,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
 
       // AI 检测计数器,避免过于频繁
       let aiCheckCounter = 0;
+      let lastProgressCheckTime = 0;
+      const progressCheckInterval = 5000; // 每5秒检测一次发布进度
 
       while (Date.now() - publishStartTime < publishMaxWait) {
         await this.page.waitForTimeout(3000);
@@ -1458,11 +1502,35 @@ export class DouyinAdapter extends BasePlatformAdapter {
           };
         }
 
-        // 检查是否跳转到管理页面 - 这是最可靠的成功标志
+        // 检查是否跳转到管理页面
         if (currentUrl.includes('/content/manage')) {
-          logger.info('[Douyin Publish] Publish success! Redirected to manage page');
+          logger.info('[Douyin Publish] Redirected to manage page, checking for background upload...');
+          
+          // 检查是否有后台上传进度条(抖音特有:右下角的上传进度框)
+          const uploadProgressBox = await this.page.locator('[class*="upload-progress"], [class*="uploading"], div:has-text("作品上传中"), div:has-text("请勿关闭页面")').count().catch(() => 0);
+          const uploadProgressText = await this.page.locator('div:has-text("上传中"), div:has-text("上传完成后")').first().textContent().catch(() => '');
+          
+          // 如果有后台上传进度,继续等待
+          if (uploadProgressBox > 0 || uploadProgressText.includes('上传')) {
+            logger.info(`[Douyin Publish] Background upload in progress: ${uploadProgressText}`);
+            const match = uploadProgressText.match(/(\d+)%/);
+            if (match) {
+              const progress = parseInt(match[1]);
+              onProgress?.(85 + Math.floor(progress * 0.15), `后台上传中: ${progress}%`);
+              if (progress >= 100) {
+                logger.info('[Douyin Publish] Background upload complete!');
+                onProgress?.(100, '发布成功!');
+                await this.closeBrowser();
+                return { success: true, videoUrl: currentUrl };
+              }
+            }
+            // 继续等待上传完成
+            continue;
+          }
+          
+          // 没有后台上传进度,发布完成
+          logger.info('[Douyin Publish] No background upload, publish complete!');
           onProgress?.(100, '发布成功!');
-
           await this.closeBrowser();
           return {
             success: true,
@@ -1470,6 +1538,48 @@ export class DouyinAdapter extends BasePlatformAdapter {
           };
         }
 
+        // 检查发布进度条(DOM方式)
+        const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"], [class*="publishing"]').first().textContent().catch(() => '');
+        if (publishProgressText) {
+          const match = publishProgressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(85 + Math.floor(progress * 0.15), `发布中: ${progress}%`);
+            logger.info(`[Douyin Publish] Publish progress: ${progress}%`);
+          }
+        }
+
+        // AI检测发布进度(定期检测)
+        if (Date.now() - lastProgressCheckTime > progressCheckInterval) {
+          lastProgressCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const publishStatus = await aiService.analyzePublishProgress(screenshot, 'douyin');
+            logger.info(`[Douyin Publish] AI publish progress status:`, publishStatus);
+            
+            if (publishStatus.isComplete) {
+              logger.info('[Douyin Publish] AI detected publish complete');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+            
+            if (publishStatus.isFailed) {
+              throw new Error(`发布失败: ${publishStatus.statusDescription}`);
+            }
+            
+            if (publishStatus.progress !== null) {
+              onProgress?.(85 + Math.floor(publishStatus.progress * 0.15), `发布中: ${publishStatus.progress}%`);
+            }
+            
+            if (publishStatus.isPublishing) {
+              logger.info(`[Douyin Publish] Still publishing: ${publishStatus.statusDescription}`);
+            }
+          } catch (aiError) {
+            logger.warn('[Douyin Publish] AI publish progress check failed:', aiError);
+          }
+        }
+
         // 检查是否有成功提示弹窗(Toast/Modal)
         // 使用更精确的选择器,避免匹配按钮文字
         const successToast = await this.page.locator('.semi-toast-content:has-text("发布成功"), .semi-modal-body:has-text("发布成功"), [class*="toast"]:has-text("发布成功"), [class*="message"]:has-text("发布成功")').count().catch(() => 0);

+ 99 - 3
server/src/automation/platforms/kuaishou.ts

@@ -10,6 +10,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
+import { aiService } from '../../ai/index.js';
 
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
@@ -250,10 +251,105 @@ export class KuaishouAdapter extends BasePlatformAdapter {
 
       onProgress?.(10, '正在上传视频...');
 
-      const fileInput = await this.page.$('input[type="file"]');
-      if (!fileInput) throw new Error('未找到文件上传入口');
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
+      let uploadTriggered = false;
 
-      await fileInput.setInputFiles(params.videoPath);
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Kuaishou Publish] Using AI to find upload entry...');
+      try {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'kuaishou', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Kuaishou Publish] AI analysis result:`, guide);
+
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Kuaishou Publish] AI suggested selector: ${guide.targetSelector}`);
+          try {
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Kuaishou Publish] Upload triggered via AI selector');
+          } catch (e) {
+            logger.warn(`[Kuaishou Publish] AI selector click failed: ${e}`);
+          }
+        }
+      } catch (e) {
+        logger.warn(`[Kuaishou Publish] AI analysis failed: ${e}`);
+      }
+
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
+      if (!uploadTriggered) {
+        logger.info('[Kuaishou Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // 快手常见上传区域选择器
+          '[class*="upload-area"]',
+          '[class*="upload-btn"]',
+          '[class*="upload-video"]',
+          '[class*="video-upload"]',
+          '[class*="drag-upload"]',
+          '.upload-trigger',
+          '.video-uploader',
+          'div[class*="uploader"]',
+          'button:has-text("上传")',
+          'div:has-text("上传视频"):not(:has(div))',
+          'span:has-text("点击上传")',
+          '[class*="add-video"]',
+          '.kwai-upload',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Kuaishou Publish] Trying selector: ${selector}`);
+              const [fileChooser] = await Promise.all([
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
+              ]);
+              await fileChooser.setFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info(`[Kuaishou Publish] Upload triggered via selector: ${selector}`);
+            }
+          } catch (e) {
+            // 继续尝试下一个选择器
+          }
+        }
+      }
+
+      // 方法3: 直接设置 file input
+      if (!uploadTriggered) {
+        logger.info('[Kuaishou Publish] Trying file input method...');
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Kuaishou Publish] Found ${fileInputs.length} file inputs`);
+        for (const fileInput of fileInputs) {
+          try {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
+              await fileInput.setInputFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info('[Kuaishou Publish] Upload triggered via file input');
+              break;
+            }
+          } catch (e) {
+            logger.warn(`[Kuaishou Publish] File input method failed: ${e}`);
+          }
+        }
+      }
+
+      if (!uploadTriggered) {
+        // 截图调试
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/kuaishou_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Kuaishou Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
+        throw new Error('未找到上传入口');
+      }
 
       // 等待上传完成
       onProgress?.(20, '视频上传中...');

+ 397 - 19
server/src/automation/platforms/weixin.ts

@@ -11,6 +11,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
+import { aiService } from '../../ai/index.js';
 
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
@@ -111,6 +112,67 @@ export class WeixinAdapter extends BasePlatformAdapter {
     }
   }
   
+  /**
+   * 关闭页面上可能存在的弹窗对话框
+   */
+  private async closeModalDialogs(): Promise<boolean> {
+    if (!this.page) return false;
+    
+    let closedAny = false;
+    
+    try {
+      const modalSelectors = [
+        // 微信视频号常见弹窗关闭按钮
+        '.weui-desktop-dialog__close',
+        '.weui-desktop-btn__default:has-text("取消")',
+        '.weui-desktop-btn__default:has-text("关闭")',
+        '.weui-desktop-dialog-close',
+        '[class*="dialog"] [class*="close"]',
+        '[class*="modal"] [class*="close"]',
+        '[role="dialog"] button[aria-label="close"]',
+        'button:has-text("关闭")',
+        'button:has-text("取消")',
+        'button:has-text("我知道了")',
+        '.close-btn',
+        '.icon-close',
+      ];
+      
+      for (const selector of modalSelectors) {
+        try {
+          const closeBtn = this.page.locator(selector).first();
+          if (await closeBtn.count() > 0 && await closeBtn.isVisible()) {
+            logger.info(`[Weixin] Found modal close button: ${selector}`);
+            await closeBtn.click({ timeout: 2000 });
+            closedAny = true;
+            await this.page.waitForTimeout(500);
+          }
+        } catch (e) {
+          // 忽略错误,继续尝试下一个选择器
+        }
+      }
+      
+      // 尝试按 ESC 键关闭弹窗
+      if (!closedAny) {
+        const hasModal = await this.page.locator('[class*="dialog"], [class*="modal"], [role="dialog"]').count();
+        if (hasModal > 0) {
+          logger.info('[Weixin] Trying ESC key to close modal...');
+          await this.page.keyboard.press('Escape');
+          await this.page.waitForTimeout(500);
+          closedAny = true;
+        }
+      }
+      
+      if (closedAny) {
+        logger.info('[Weixin] Successfully closed modal dialog');
+      }
+      
+    } catch (error) {
+      logger.warn('[Weixin] Error closing modal:', error);
+    }
+    
+    return closedAny;
+  }
+
   async getAccountInfo(cookies: string): Promise<AccountProfile> {
     try {
       await this.initBrowser();
@@ -245,10 +307,18 @@ export class WeixinAdapter extends BasePlatformAdapter {
     const useHeadless = options?.headless ?? true;
 
     try {
+      logger.info('[Weixin Publish] Initializing browser...');
       await this.initBrowser({ headless: useHeadless });
+      
+      if (!this.page) {
+        throw new Error('浏览器初始化失败,page 为 null');
+      }
+      
+      logger.info('[Weixin Publish] Setting cookies...');
       await this.setCookies(cookies);
 
-      if (!this.page) throw new Error('Page not initialized');
+      // 再次检查 page 状态
+      if (!this.page) throw new Error('Page not initialized after setCookies');
 
       onProgress?.(5, '正在打开上传页面...');
 
@@ -259,6 +329,9 @@ export class WeixinAdapter extends BasePlatformAdapter {
 
       await this.page.waitForTimeout(3000);
 
+      // 先关闭可能存在的弹窗
+      await this.closeModalDialogs();
+
       // 检查是否需要登录
       const currentUrl = this.page.url();
       if (currentUrl.includes('login')) {
@@ -269,34 +342,258 @@ export class WeixinAdapter extends BasePlatformAdapter {
         };
       }
 
+      // 再次关闭可能的弹窗
+      await this.closeModalDialogs();
+
+      // 检查是否在发布页面,如果不在则尝试点击"发表视频"按钮
+      const pageUrl = this.page.url();
+      if (!pageUrl.includes('post/create')) {
+        logger.info('[Weixin Publish] Not on publish page, looking for "发表视频" button...');
+        onProgress?.(8, '正在进入发布页面...');
+        
+        // 使用AI寻找发表视频按钮
+        try {
+          const screenshot = await this.screenshotBase64();
+          const guide = await aiService.getPageOperationGuide(screenshot, 'weixin_video', '找到"发表视频"按钮并点击进入视频发布页面');
+          logger.info(`[Weixin Publish] AI guide for publish button:`, guide);
+          
+          if (guide.hasAction && guide.targetSelector) {
+            await this.page.click(guide.targetSelector, { timeout: 5000 });
+            await this.page.waitForTimeout(3000);
+          }
+        } catch (e) {
+          logger.warn(`[Weixin Publish] AI could not find publish button: ${e}`);
+        }
+        
+        // 尝试常见的发表视频按钮选择器
+        const publishBtnSelectors = [
+          'button:has-text("发表视频")',
+          'a:has-text("发表视频")',
+          '[class*="publish"]:has-text("发表")',
+          '[class*="create"]:has-text("发表")',
+          '.post-video-btn',
+        ];
+        
+        for (const selector of publishBtnSelectors) {
+          try {
+            const btn = this.page.locator(selector).first();
+            if (await btn.count() > 0 && await btn.isVisible()) {
+              logger.info(`[Weixin Publish] Found publish button: ${selector}`);
+              await btn.click({ timeout: 5000 });
+              await this.page.waitForTimeout(3000);
+              break;
+            }
+          } catch (e) {
+            // 继续尝试下一个选择器
+          }
+        }
+        
+        // 关闭可能弹出的弹窗
+        await this.closeModalDialogs();
+      }
+
       onProgress?.(10, '正在上传视频...');
 
-      // 上传视频
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
       let uploadTriggered = false;
-      const uploadDiv = this.page.locator('div.upload-content, [class*="upload-area"]').first();
-      if (await uploadDiv.count() > 0) {
+
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Weixin Publish] Using AI to find upload entry...');
+      try {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'weixin_video', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Weixin Publish] AI analysis result:`, guide);
+
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Weixin Publish] AI suggested selector: ${guide.targetSelector}`);
+          try {
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 10000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Weixin Publish] Upload triggered via AI selector');
+          } catch (e) {
+            logger.warn(`[Weixin Publish] AI selector click failed: ${e}`);
+          }
+        }
+      } catch (e) {
+        logger.warn(`[Weixin Publish] AI analysis failed: ${e}`);
+      }
+
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
+      if (!uploadTriggered) {
+        logger.info('[Weixin Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // 微信视频号发布页面 - 带"+"号的上传区域
+          '[class*="add-media"]',
+          '[class*="add-video"]',
+          '[class*="media-add"]',
+          '[class*="video-add"]',
+          '[class*="plus"]',
+          '[class*="add-btn"]',
+          '[class*="add-icon"]',
+          // 视频封面/媒体区域
+          '[class*="video-cover"]',
+          '[class*="media-cover"]',
+          '[class*="cover-upload"]',
+          '[class*="media-upload"]',
+          '[class*="post-media"]',
+          // 通用上传区域
+          '[class*="upload-area"]',
+          '[class*="upload-btn"]',
+          '[class*="upload-video"]',
+          '[class*="video-upload"]',
+          '[class*="upload-content"]',
+          '[class*="upload-zone"]',
+          '[class*="upload-wrap"]',
+          '[class*="uploader"]',
+          // 拖拽区域
+          '[class*="drag"]',
+          '[class*="drop"]',
+          // 匹配上传提示文字
+          'div:has-text("上传时长")',
+          'div:has-text("点击上传")',
+          'div:has-text("拖拽上传")',
+          'div:has-text("MP4")',
+          // 带虚线边框的容器
+          '[style*="dashed"]',
+          '[class*="dashed"]',
+          // 微信视频号特有选择器
+          '[class*="post-cover"]',
+          '.weui-desktop-upload__area',
+          '[class*="finder-upload"]',
+          '[class*="finder-post"]',
+          // 通用触发器
+          '.upload-trigger',
+          '.video-uploader',
+          '.add-video-btn',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Weixin Publish] Trying selector: ${selector}`);
+              const [fileChooser] = await Promise.all([
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
+              ]);
+              await fileChooser.setFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info(`[Weixin Publish] Upload triggered via selector: ${selector}`);
+            }
+          } catch (e) {
+            // 继续尝试下一个选择器
+          }
+        }
+      }
+
+      // 方法3: 直接设置 file input
+      if (!uploadTriggered) {
+        logger.info('[Weixin Publish] Trying file input method...');
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Weixin Publish] Found ${fileInputs.length} file inputs`);
+        for (const fileInput of fileInputs) {
+          try {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
+              await fileInput.setInputFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info('[Weixin Publish] Upload triggered via file input');
+              break;
+            }
+          } catch (e) {
+            logger.warn(`[Weixin Publish] File input method failed: ${e}`);
+          }
+        }
+      }
+
+      // 方法4: 如果AI给出了坐标,尝试基于坐标点击
+      if (!uploadTriggered) {
+        logger.info('[Weixin Publish] Trying AI position-based click...');
         try {
-          const [fileChooser] = await Promise.all([
-            this.page.waitForEvent('filechooser', { timeout: 10000 }),
-            uploadDiv.click(),
-          ]);
-          await fileChooser.setFiles(params.videoPath);
-          uploadTriggered = true;
-        } catch {
-          logger.warn('[Weixin Publish] File chooser method failed');
+          const screenshot = await this.screenshotBase64();
+          const guide = await aiService.getPageOperationGuide(screenshot, 'weixin_video', '请找到页面上的视频上传区域或"发表视频"按钮,返回该元素的中心坐标');
+          logger.info(`[Weixin Publish] AI position analysis:`, guide);
+          
+          if (guide.hasAction && guide.targetPosition) {
+            const { x, y } = guide.targetPosition;
+            logger.info(`[Weixin Publish] Clicking at position: ${x}, ${y}`);
+            
+            // 先尝试普通点击(可能是"发表视频"按钮)
+            await this.page.mouse.click(x, y);
+            await this.page.waitForTimeout(2000);
+            
+            // 检查是否触发了文件选择器或跳转到了发布页
+            const newUrl = this.page.url();
+            if (newUrl.includes('post/create')) {
+              logger.info('[Weixin Publish] Navigated to publish page, retrying upload...');
+              // 重新尝试上传
+              const uploadArea = this.page.locator('[class*="upload"], [class*="drag"]').first();
+              if (await uploadArea.count() > 0) {
+                const [fileChooser] = await Promise.all([
+                  this.page.waitForEvent('filechooser', { timeout: 10000 }),
+                  uploadArea.click(),
+                ]);
+                await fileChooser.setFiles(params.videoPath);
+                uploadTriggered = true;
+                logger.info('[Weixin Publish] Upload triggered after navigation');
+              }
+            }
+          }
+        } catch (e) {
+          logger.warn(`[Weixin Publish] Position-based click failed: ${e}`);
         }
       }
 
-      // 备用方法:直接设置 file input
+      // 方法5: 点击页面左侧区域(微信视频号发布页面的上传区域在左侧)
       if (!uploadTriggered) {
-        const fileInput = await this.page.$('input[type="file"]');
-        if (fileInput) {
-          await fileInput.setInputFiles(params.videoPath);
-          uploadTriggered = true;
+        logger.info('[Weixin Publish] Trying left area click (upload area is usually on the left)...');
+        try {
+          const viewport = this.page.viewportSize();
+          if (viewport) {
+            // 微信视频号发布页面的上传区域在页面左侧中央
+            // 根据截图布局,大约在 x=400-550, y=250-400 的区域
+            const clickPositions = [
+              { x: viewport.width * 0.35, y: viewport.height * 0.35 }, // 左侧偏上
+              { x: viewport.width * 0.35, y: viewport.height * 0.4 },  // 左侧中央
+              { x: 450, y: 300 }, // 固定位置尝试
+              { x: 540, y: 350 }, // 固定位置尝试
+            ];
+            
+            for (const pos of clickPositions) {
+              if (uploadTriggered) break;
+              try {
+                logger.info(`[Weixin Publish] Trying click at: ${pos.x}, ${pos.y}`);
+                const [fileChooser] = await Promise.all([
+                  this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                  this.page.mouse.click(pos.x, pos.y),
+                ]);
+                await fileChooser.setFiles(params.videoPath);
+                uploadTriggered = true;
+                logger.info(`[Weixin Publish] Upload triggered via position click: ${pos.x}, ${pos.y}`);
+              } catch (e) {
+                // 继续尝试下一个位置
+              }
+            }
+          }
+        } catch (e) {
+          logger.warn(`[Weixin Publish] Left area click failed: ${e}`);
         }
       }
 
       if (!uploadTriggered) {
+        // 截图调试
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/weixin_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Weixin Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
         throw new Error('未找到上传入口');
       }
 
@@ -305,6 +602,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
       // 等待上传完成
       const maxWaitTime = 300000; // 5分钟
       const startTime = Date.now();
+      let lastAiCheckTime = 0;
+      const aiCheckInterval = 10000; // 每10秒使用AI检测一次
 
       while (Date.now() - startTime < maxWaitTime) {
         // 检查发布按钮是否可用
@@ -318,13 +617,48 @@ export class WeixinAdapter extends BasePlatformAdapter {
           // 继续等待
         }
 
-        // 检查上传进度
+        // 检查上传进度(通过DOM)
+        let progressDetected = false;
         const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
         if (progressText) {
           const match = progressText.match(/(\d+)%/);
           if (match) {
             const progress = parseInt(match[1]);
             onProgress?.(20 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
+            progressDetected = true;
+            if (progress >= 100) {
+              logger.info('[Weixin Publish] Upload progress reached 100%');
+              break;
+            }
+          }
+        }
+
+        // 使用AI检测上传进度(每隔一段时间检测一次)
+        if (!progressDetected && Date.now() - lastAiCheckTime > aiCheckInterval) {
+          lastAiCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'weixin_video');
+            logger.info(`[Weixin Publish] AI upload status:`, uploadStatus);
+            
+            if (uploadStatus.isComplete) {
+              logger.info('[Weixin Publish] AI detected upload complete');
+              break;
+            }
+            
+            if (uploadStatus.isFailed) {
+              throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
+            }
+            
+            if (uploadStatus.progress !== null) {
+              onProgress?.(20 + Math.floor(uploadStatus.progress * 0.4), `视频上传中: ${uploadStatus.progress}%`);
+              if (uploadStatus.progress >= 100) {
+                logger.info('[Weixin Publish] AI detected progress 100%');
+                break;
+              }
+            }
+          } catch (aiError) {
+            logger.warn('[Weixin Publish] AI progress check failed:', aiError);
           }
         }
 
@@ -363,6 +697,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
       const publishMaxWait = 120000; // 2分钟
       const publishStartTime = Date.now();
       let aiCheckCounter = 0;
+      let lastProgressCheckTime = 0;
+      const progressCheckInterval = 5000; // 每5秒检测一次发布进度
 
       while (Date.now() - publishStartTime < publishMaxWait) {
         await this.page.waitForTimeout(3000);
@@ -378,13 +714,55 @@ export class WeixinAdapter extends BasePlatformAdapter {
           };
         }
 
+        // 检查发布进度条(DOM方式)
+        const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
+        if (publishProgressText) {
+          const match = publishProgressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(90 + Math.floor(progress * 0.1), `发布中: ${progress}%`);
+            logger.info(`[Weixin Publish] Publish progress: ${progress}%`);
+          }
+        }
+
+        // AI检测发布进度(定期检测)
+        if (Date.now() - lastProgressCheckTime > progressCheckInterval) {
+          lastProgressCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const publishStatus = await aiService.analyzePublishProgress(screenshot, 'weixin_video');
+            logger.info(`[Weixin Publish] AI publish status:`, publishStatus);
+            
+            if (publishStatus.isComplete) {
+              logger.info('[Weixin Publish] AI detected publish complete');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+            
+            if (publishStatus.isFailed) {
+              throw new Error(`发布失败: ${publishStatus.statusDescription}`);
+            }
+            
+            if (publishStatus.progress !== null) {
+              onProgress?.(90 + Math.floor(publishStatus.progress * 0.1), `发布中: ${publishStatus.progress}%`);
+            }
+            
+            if (publishStatus.isPublishing) {
+              logger.info(`[Weixin Publish] Still publishing: ${publishStatus.statusDescription}`);
+            }
+          } catch (aiError) {
+            logger.warn('[Weixin Publish] AI publish progress check failed:', aiError);
+          }
+        }
+
         // 检查错误提示
         const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
         if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
           throw new Error(`发布失败: ${errorHint}`);
         }
 
-        // AI 辅助检测(每 3 次循环)
+        // AI 辅助检测状态(每 3 次循环)
         aiCheckCounter++;
         if (aiCheckCounter >= 3) {
           aiCheckCounter = 0;

+ 275 - 96
server/src/automation/platforms/xiaohongshu.ts

@@ -12,6 +12,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
+import { aiService } from '../../ai/index.js';
 
 // 小红书 Python API 服务配置
 const XHS_PYTHON_SERVICE_URL = process.env.XHS_SERVICE_URL || 'http://localhost:5005';
@@ -25,7 +26,7 @@ const SERVER_ROOT = path.resolve(process.cwd());
 export class XiaohongshuAdapter extends BasePlatformAdapter {
   readonly platform: PlatformType = 'xiaohongshu';
   readonly loginUrl = 'https://creator.xiaohongshu.com/';
-  readonly publishUrl = 'https://creator.xiaohongshu.com/publish/publish';
+  readonly publishUrl = 'https://creator.xiaohongshu.com/publish/publish?from=menu&target=video';
   readonly creatorHomeUrl = 'https://creator.xiaohongshu.com/creator/home';
   readonly contentManageUrl = 'https://creator.xiaohongshu.com/creator/content';
 
@@ -141,6 +142,75 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
   }
 
   /**
+   * 关闭页面上可能存在的弹窗对话框
+   */
+  private async closeModalDialogs(): Promise<boolean> {
+    if (!this.page) return false;
+    
+    let closedAny = false;
+    
+    try {
+      // 检查并关闭 Element UI / Vue 弹窗
+      const modalSelectors = [
+        // Element UI 弹窗关闭按钮
+        '.el-dialog__close',
+        '.el-dialog__headerbtn',
+        '.el-message-box__close',
+        '.el-overlay-dialog .el-icon-close',
+        // 通用关闭按钮
+        '[class*="modal"] [class*="close"]',
+        '[class*="dialog"] [class*="close"]',
+        '[role="dialog"] button[aria-label="close"]',
+        '[role="dialog"] [class*="close"]',
+        // 取消/关闭按钮
+        '.el-dialog__footer button:has-text("取消")',
+        '.el-dialog__footer button:has-text("关闭")',
+        '[role="dialog"] button:has-text("取消")',
+        '[role="dialog"] button:has-text("关闭")',
+        // 遮罩层(点击遮罩关闭)
+        '.el-overlay[style*="display: none"]',
+      ];
+      
+      for (const selector of modalSelectors) {
+        try {
+          const closeBtn = this.page.locator(selector).first();
+          if (await closeBtn.count() > 0 && await closeBtn.isVisible()) {
+            logger.info(`[Xiaohongshu] Found modal close button: ${selector}`);
+            await closeBtn.click({ timeout: 2000 });
+            closedAny = true;
+            await this.page.waitForTimeout(500);
+          }
+        } catch (e) {
+          // 忽略错误,继续尝试下一个选择器
+        }
+      }
+      
+      // 尝试按 ESC 键关闭弹窗
+      if (!closedAny) {
+        const hasModal = await this.page.locator('.el-overlay-dialog, [role="dialog"]').count();
+        if (hasModal > 0) {
+          logger.info('[Xiaohongshu] Trying ESC key to close modal...');
+          await this.page.keyboard.press('Escape');
+          await this.page.waitForTimeout(500);
+          
+          // 检查是否关闭成功
+          const stillHasModal = await this.page.locator('.el-overlay-dialog, [role="dialog"]').count();
+          closedAny = stillHasModal < hasModal;
+        }
+      }
+      
+      if (closedAny) {
+        logger.info('[Xiaohongshu] Successfully closed modal dialog');
+      }
+      
+    } catch (error) {
+      logger.warn('[Xiaohongshu] Error closing modal:', error);
+    }
+    
+    return closedAny;
+  }
+
+  /**
    * 获取账号信息
    * 通过拦截 API 响应获取准确数据
    */
@@ -483,7 +553,14 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
     const useHeadless = options?.headless ?? true;
 
     try {
+      logger.info('[Xiaohongshu Publish] Initializing browser...');
       await this.initBrowser({ headless: useHeadless });
+      
+      if (!this.page) {
+        throw new Error('浏览器初始化失败,page 为 null');
+      }
+      
+      logger.info('[Xiaohongshu Publish] Setting cookies...');
       await this.setCookies(cookies);
 
       if (!useHeadless) {
@@ -491,7 +568,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
         onProgress?.(1, '已打开浏览器窗口,请注意查看...');
       }
 
-      if (!this.page) throw new Error('Page not initialized');
+      // 再次检查 page 状态
+      if (!this.page) throw new Error('Page not initialized after setCookies');
 
       // 检查视频文件是否存在
       const fs = await import('fs');
@@ -530,90 +608,86 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
         }
       } catch {}
 
-      // 上传视频文件 - 小红书需要点击"上传视频"按钮触发文件选择
+      // 上传视频 - 优先使用 AI 截图分析找到上传入口
       let uploadTriggered = false;
-      
-      // 方法1: 点击"上传视频"按钮触发 file chooser
+
+      // 方法1: AI 截图分析找到上传入口
+      logger.info('[Xiaohongshu Publish] Using AI to find upload entry...');
       try {
-        logger.info('[Xiaohongshu Publish] Looking for upload button...');
-        
-        // 小红书的上传按钮通常显示"上传视频"文字
-        const uploadBtnSelectors = [
-          'button:has-text("上传视频")',
-          'div:has-text("上传视频"):not(:has(*))', // 纯文字的 div
-          '[class*="upload-btn"]',
-          '[class*="upload"] button',
-          'span:has-text("上传视频")',
-        ];
-        
-        for (const selector of uploadBtnSelectors) {
+        const screenshot = await this.screenshotBase64();
+        const guide = await aiService.getPageOperationGuide(screenshot, 'xiaohongshu', '找到视频上传入口并点击上传按钮');
+        logger.info(`[Xiaohongshu Publish] AI analysis result:`, guide);
+
+        if (guide.hasAction && guide.targetSelector) {
+          logger.info(`[Xiaohongshu Publish] AI suggested selector: ${guide.targetSelector}`);
           try {
-            const uploadBtn = this.page.locator(selector).first();
-            if (await uploadBtn.count() > 0 && await uploadBtn.isVisible()) {
-              logger.info(`[Xiaohongshu Publish] Found upload button via: ${selector}`);
-              const [fileChooser] = await Promise.all([
-                this.page.waitForEvent('filechooser', { timeout: 15000 }),
-                uploadBtn.click(),
-              ]);
-              await fileChooser.setFiles(params.videoPath);
-              uploadTriggered = true;
-              logger.info('[Xiaohongshu Publish] File selected via file chooser (button click)');
-              break;
-            }
+            const [fileChooser] = await Promise.all([
+              this.page.waitForEvent('filechooser', { timeout: 15000 }),
+              this.page.click(guide.targetSelector),
+            ]);
+            await fileChooser.setFiles(params.videoPath);
+            uploadTriggered = true;
+            logger.info('[Xiaohongshu Publish] Upload triggered via AI selector');
           } catch (e) {
-            logger.warn(`[Xiaohongshu Publish] Button click failed for ${selector}`);
+            logger.warn(`[Xiaohongshu Publish] AI selector click failed: ${e}`);
           }
         }
       } catch (e) {
-        logger.warn('[Xiaohongshu Publish] Upload button method failed:', e);
+        logger.warn(`[Xiaohongshu Publish] AI analysis failed: ${e}`);
       }
 
-      // 方法2: 点击上传区域(拖拽区域)
+      // 方法2: 尝试点击常见的上传区域触发 file chooser
       if (!uploadTriggered) {
-        try {
-          logger.info('[Xiaohongshu Publish] Trying click upload area...');
-          const uploadAreaSelectors = [
-            '[class*="upload-wrapper"]',
-            '[class*="upload-area"]', 
-            '[class*="drag-area"]',
-            '[class*="drop"]',
-            'div:has-text("拖拽视频到此")',
-          ];
-          
-          for (const selector of uploadAreaSelectors) {
-            const uploadArea = this.page.locator(selector).first();
-            if (await uploadArea.count() > 0 && await uploadArea.isVisible()) {
-              logger.info(`[Xiaohongshu Publish] Found upload area via: ${selector}`);
+        logger.info('[Xiaohongshu Publish] Trying common upload selectors...');
+        const uploadSelectors = [
+          // 小红书常见上传区域选择器
+          '[class*="upload-area"]',
+          '[class*="upload-btn"]',
+          '[class*="upload-trigger"]',
+          '[class*="upload-container"]',
+          '[class*="drag-area"]',
+          'div[class*="upload"] div',
+          '.upload-wrapper',
+          '.video-upload',
+          'button:has-text("上传")',
+          'div:has-text("上传视频"):not(:has(div))',
+          'span:has-text("上传视频")',
+          '[class*="add-video"]',
+        ];
+        
+        for (const selector of uploadSelectors) {
+          if (uploadTriggered) break;
+          try {
+            const element = this.page.locator(selector).first();
+            if (await element.count() > 0 && await element.isVisible()) {
+              logger.info(`[Xiaohongshu Publish] Trying selector: ${selector}`);
               const [fileChooser] = await Promise.all([
-                this.page.waitForEvent('filechooser', { timeout: 15000 }),
-                uploadArea.click(),
+                this.page.waitForEvent('filechooser', { timeout: 5000 }),
+                element.click(),
               ]);
               await fileChooser.setFiles(params.videoPath);
               uploadTriggered = true;
-              logger.info('[Xiaohongshu Publish] File selected via file chooser (area click)');
-              break;
+              logger.info(`[Xiaohongshu Publish] Upload triggered via selector: ${selector}`);
             }
+          } catch (e) {
+            // 继续尝试下一个选择器
           }
-        } catch (e) {
-          logger.warn('[Xiaohongshu Publish] Upload area method failed:', e);
         }
       }
 
-      // 方法3: 直接设置隐藏的 file input(最后尝试)
+      // 方法3: 直接设置 file input
       if (!uploadTriggered) {
         logger.info('[Xiaohongshu Publish] Trying direct file input...');
-        const uploadSelectors = [
-          'input[type="file"][accept*="video"]',
-          'input[type="file"]',
-        ];
-
-        for (const selector of uploadSelectors) {
+        const fileInputs = await this.page.$$('input[type="file"]');
+        logger.info(`[Xiaohongshu Publish] Found ${fileInputs.length} file inputs`);
+        
+        for (const fileInput of fileInputs) {
           try {
-            const fileInput = await this.page.$(selector);
-            if (fileInput) {
+            const accept = await fileInput.getAttribute('accept');
+            if (!accept || accept.includes('video') || accept.includes('*')) {
               await fileInput.setInputFiles(params.videoPath);
               uploadTriggered = true;
-              logger.info(`[Xiaohongshu Publish] File set via direct input: ${selector}`);
+              logger.info('[Xiaohongshu Publish] Upload triggered via file input');
               
               // 直接设置后需要等待一下,让页面响应
               await this.page.waitForTimeout(2000);
@@ -623,30 +697,23 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
               if (hasChange) {
                 logger.info('[Xiaohongshu Publish] Page responded to file input');
                 break;
-              } else {
-                // 如果页面没有响应,尝试触发 change 事件
-                await this.page.evaluate((sel) => {
-                  const input = document.querySelector(sel) as HTMLInputElement;
-                  if (input) {
-                    input.dispatchEvent(new Event('change', { bubbles: true }));
-                  }
-                }, selector);
-                await this.page.waitForTimeout(2000);
-                logger.info('[Xiaohongshu Publish] Dispatched change event');
               }
-              break;
             }
           } catch (e) {
-            logger.warn(`[Xiaohongshu Publish] Failed with selector ${selector}`);
+            logger.warn(`[Xiaohongshu Publish] File input method failed: ${e}`);
           }
         }
       }
 
       if (!uploadTriggered) {
         // 截图调试
-        const screenshotPath = `uploads/debug/xhs_no_upload_${Date.now()}.png`;
-        await this.page.screenshot({ path: screenshotPath, fullPage: true });
-        logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/xhs_no_upload_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
+          }
+        } catch {}
         throw new Error('无法上传视频文件');
       }
 
@@ -655,8 +722,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       // 等待视频上传完成
       const maxWaitTime = 300000; // 5分钟
       const startTime = Date.now();
+      let lastAiCheckTime = 0;
+      const aiCheckInterval = 10000; // 每10秒使用AI检测一次
+      let uploadComplete = false;
 
-      while (Date.now() - startTime < maxWaitTime) {
+      while (Date.now() - startTime < maxWaitTime && !uploadComplete) {
         await this.page.waitForTimeout(3000);
 
         // 检查当前URL是否变化(上传成功后可能跳转)
@@ -665,7 +735,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
           logger.info(`[Xiaohongshu Publish] URL changed to: ${newUrl}`);
         }
 
-        // 检查上传进度
+        // 检查上传进度(通过DOM)
+        let progressDetected = false;
         const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
         if (progressText) {
           const match = progressText.match(/(\d+)%/);
@@ -673,6 +744,12 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
             const progress = parseInt(match[1]);
             onProgress?.(15 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
             logger.info(`[Xiaohongshu Publish] Upload progress: ${progress}%`);
+            progressDetected = true;
+            if (progress >= 100) {
+              logger.info('[Xiaohongshu Publish] Upload progress reached 100%');
+              uploadComplete = true;
+              break;
+            }
           }
         }
 
@@ -691,17 +768,52 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
           const count = await this.page.locator(selector).count();
           if (count > 0) {
             logger.info(`[Xiaohongshu Publish] Video upload completed, found: ${selector}`);
+            uploadComplete = true;
             break;
           }
         }
         
+        if (uploadComplete) break;
+        
         // 如果标题输入框出现,说明可以开始填写了
         const titleInput = await this.page.locator('input[placeholder*="标题"]').count();
         if (titleInput > 0) {
           logger.info('[Xiaohongshu Publish] Title input found, upload must be complete');
+          uploadComplete = true;
           break;
         }
 
+        // 使用AI检测上传进度(每隔一段时间检测一次)
+        if (!progressDetected && !uploadComplete && Date.now() - lastAiCheckTime > aiCheckInterval) {
+          lastAiCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'xiaohongshu');
+            logger.info(`[Xiaohongshu Publish] AI upload status:`, uploadStatus);
+            
+            if (uploadStatus.isComplete) {
+              logger.info('[Xiaohongshu Publish] AI detected upload complete');
+              uploadComplete = true;
+              break;
+            }
+            
+            if (uploadStatus.isFailed) {
+              throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
+            }
+            
+            if (uploadStatus.progress !== null) {
+              onProgress?.(15 + Math.floor(uploadStatus.progress * 0.4), `视频上传中: ${uploadStatus.progress}%`);
+              if (uploadStatus.progress >= 100) {
+                logger.info('[Xiaohongshu Publish] AI detected progress 100%');
+                uploadComplete = true;
+                break;
+              }
+            }
+          } catch (aiError) {
+            logger.warn('[Xiaohongshu Publish] AI progress check failed:', aiError);
+          }
+        }
+
         // 检查是否上传失败
         const failText = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
         if (failText && failText.includes('失败')) {
@@ -795,13 +907,20 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       const stillInEditMode = await this.page.locator('input[placeholder*="标题"], [class*="video-preview"]').count() > 0;
       if (!stillInEditMode) {
         logger.error('[Xiaohongshu Publish] Page is no longer in edit mode!');
-        const screenshotPath = `uploads/debug/xhs_not_in_edit_${Date.now()}.png`;
-        await this.page.screenshot({ path: screenshotPath, fullPage: true });
+        try {
+          if (this.page) {
+            const screenshotPath = `uploads/debug/xhs_not_in_edit_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+          }
+        } catch {}
         throw new Error('页面状态异常,请重试');
       }
 
       onProgress?.(85, '正在发布...');
 
+      // 先关闭可能存在的弹窗
+      await this.closeModalDialogs();
+
       // 滚动到页面底部,确保发布按钮可见
       logger.info('[Xiaohongshu Publish] Scrolling to bottom...');
       await this.page.evaluate(() => {
@@ -813,9 +932,13 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       logger.info('[Xiaohongshu Publish] Looking for publish button...');
       
       // 先截图看当前页面状态
-      const beforeClickPath = `uploads/debug/xhs_before_publish_${Date.now()}.png`;
-      await this.page.screenshot({ path: beforeClickPath, fullPage: true });
-      logger.info(`[Xiaohongshu Publish] Screenshot before publish: ${beforeClickPath}`);
+      try {
+        if (this.page) {
+          const beforeClickPath = `uploads/debug/xhs_before_publish_${Date.now()}.png`;
+          await this.page.screenshot({ path: beforeClickPath, fullPage: true });
+          logger.info(`[Xiaohongshu Publish] Screenshot before publish: ${beforeClickPath}`);
+        }
+      } catch {}
       
       let publishClicked = false;
       
@@ -923,9 +1046,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       if (!publishClicked) {
         // 截图调试
         try {
-          const screenshotPath = `uploads/debug/xhs_no_publish_btn_${Date.now()}.png`;
-          await this.page.screenshot({ path: screenshotPath, fullPage: true });
-          logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
+          if (this.page) {
+            const screenshotPath = `uploads/debug/xhs_no_publish_btn_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
+          }
         } catch {}
         throw new Error('未找到发布按钮');
       }
@@ -936,6 +1061,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       const publishMaxWait = 120000; // 2分钟
       const publishStartTime = Date.now();
       let aiCheckCounter = 0;
+      let lastProgressCheckTime = 0;
+      const progressCheckInterval = 5000; // 每5秒检测一次发布进度
 
       while (Date.now() - publishStartTime < publishMaxWait) {
         await this.page.waitForTimeout(3000);
@@ -965,6 +1092,48 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
           };
         }
 
+        // 检查发布进度条(DOM方式)
+        const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
+        if (publishProgressText) {
+          const match = publishProgressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(90 + Math.floor(progress * 0.1), `发布中: ${progress}%`);
+            logger.info(`[Xiaohongshu Publish] Publish progress: ${progress}%`);
+          }
+        }
+
+        // AI检测发布进度(定期检测)
+        if (Date.now() - lastProgressCheckTime > progressCheckInterval) {
+          lastProgressCheckTime = Date.now();
+          try {
+            const screenshot = await this.screenshotBase64();
+            const publishStatus = await aiService.analyzePublishProgress(screenshot, 'xiaohongshu');
+            logger.info(`[Xiaohongshu Publish] AI publish progress status:`, publishStatus);
+            
+            if (publishStatus.isComplete) {
+              logger.info('[Xiaohongshu Publish] AI detected publish complete');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+            
+            if (publishStatus.isFailed) {
+              throw new Error(`发布失败: ${publishStatus.statusDescription}`);
+            }
+            
+            if (publishStatus.progress !== null) {
+              onProgress?.(90 + Math.floor(publishStatus.progress * 0.1), `发布中: ${publishStatus.progress}%`);
+            }
+            
+            if (publishStatus.isPublishing) {
+              logger.info(`[Xiaohongshu Publish] Still publishing: ${publishStatus.statusDescription}`);
+            }
+          } catch (aiError) {
+            logger.warn('[Xiaohongshu Publish] AI publish progress check failed:', aiError);
+          }
+        }
+
         // 检查错误提示
         const errorToast = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
         if (errorToast && (errorToast.includes('失败') || errorToast.includes('错误'))) {
@@ -1027,6 +1196,10 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
             // AI 建议需要操作
             if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
               logger.info(`[Xiaohongshu Publish] AI suggests action: ${aiStatus.nextAction.targetDescription}`);
+              
+              // 先尝试关闭可能存在的弹窗
+              await this.closeModalDialogs();
+              
               const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
               if (guide?.hasAction) {
                 await this.aiExecuteOperation(guide);
@@ -1060,9 +1233,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
 
       // 截图调试
       try {
-        const screenshotPath = `uploads/debug/xhs_publish_timeout_${Date.now()}.png`;
-        await this.page.screenshot({ path: screenshotPath, fullPage: true });
-        logger.info(`[Xiaohongshu Publish] Timeout screenshot saved: ${screenshotPath}`);
+        if (this.page) {
+          const screenshotPath = `uploads/debug/xhs_publish_timeout_${Date.now()}.png`;
+          await this.page.screenshot({ path: screenshotPath, fullPage: true });
+          logger.info(`[Xiaohongshu Publish] Timeout screenshot saved: ${screenshotPath}`);
+        }
       } catch {}
 
       throw new Error('发布超时,请手动检查是否发布成功');
@@ -1311,9 +1486,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
 
       // 截图用于调试
       try {
-        const screenshotPath = `uploads/debug/xhs_delete_page_${Date.now()}.png`;
-        await this.page.screenshot({ path: screenshotPath, fullPage: true });
-        logger.info(`[Xiaohongshu Delete] Page screenshot: ${screenshotPath}`);
+        if (this.page) {
+          const screenshotPath = `uploads/debug/xhs_delete_page_${Date.now()}.png`;
+          await this.page.screenshot({ path: screenshotPath, fullPage: true });
+          logger.info(`[Xiaohongshu Delete] Page screenshot: ${screenshotPath}`);
+        }
       } catch {}
 
       // 在笔记管理页面找到对应的笔记行
@@ -1404,9 +1581,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       if (!deleteClicked) {
         // 截图调试
         try {
-          const screenshotPath = `uploads/debug/xhs_delete_no_btn_${Date.now()}.png`;
-          await this.page.screenshot({ path: screenshotPath, fullPage: true });
-          logger.info(`[Xiaohongshu Delete] No delete button found, screenshot: ${screenshotPath}`);
+          if (this.page) {
+            const screenshotPath = `uploads/debug/xhs_delete_no_btn_${Date.now()}.png`;
+            await this.page.screenshot({ path: screenshotPath, fullPage: true });
+            logger.info(`[Xiaohongshu Delete] No delete button found, screenshot: ${screenshotPath}`);
+          }
         } catch {}
         throw new Error('未找到删除按钮');
       }

+ 5 - 1
server/src/services/HeadlessBrowserService.ts

@@ -1635,10 +1635,14 @@ class HeadlessBrowserService {
         homeUrl: 'https://creator.xiaohongshu.com/',
         loginIndicators: ['login', 'passport'],
       },
-      weixin: {
+      weixin_video: {
         homeUrl: 'https://channels.weixin.qq.com/platform',
         loginIndicators: ['login', 'passport'],
       },
+      baijiahao: {
+        homeUrl: 'https://baijiahao.baidu.com/builder/rc/home',
+        loginIndicators: ['login', 'passport'],
+      },
       toutiao: {
         homeUrl: 'https://mp.toutiao.com/profile_v4/index',
         loginIndicators: ['login', 'passport', 'sso'],

+ 27 - 3
server/src/services/PublishService.ts

@@ -96,6 +96,30 @@ export class PublishService {
   }
 
   async createTask(userId: number, data: CreatePublishTaskRequest): Promise<PublishTaskType> {
+    // 验证目标账号是否存在
+    const validAccountIds: number[] = [];
+    const invalidAccountIds: number[] = [];
+    
+    for (const accountId of data.targetAccounts) {
+      const account = await this.accountRepository.findOne({ 
+        where: { id: accountId, userId } 
+      });
+      if (account) {
+        validAccountIds.push(accountId);
+      } else {
+        invalidAccountIds.push(accountId);
+        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,
@@ -104,7 +128,7 @@ export class PublishService {
       description: data.description || null,
       coverPath: data.coverPath || null,
       tags: data.tags || null,
-      targetAccounts: data.targetAccounts,
+      targetAccounts: validAccountIds, // 只保存有效的账号 ID
       platformConfigs: data.platformConfigs || null,
       status: 'pending', // 初始状态为 pending,任务队列执行时再更新为 processing
       scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : null,
@@ -112,8 +136,8 @@ export class PublishService {
 
     await this.taskRepository.save(task);
 
-    // 创建发布结果记录
-    for (const accountId of data.targetAccounts) {
+    // 创建发布结果记录(只为有效账号创建)
+    for (const accountId of validAccountIds) {
       const result = this.resultRepository.create({
         taskId: task.id,
         accountId,

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů