Преглед изворни кода

Enhance video publishing functionality and error handling

- Added functions to validate video files and parse datetime strings in app.py.
- Improved error messages for missing parameters and invalid video files in the publish_video function.
- Enhanced logging for video publishing processes in Douyin and Xiaohongshu platforms, including detailed status updates and error handling.
- Updated automation platform adapters to convert relative video and cover paths to absolute paths for consistency.
- Improved error handling in the Weixin and Kuaishou adapters to ensure robust video publishing.
Ethanfly пре 14 часа
родитељ
комит
4ebd86507b

+ 69 - 3
server/python/app.py

@@ -14,15 +14,67 @@
 
 import asyncio
 import os
+import sys
 import argparse
 import traceback
 from datetime import datetime
+from pathlib import Path
+
+# 确保当前目录在 Python 路径中
+CURRENT_DIR = Path(__file__).parent.resolve()
+if str(CURRENT_DIR) not in sys.path:
+    sys.path.insert(0, str(CURRENT_DIR))
+
 from flask import Flask, request, jsonify
 from flask_cors import CORS
 
 from platforms import get_publisher, PLATFORM_MAP
 from platforms.base import PublishParams
-from utils.helpers import parse_datetime, validate_video_file
+
+
+def parse_datetime(date_str: str):
+    """解析日期时间字符串"""
+    if not date_str:
+        return None
+    
+    formats = [
+        "%Y-%m-%d %H:%M:%S",
+        "%Y-%m-%d %H:%M",
+        "%Y/%m/%d %H:%M:%S",
+        "%Y/%m/%d %H:%M",
+        "%Y-%m-%dT%H:%M:%S",
+        "%Y-%m-%dT%H:%M:%SZ",
+    ]
+    
+    for fmt in formats:
+        try:
+            return datetime.strptime(date_str, fmt)
+        except ValueError:
+            continue
+    
+    return None
+
+
+def validate_video_file(video_path: str) -> bool:
+    """验证视频文件是否有效"""
+    if not video_path:
+        return False
+    
+    if not os.path.exists(video_path):
+        return False
+    
+    if not os.path.isfile(video_path):
+        return False
+    
+    valid_extensions = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.webm']
+    ext = os.path.splitext(video_path)[1].lower()
+    if ext not in valid_extensions:
+        return False
+    
+    if os.path.getsize(video_path) < 1024:
+        return False
+    
+    return True
 
 # 创建 Flask 应用
 app = Flask(__name__)
@@ -96,22 +148,36 @@ def publish_video():
         post_time = data.get("post_time")
         location = data.get("location", "重庆市")
         
+        # 调试日志
+        print(f"[Publish] 收到请求: platform={platform}, title={title}, video_path={video_path}")
+        
         # 参数验证
         if not platform:
+            print("[Publish] 错误: 缺少 platform 参数")
             return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
         if platform not in PLATFORM_MAP:
+            print(f"[Publish] 错误: 不支持的平台 {platform}")
             return jsonify({
                 "success": False, 
                 "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
             }), 400
         if not cookie_str:
+            print("[Publish] 错误: 缺少 cookie 参数")
             return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
         if not title:
+            print("[Publish] 错误: 缺少 title 参数")
             return jsonify({"success": False, "error": "缺少 title 参数"}), 400
         if not video_path:
+            print("[Publish] 错误: 缺少 video_path 参数")
             return jsonify({"success": False, "error": "缺少 video_path 参数"}), 400
-        if not validate_video_file(video_path):
-            return jsonify({"success": False, "error": f"视频文件无效: {video_path}"}), 400
+        
+        # 视频文件验证(增加详细信息)
+        if not os.path.exists(video_path):
+            print(f"[Publish] 错误: 视频文件不存在: {video_path}")
+            return jsonify({"success": False, "error": f"视频文件不存在: {video_path}"}), 400
+        if not os.path.isfile(video_path):
+            print(f"[Publish] 错误: 路径不是文件: {video_path}")
+            return jsonify({"success": False, "error": f"路径不是文件: {video_path}"}), 400
         
         # 解析发布时间
         publish_date = parse_datetime(post_time) if post_time else None

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


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


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


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


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


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


+ 43 - 4
server/python/platforms/douyin.py

@@ -49,13 +49,22 @@ class DouyinPublisher(BasePublisher):
     
     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, "正在初始化浏览器...")
         
         # 初始化浏览器
         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:
@@ -65,6 +74,8 @@ class DouyinPublisher(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, "正在打开上传页面...")
         
         # 访问上传页面
@@ -174,32 +185,60 @@ class DouyinPublisher(BasePublisher):
             await self.set_schedule_time(params.publish_date)
         
         self.report_progress(80, "正在发布...")
+        print(f"[{self.platform_name}] 查找发布按钮...")
         
         # 点击发布
-        for _ in range(30):
+        publish_clicked = False
+        for i in range(30):
             try:
                 publish_btn = self.page.get_by_role('button', name="发布", exact=True)
-                if await publish_btn.count():
+                btn_count = await publish_btn.count()
+                print(f"[{self.platform_name}] 发布按钮数量: {btn_count}")
+                
+                if btn_count > 0:
+                    print(f"[{self.platform_name}] 点击发布按钮...")
                     await publish_btn.click()
+                    publish_clicked = True
+                
                 await self.page.wait_for_url(
                     "https://creator.douyin.com/creator-micro/content/manage",
                     timeout=5000
                 )
                 self.report_progress(100, "发布成功")
+                print(f"[{self.platform_name}] 发布成功! 已跳转到内容管理页面")
                 return PublishResult(
                     success=True,
                     platform=self.platform_name,
                     message="发布成功"
                 )
-            except:
+            except Exception as e:
                 current_url = self.page.url
+                print(f"[{self.platform_name}] 尝试 {i+1}/30, 当前URL: {current_url}")
+                
                 if "content/manage" in current_url:
                     self.report_progress(100, "发布成功")
+                    print(f"[{self.platform_name}] 发布成功! 已在内容管理页面")
                     return PublishResult(
                         success=True,
                         platform=self.platform_name,
                         message="发布成功"
                     )
+                
+                # 检查是否有错误提示
+                try:
+                    error_toast = self.page.locator('[class*="toast"][class*="error"], [class*="error-tip"]')
+                    if await error_toast.count() > 0:
+                        error_text = await error_toast.first.text_content()
+                        if error_text:
+                            print(f"[{self.platform_name}] 检测到错误提示: {error_text}")
+                            raise Exception(f"发布失败: {error_text}")
+                except:
+                    pass
+                
                 await asyncio.sleep(1)
         
-        raise Exception("发布超时")
+        # 发布超时,保存截图
+        screenshot_path = f"debug_publish_timeout_{self.platform_name}.png"
+        await self.page.screenshot(path=screenshot_path, full_page=True)
+        print(f"[{self.platform_name}] 发布超时,截图保存到: {screenshot_path}")
+        raise Exception(f"发布超时(截图: {screenshot_path})")

+ 262 - 65
server/python/platforms/xiaohongshu.py

@@ -91,62 +91,110 @@ class XiaohongshuPublisher(BasePublisher):
             raise Exception("xhs SDK 未安装,请运行: pip install xhs")
         
         self.report_progress(10, "正在通过 API 发布...")
+        print(f"[{self.platform_name}] 使用 XHS SDK API 发布...")
+        print(f"[{self.platform_name}] 视频路径: {params.video_path}")
+        print(f"[{self.platform_name}] 标题: {params.title}")
         
         # 转换 cookie 格式
         cookie_list = self.parse_cookies(cookies)
         cookie_string = self.cookies_to_string(cookie_list) if cookie_list else cookies
+        print(f"[{self.platform_name}] Cookie 长度: {len(cookie_string)}")
         
         self.report_progress(20, "正在上传视频...")
         
         # 创建客户端
         xhs_client = XhsClient(cookie_string, sign=self.sign_sync)
         
+        print(f"[{self.platform_name}] 开始调用 create_video_note...")
+        
         # 发布视频
-        result = xhs_client.create_video_note(
-            title=params.title,
-            desc=params.description or params.title,
-            topics=params.tags or [],
-            post_time=params.publish_date.strftime("%Y-%m-%d %H:%M:%S") if params.publish_date else None,
-            video_path=params.video_path,
-            cover_path=params.cover_path if params.cover_path and os.path.exists(params.cover_path) else None
-        )
+        try:
+            result = xhs_client.create_video_note(
+                title=params.title,
+                desc=params.description or params.title,
+                topics=params.tags or [],
+                post_time=params.publish_date.strftime("%Y-%m-%d %H:%M:%S") if params.publish_date else None,
+                video_path=params.video_path,
+                cover_path=params.cover_path if params.cover_path and os.path.exists(params.cover_path) else None
+            )
+            print(f"[{self.platform_name}] SDK 返回结果: {result}")
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            print(f"[{self.platform_name}] SDK 调用失败: {e}")
+            raise Exception(f"XHS SDK 发布失败: {e}")
+        
+        # 验证返回结果
+        if not result:
+            raise Exception("XHS SDK 返回空结果")
+        
+        # 检查是否有错误
+        if isinstance(result, dict):
+            if result.get("code") and result.get("code") != 0:
+                raise Exception(f"发布失败: {result.get('msg', '未知错误')}")
+            if result.get("success") == False:
+                raise Exception(f"发布失败: {result.get('msg', result.get('error', '未知错误'))}")
+        
+        note_id = result.get("note_id", "") if isinstance(result, dict) else ""
+        video_url = result.get("url", "") if isinstance(result, dict) else ""
+        
+        if not note_id:
+            print(f"[{self.platform_name}] 警告: 未获取到 note_id,返回结果: {result}")
         
         self.report_progress(100, "发布成功")
+        print(f"[{self.platform_name}] 发布成功! note_id={note_id}, url={video_url}")
         
         return PublishResult(
             success=True,
             platform=self.platform_name,
-            video_id=result.get("note_id", ""),
-            video_url=result.get("url", ""),
+            video_id=note_id,
+            video_url=video_url,
             message="发布成功"
         )
     
     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}] XHS SDK 可用: {XHS_SDK_AVAILABLE}")
+        print(f"{'='*60}")
+        
         # 检查视频文件
         if not os.path.exists(params.video_path):
             raise Exception(f"视频文件不存在: {params.video_path}")
         
-        self.report_progress(5, "正在准备发布...")
+        print(f"[{self.platform_name}] 视频文件存在,大小: {os.path.getsize(params.video_path)} bytes")
         
-        # 优先使用 API 方式
-        if XHS_SDK_AVAILABLE:
-            try:
-                return await self.publish_via_api(cookies, params)
-            except Exception as e:
-                print(f"[{self.platform_name}] API 发布失败: {e}")
-                print(f"[{self.platform_name}] 尝试使用 Playwright 方式...")
+        self.report_progress(5, "正在准备发布...")
         
-        # 回退到 Playwright 方式
+        # 临时禁用 API 方式,直接使用 Playwright(更稳定)
+        # TODO: 后续优化 API 方式的返回值验证
+        # if XHS_SDK_AVAILABLE:
+        #     try:
+        #         result = await self.publish_via_api(cookies, params)
+        #         print(f"[{self.platform_name}] API 发布完成: {result}")
+        #         return result
+        #     except Exception as e:
+        #         import traceback
+        #         traceback.print_exc()
+        #         print(f"[{self.platform_name}] API 发布失败: {e}")
+        #         print(f"[{self.platform_name}] 尝试使用 Playwright 方式...")
+        
+        # 使用 Playwright 方式发布(更可靠)
+        print(f"[{self.platform_name}] 使用 Playwright 方式发布...")
         return await self.publish_via_playwright(cookies, params)
     
     async def publish_via_playwright(self, cookies: str, params: PublishParams) -> PublishResult:
-        """通过 Playwright 发布视频(备用方式)"""
+        """通过 Playwright 发布视频"""
         self.report_progress(10, "正在初始化浏览器...")
+        print(f"[{self.platform_name}] Playwright 方式开始...")
         
         await self.init_browser()
         
         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:
@@ -154,112 +202,261 @@ class XiaohongshuPublisher(BasePublisher):
         
         self.report_progress(15, "正在打开发布页面...")
         
-        await self.page.goto(self.publish_url)
+        # 直接访问视频发布页面
+        publish_url = "https://creator.xiaohongshu.com/publish/publish?source=official"
+        print(f"[{self.platform_name}] 打开页面: {publish_url}")
+        await self.page.goto(publish_url)
         await asyncio.sleep(3)
         
+        current_url = self.page.url
+        print(f"[{self.platform_name}] 当前 URL: {current_url}")
+        
         # 检查登录状态
-        if "login" in self.page.url or "passport" in self.page.url:
-            raise Exception("登录已过期,请重新登录")
+        if "login" in current_url or "passport" in current_url:
+            screenshot_path = f"debug_login_required_{self.platform_name}.png"
+            await self.page.screenshot(path=screenshot_path)
+            raise Exception(f"登录已过期,请重新登录(截图: {screenshot_path})")
         
         self.report_progress(20, "正在上传视频...")
         
-        # 尝试点击视频标签
-        try:
-            video_tab = self.page.locator('div.tab:has-text("上传视频"), span:has-text("上传视频")').first
-            if await video_tab.count() > 0:
-                await video_tab.click()
-                await asyncio.sleep(1)
-        except:
-            pass
+        # 等待页面加载
+        await asyncio.sleep(2)
         
         # 上传视频
         upload_triggered = False
         
-        # 方法1: 点击上传按钮
-        try:
-            upload_btn = self.page.locator('button:has-text("上传视频")').first
-            if await upload_btn.count() > 0:
-                async with self.page.expect_file_chooser() as fc_info:
-                    await upload_btn.click()
-                file_chooser = await fc_info.value
-                await file_chooser.set_files(params.video_path)
-                upload_triggered = True
-        except:
-            pass
-        
-        # 方法2: 直接设置 file input
+        # 方法1: 直接设置隐藏的 file input
+        print(f"[{self.platform_name}] 尝试方法1: 设置 file input")
+        file_inputs = self.page.locator('input[type="file"]')
+        input_count = await file_inputs.count()
+        print(f"[{self.platform_name}] 找到 {input_count} 个 file input")
+        
+        if input_count > 0:
+            # 找到接受视频的 input
+            for i in range(input_count):
+                input_el = file_inputs.nth(i)
+                accept = await input_el.get_attribute('accept') or ''
+                print(f"[{self.platform_name}] Input {i} accept: {accept}")
+                if 'video' in accept or '*' in accept or not accept:
+                    await input_el.set_input_files(params.video_path)
+                    upload_triggered = True
+                    print(f"[{self.platform_name}] 视频文件已设置到 input {i}")
+                    break
+        
+        # 方法2: 点击上传区域触发文件选择器
         if not upload_triggered:
-            file_input = await self.page.$('input[type="file"]')
-            if file_input:
-                await file_input.set_input_files(params.video_path)
-                upload_triggered = True
+            print(f"[{self.platform_name}] 尝试方法2: 点击上传区域")
+            try:
+                upload_area = self.page.locator('[class*="upload-wrapper"], [class*="upload-area"], .upload-input').first
+                if await upload_area.count() > 0:
+                    async with self.page.expect_file_chooser(timeout=5000) as fc_info:
+                        await upload_area.click()
+                    file_chooser = await fc_info.value
+                    await file_chooser.set_files(params.video_path)
+                    upload_triggered = True
+                    print(f"[{self.platform_name}] 通过点击上传区域上传成功")
+            except Exception as e:
+                print(f"[{self.platform_name}] 方法2失败: {e}")
         
         if not upload_triggered:
-            raise Exception("无法上传视频文件")
+            screenshot_path = f"debug_upload_failed_{self.platform_name}.png"
+            await self.page.screenshot(path=screenshot_path)
+            raise Exception(f"无法上传视频文件(截图: {screenshot_path})")
         
         self.report_progress(40, "等待视频上传完成...")
+        print(f"[{self.platform_name}] 等待视频上传和处理...")
         
-        # 等待上传完成
-        for _ in range(100):
+        # 等待上传完成(检测页面变化)
+        upload_complete = False
+        for i in range(60):  # 最多等待3分钟
             await asyncio.sleep(3)
-            # 检查标题输入框是否出现
-            title_input = await self.page.locator('input[placeholder*="标题"]').count()
-            if title_input > 0:
+            
+            # 检查是否有标题输入框(上传完成后出现)
+            title_input_count = await self.page.locator('input[placeholder*="标题"], input[placeholder*="填写标题"]').count()
+            # 或者检查编辑器区域
+            editor_count = await self.page.locator('[class*="ql-editor"], [contenteditable="true"]').count()
+            # 检查发布按钮是否可见
+            publish_btn_count = await self.page.locator('.publishBtn, button:has-text("发布")').count()
+            
+            print(f"[{self.platform_name}] 检测 {i+1}: 标题框={title_input_count}, 编辑器={editor_count}, 发布按钮={publish_btn_count}")
+            
+            if title_input_count > 0 or (editor_count > 0 and publish_btn_count > 0):
+                upload_complete = True
+                print(f"[{self.platform_name}] 视频上传完成!")
                 break
         
+        if not upload_complete:
+            screenshot_path = f"debug_upload_timeout_{self.platform_name}.png"
+            await self.page.screenshot(path=screenshot_path)
+            raise Exception(f"视频上传超时(截图: {screenshot_path})")
+        
+        await asyncio.sleep(2)
+        
         self.report_progress(60, "正在填写笔记信息...")
+        print(f"[{self.platform_name}] 填写标题: {params.title[:20]}")
         
         # 填写标题
+        title_filled = False
         title_selectors = [
             'input[placeholder*="标题"]',
+            'input[placeholder*="填写标题"]',
             '[class*="title"] input',
+            '.c-input_inner',
         ]
         for selector in title_selectors:
             title_input = self.page.locator(selector).first
             if await title_input.count() > 0:
+                await title_input.click()
+                await title_input.fill('')  # 先清空
                 await title_input.fill(params.title[:20])
+                title_filled = True
+                print(f"[{self.platform_name}] 标题已填写,使用选择器: {selector}")
                 break
         
+        if not title_filled:
+            print(f"[{self.platform_name}] 警告: 未找到标题输入框")
+        
         # 填写描述和标签
         if params.description or params.tags:
+            desc_filled = False
             desc_selectors = [
+                '[class*="ql-editor"]',
                 '[class*="content-input"] [contenteditable="true"]',
                 '[class*="editor"] [contenteditable="true"]',
+                '.ql-editor',
             ]
             for selector in desc_selectors:
                 desc_input = self.page.locator(selector).first
                 if await desc_input.count() > 0:
                     await desc_input.click()
+                    await asyncio.sleep(0.5)
+                    
                     if params.description:
-                        await self.page.keyboard.type(params.description, delay=30)
+                        await self.page.keyboard.type(params.description, delay=20)
+                        print(f"[{self.platform_name}] 描述已填写")
+                    
                     if params.tags:
+                        # 添加标签
                         await self.page.keyboard.press("Enter")
-                        for tag in params.tags:
-                            await self.page.keyboard.type(f"#{tag} ", delay=30)
+                        for tag in params.tags[:5]:  # 最多5个标签
+                            await self.page.keyboard.type(f"#{tag}", delay=20)
+                            await asyncio.sleep(0.3)
+                            await self.page.keyboard.press("Space")
+                        print(f"[{self.platform_name}] 标签已填写: {params.tags[:5]}")
+                    
+                    desc_filled = True
                     break
+            
+            if not desc_filled:
+                print(f"[{self.platform_name}] 警告: 未找到描述输入框")
         
+        await asyncio.sleep(2)
         self.report_progress(80, "正在发布...")
         
         await asyncio.sleep(2)
         
+        # 滚动到页面底部确保发布按钮可见
+        await self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
+        await asyncio.sleep(1)
+        
+        print(f"[{self.platform_name}] 查找发布按钮...")
+        
         # 点击发布
         publish_selectors = [
             'button.publishBtn',
             '.publishBtn',
-            'button:has-text("发布")',
+            'button.d-button.red',
+            'button:has-text("发布"):not(:has-text("定时发布"))',
+            '[class*="publish"][class*="btn"]',
         ]
         
+        publish_clicked = False
         for selector in publish_selectors:
-            btn = self.page.locator(selector).first
-            if await btn.count() > 0 and await btn.is_visible():
-                box = await btn.bounding_box()
-                if box:
-                    await self.page.mouse.click(box['x'] + box['width']/2, box['y'] + box['height']/2)
+            try:
+                btn = self.page.locator(selector).first
+                if await btn.count() > 0:
+                    is_visible = await btn.is_visible()
+                    is_enabled = await btn.is_enabled()
+                    print(f"[{self.platform_name}] 按钮 {selector}: visible={is_visible}, enabled={is_enabled}")
+                    
+                    if is_visible and is_enabled:
+                        box = await btn.bounding_box()
+                        if box:
+                            print(f"[{self.platform_name}] 点击发布按钮: {selector}, 位置: ({box['x']}, {box['y']})")
+                            # 使用真实鼠标点击
+                            await self.page.mouse.click(box['x'] + box['width']/2, box['y'] + box['height']/2)
+                            publish_clicked = True
+                            break
+            except Exception as e:
+                print(f"[{self.platform_name}] 选择器 {selector} 错误: {e}")
+        
+        if not publish_clicked:
+            # 保存截图用于调试
+            screenshot_path = f"debug_publish_failed_{self.platform_name}.png"
+            await self.page.screenshot(path=screenshot_path, full_page=True)
+            print(f"[{self.platform_name}] 未找到发布按钮,截图保存到: {screenshot_path}")
+            
+            # 打印页面 HTML 结构用于调试
+            buttons = await self.page.query_selector_all('button')
+            print(f"[{self.platform_name}] 页面上共有 {len(buttons)} 个按钮")
+            for i, btn in enumerate(buttons[:10]):
+                text = await btn.text_content() or ''
+                cls = await btn.get_attribute('class') or ''
+                print(f"  按钮 {i}: text='{text.strip()[:30]}', class='{cls[:50]}'")
+            
+            raise Exception("未找到发布按钮")
+        
+        print(f"[{self.platform_name}] 已点击发布按钮,等待发布完成...")
+        self.report_progress(90, "等待发布结果...")
+        
+        # 等待发布完成(检测 URL 变化或成功提示)
+        publish_success = False
+        for i in range(20):  # 最多等待 20 秒
+            await asyncio.sleep(1)
+            
+            current_url = self.page.url
+            
+            # 检查是否跳转到发布成功页面或内容管理页面
+            if "published=true" in current_url or "success" in current_url or "content" in current_url:
+                publish_success = True
+                print(f"[{self.platform_name}] 发布成功! 跳转到: {current_url}")
+                break
+            
+            # 检查是否有成功提示
+            try:
+                success_msg = await self.page.locator('[class*="success"], .toast-success, [class*="Toast"]').first.is_visible()
+                if success_msg:
+                    publish_success = True
+                    print(f"[{self.platform_name}] 检测到成功提示!")
                     break
-        
-        await asyncio.sleep(5)
+            except:
+                pass
+            
+            # 检查是否有错误提示
+            try:
+                error_elements = self.page.locator('[class*="error"], .toast-error, [class*="fail"]')
+                if await error_elements.count() > 0:
+                    error_text = await error_elements.first.text_content()
+                    if error_text and len(error_text.strip()) > 0:
+                        raise Exception(f"发布失败: {error_text.strip()}")
+            except Exception as e:
+                if "发布失败" in str(e):
+                    raise
+        
+        # 如果没有明确的成功标志,保存截图
+        if not publish_success:
+            final_url = self.page.url
+            print(f"[{self.platform_name}] 发布结果不确定,当前 URL: {final_url}")
+            screenshot_path = f"debug_publish_result_{self.platform_name}.png"
+            await self.page.screenshot(path=screenshot_path, full_page=True)
+            print(f"[{self.platform_name}] 截图保存到: {screenshot_path}")
+            
+            # 如果 URL 还是发布页面,可能发布失败
+            if "publish/publish" in final_url:
+                raise Exception(f"发布可能失败,仍停留在发布页面(截图: {screenshot_path})")
         
         self.report_progress(100, "发布完成")
+        print(f"[{self.platform_name}] Playwright 方式发布完成!")
         
         return PublishResult(
             success=True,

+ 15 - 2
server/src/automation/platforms/douyin.ts

@@ -1,4 +1,5 @@
 /// <reference lib="dom" />
+import path from 'path';
 import { BasePlatformAdapter } from './base.js';
 import type {
   AccountProfile,
@@ -15,6 +16,9 @@ import { logger } from '../../utils/logger.js';
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
 
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
 /**
  * 抖音平台适配器
  */
@@ -750,6 +754,15 @@ export class DouyinAdapter extends BasePlatformAdapter {
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
+        ? params.videoPath 
+        : path.resolve(SERVER_ROOT, params.videoPath);
+      
+      const absoluteCoverPath = params.coverPath 
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
       const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
         method: 'POST',
         headers: {
@@ -760,8 +773,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
           cookie: cookies,
           title: params.title,
           description: params.description || params.title,
-          video_path: params.videoPath,
-          cover_path: params.coverPath,
+          video_path: absoluteVideoPath,
+          cover_path: absoluteCoverPath,
           tags: params.tags || [],
           post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
           location: params.location || '重庆市',

+ 15 - 2
server/src/automation/platforms/kuaishou.ts

@@ -1,3 +1,4 @@
+import path from 'path';
 import { BasePlatformAdapter } from './base.js';
 import type {
   AccountProfile,
@@ -13,6 +14,9 @@ import { logger } from '../../utils/logger.js';
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
 
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
 /**
  * 快手平台适配器
  */
@@ -154,6 +158,15 @@ export class KuaishouAdapter extends BasePlatformAdapter {
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
+        ? params.videoPath 
+        : path.resolve(SERVER_ROOT, params.videoPath);
+      
+      const absoluteCoverPath = params.coverPath 
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
       const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
         method: 'POST',
         headers: {
@@ -164,8 +177,8 @@ export class KuaishouAdapter extends BasePlatformAdapter {
           cookie: cookies,
           title: params.title,
           description: params.description || params.title,
-          video_path: params.videoPath,
-          cover_path: params.coverPath,
+          video_path: absoluteVideoPath,
+          cover_path: absoluteCoverPath,
           tags: params.tags || [],
           post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
           location: params.location || '重庆市',

+ 15 - 2
server/src/automation/platforms/weixin.ts

@@ -1,4 +1,5 @@
 /// <reference lib="dom" />
+import path from 'path';
 import { BasePlatformAdapter } from './base.js';
 import type {
   AccountProfile,
@@ -14,6 +15,9 @@ import { logger } from '../../utils/logger.js';
 // Python 多平台发布服务配置
 const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
 
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
 /**
  * 微信视频号平台适配器
  * 参考: matrix/tencent_uploader/main.py
@@ -175,6 +179,15 @@ export class WeixinAdapter extends BasePlatformAdapter {
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
+        ? params.videoPath 
+        : path.resolve(SERVER_ROOT, params.videoPath);
+      
+      const absoluteCoverPath = params.coverPath 
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
       const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
         method: 'POST',
         headers: {
@@ -185,8 +198,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
           cookie: cookies,
           title: params.title,
           description: params.description || params.title,
-          video_path: params.videoPath,
-          cover_path: params.coverPath,
+          video_path: absoluteVideoPath,
+          cover_path: absoluteCoverPath,
           tags: params.tags || [],
           post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
           location: params.location || '重庆市',

+ 34 - 9
server/src/automation/platforms/xiaohongshu.ts

@@ -1,4 +1,5 @@
 /// <reference lib="dom" />
+import path from 'path';
 import { BasePlatformAdapter } from './base.js';
 import type {
   AccountProfile,
@@ -15,6 +16,9 @@ import { logger } from '../../utils/logger.js';
 // 小红书 Python API 服务配置
 const XHS_PYTHON_SERVICE_URL = process.env.XHS_SERVICE_URL || 'http://localhost:5005';
 
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
 /**
  * 小红书平台适配器
  */
@@ -388,24 +392,45 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
 
       onProgress?.(10, '正在上传视频...');
 
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
+        ? params.videoPath 
+        : path.resolve(SERVER_ROOT, params.videoPath);
+      
+      const absoluteCoverPath = params.coverPath 
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
+      const requestBody = {
+        platform: 'xiaohongshu',
+        cookie: cookieStr,
+        title: params.title,
+        description: params.description || params.title,
+        video_path: absoluteVideoPath,
+        cover_path: absoluteCoverPath,
+        tags: params.tags || [],
+        post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+      };
+
+      logger.info('[Xiaohongshu API] Request body:', {
+        platform: requestBody.platform,
+        title: requestBody.title,
+        video_path: requestBody.video_path,
+        has_cookie: !!requestBody.cookie,
+        cookie_length: requestBody.cookie?.length || 0,
+      });
+
       const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/publish`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
-        body: JSON.stringify({
-          cookie: cookieStr,
-          title: params.title,
-          description: params.description || params.title,
-          video_path: params.videoPath,
-          cover_path: params.coverPath,
-          topics: params.tags || [],
-          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
-        }),
+        body: JSON.stringify(requestBody),
         signal: AbortSignal.timeout(300000), // 5分钟超时
       });
 
       const result = await response.json();
+      logger.info('[Xiaohongshu API] Response:', result);
       
       if (result.success) {
         onProgress?.(100, '发布成功');