Ethanfly 1 месяц назад
Родитель
Сommit
6a167646a2

+ 71 - 6
client/src/api/request.ts

@@ -36,6 +36,22 @@ request.interceptors.request.use(
   }
 );
 
+// 是否正在刷新 token
+let isRefreshing = false;
+// 等待刷新的请求队列
+let refreshSubscribers: ((token: string) => void)[] = [];
+
+// 通知所有等待的请求
+function onRefreshed(token: string) {
+  refreshSubscribers.forEach(callback => callback(token));
+  refreshSubscribers = [];
+}
+
+// 添加到等待队列
+function addRefreshSubscriber(callback: (token: string) => void) {
+  refreshSubscribers.push(callback);
+}
+
 // 响应拦截器
 request.interceptors.response.use(
   (response: AxiosResponse<ApiResponse>) => {
@@ -52,23 +68,72 @@ request.interceptors.response.use(
   async (error) => {
     const originalRequest = error.config;
     
-    // Token 过期,尝试刷新
-    if (error.response?.status === 401 && !originalRequest._retry) {
+    // 排除不需要刷新 token 的请求
+    const isAuthRequest = originalRequest.url?.includes('/api/auth/refresh') 
+      || originalRequest.url?.includes('/api/auth/login')
+      || originalRequest.url?.includes('/api/auth/register');
+    
+    // Token 过期,尝试刷新(排除认证相关请求)
+    if (error.response?.status === 401 && !originalRequest._retry && !isAuthRequest) {
       originalRequest._retry = true;
       
+      // 如果已经在刷新中,将请求加入队列等待
+      if (isRefreshing) {
+        return new Promise((resolve) => {
+          addRefreshSubscriber((token: string) => {
+            originalRequest.headers.Authorization = `Bearer ${token}`;
+            resolve(request(originalRequest));
+          });
+        });
+      }
+      
+      isRefreshing = true;
       const authStore = useAuthStore();
-      const refreshed = await authStore.refreshAccessToken();
       
-      if (refreshed) {
-        return request(originalRequest);
+      try {
+        const refreshed = await authStore.refreshAccessToken();
+        
+        if (refreshed) {
+          const newToken = authStore.accessToken!;
+          onRefreshed(newToken);
+          originalRequest.headers.Authorization = `Bearer ${newToken}`;
+          return request(originalRequest);
+        }
+      } catch {
+        // 刷新失败
+      } finally {
+        isRefreshing = false;
       }
       
-      // 刷新失败,跳转登录
+      // 刷新失败,清除等待队列并跳转登录
+      refreshSubscribers = [];
+      authStore.clearTokens();
       ElMessage.error('登录已过期,请重新登录');
       router.push('/login');
       return Promise.reject(error);
     }
     
+    // 认证请求失败(login/register 等)
+    if (isAuthRequest) {
+      // 获取错误消息
+      const message = error.response?.data?.error?.message 
+        || error.response?.data?.message 
+        || error.message 
+        || '认证失败';
+      
+      // 显示错误消息
+      ElMessage.error(message);
+      
+      // refresh 请求失败才清除 token 并跳转
+      if (originalRequest.url?.includes('/api/auth/refresh')) {
+        const authStore = useAuthStore();
+        authStore.clearTokens();
+        router.push('/login');
+      }
+      
+      return Promise.reject(error);
+    }
+    
     // 其他错误
     const message = error.response?.data?.error?.message 
       || error.response?.data?.message 

+ 154 - 12
client/src/stores/taskQueue.ts

@@ -16,6 +16,9 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
   // 作品刷新信号(当 sync_works 任务完成时递增)
   const worksRefreshTrigger = ref(0);
   
+  // 账号刷新信号(当 sync_account 任务完成时递增)
+  const accountRefreshTrigger = ref(0);
+  
   // 验证码相关状态
   const showCaptchaDialog = ref(false);
   const captchaTaskId = ref('');
@@ -39,28 +42,75 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
   // 获取任务类型配置
   const getTaskConfig = (type: TaskType) => TASK_TYPE_CONFIG[type];
 
+  // WebSocket 重连计时器
+  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
+  let reconnectAttempts = 0;
+  const maxReconnectAttempts = 10;
+  
+  // 轮询任务状态(当 WebSocket 不可用时)
+  const pollingTimers = new Map<string, ReturnType<typeof setTimeout>>();
+  
+  function stopTaskPolling(taskId: string) {
+    const timer = pollingTimers.get(taskId);
+    if (timer) {
+      clearTimeout(timer);
+      pollingTimers.delete(taskId);
+    }
+  }
+  
+  function stopAllPolling() {
+    pollingTimers.forEach((timer) => clearTimeout(timer));
+    pollingTimers.clear();
+  }
+  
   // WebSocket 连接
   function connectWebSocket() {
     const authStore = useAuthStore();
     const serverStore = useServerStore();
     
+    // 清除之前的重连计时器
+    if (reconnectTimer) {
+      clearTimeout(reconnectTimer);
+      reconnectTimer = null;
+    }
+    
     if (!authStore.accessToken) {
-      console.warn('[TaskWS] No token available');
+      console.warn('[TaskWS] No token available, will retry in 2s');
+      // 没有 token 时延迟重试
+      reconnectTimer = setTimeout(connectWebSocket, 2000);
       return;
     }
 
+    // 如果已经连接,不重复连接
     if (ws.value && ws.value.readyState === WebSocket.OPEN) {
+      console.log('[TaskWS] Already connected');
+      return;
+    }
+    
+    // 如果正在连接中,等待
+    if (ws.value && ws.value.readyState === WebSocket.CONNECTING) {
+      console.log('[TaskWS] Connection in progress...');
       return;
     }
 
     const serverUrl = serverStore.currentServer?.url || 'http://localhost:3000';
     const wsUrl = serverUrl.replace(/^http/, 'ws') + '/ws';
+    
+    console.log('[TaskWS] Connecting to:', wsUrl);
 
     try {
+      // 关闭旧连接
+      if (ws.value) {
+        ws.value.onclose = null; // 防止触发重连
+        ws.value.close();
+        ws.value = null;
+      }
+      
       ws.value = new WebSocket(wsUrl);
 
       ws.value.onopen = () => {
-        console.log('[TaskWS] Connected');
+        console.log('[TaskWS] Connected, authenticating...');
+        reconnectAttempts = 0; // 重置重连计数
         ws.value?.send(JSON.stringify({ 
           type: 'auth', 
           payload: { token: authStore.accessToken } 
@@ -70,32 +120,63 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
       ws.value.onmessage = (event) => {
         try {
           const data = JSON.parse(event.data);
+          console.log('[TaskWS] Received:', data.type || data.payload?.event);
           handleWebSocketMessage(data);
         } catch (e) {
           console.error('[TaskWS] Parse error:', e);
         }
       };
 
-      ws.value.onclose = () => {
-        console.log('[TaskWS] Disconnected');
+      ws.value.onclose = (event) => {
+        console.log('[TaskWS] Disconnected, code:', event.code, 'reason:', event.reason);
         wsConnected.value = false;
-        // 5秒后重连
-        setTimeout(connectWebSocket, 5000);
+        ws.value = null;
+        
+        // 只有在正常情况下才重连(不是主动关闭)
+        if (reconnectAttempts < maxReconnectAttempts) {
+          reconnectAttempts++;
+          const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), 30000); // 指数退避,最大 30 秒
+          console.log(`[TaskWS] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
+          reconnectTimer = setTimeout(connectWebSocket, delay);
+        } else {
+          console.error('[TaskWS] Max reconnect attempts reached');
+        }
       };
 
-      ws.value.onerror = () => {
-        ws.value?.close();
+      ws.value.onerror = (error) => {
+        console.error('[TaskWS] Error:', error);
       };
     } catch (e) {
       console.error('[TaskWS] Setup error:', e);
+      // 出错后也尝试重连
+      reconnectTimer = setTimeout(connectWebSocket, 5000);
     }
   }
 
   function disconnectWebSocket() {
+    // 清除重连计时器
+    if (reconnectTimer) {
+      clearTimeout(reconnectTimer);
+      reconnectTimer = null;
+    }
+    reconnectAttempts = maxReconnectAttempts; // 防止自动重连
+    
+    // 停止所有轮询
+    stopAllPolling();
+    
     if (ws.value) {
+      ws.value.onclose = null; // 防止触发重连逻辑
       ws.value.close();
       ws.value = null;
     }
+    wsConnected.value = false;
+  }
+  
+  // 强制重连
+  function reconnectWebSocket() {
+    reconnectAttempts = 0;
+    disconnectWebSocket();
+    setTimeout(connectWebSocket, 100);
   }
 
   // 处理 WebSocket 消息
@@ -144,10 +225,17 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
         const task = data.payload?.task as Task;
         if (task) {
           updateTask(task);
-          // 如果是 sync_works 任务完成,触发作品列表刷新
-          if (event === 'completed' && task.type === 'sync_works') {
-            worksRefreshTrigger.value++;
-            console.log('[TaskQueue] sync_works completed, triggering refresh');
+          if (event === 'completed') {
+            // 如果是 sync_works 任务完成,触发作品列表刷新
+            if (task.type === 'sync_works') {
+              worksRefreshTrigger.value++;
+              console.log('[TaskQueue] sync_works completed, triggering refresh');
+            }
+            // 如果是 sync_account 任务完成,触发账号列表刷新
+            if (task.type === 'sync_account') {
+              accountRefreshTrigger.value++;
+              console.log('[TaskQueue] sync_account completed, triggering refresh');
+            }
           }
         }
         break;
@@ -271,12 +359,63 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
         tasks.value.unshift(task);
       }
       
+      // 如果 WebSocket 未连接,启动轮询
+      if (!wsConnected.value && task) {
+        console.log('[TaskQueue] WebSocket not connected, starting polling for task:', task.id);
+        startTaskPolling(task.id);
+      }
+      
       return task;
     } catch (e) {
       console.error('Failed to create task:', e);
       return null;
     }
   }
+  
+  function startTaskPolling(taskId: string) {
+    // 防止重复轮询
+    if (pollingTimers.has(taskId)) return;
+    
+    const poll = async () => {
+      // 如果 WebSocket 已连接,停止轮询
+      if (wsConnected.value) {
+        stopTaskPolling(taskId);
+        return;
+      }
+      
+      try {
+        await fetchTasks();
+        const task = tasks.value.find(t => t.id === taskId);
+        
+        // 如果任务已完成,停止轮询
+        if (task && (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled')) {
+          console.log('[TaskQueue] Task completed via polling:', taskId, 'status:', task.status);
+          if (task.status === 'completed') {
+            // 触发作品刷新
+            if (task.type === 'sync_works') {
+              worksRefreshTrigger.value++;
+            }
+            // 触发账号刷新
+            if (task.type === 'sync_account') {
+              accountRefreshTrigger.value++;
+            }
+          }
+          stopTaskPolling(taskId);
+          return;
+        }
+        
+        // 继续轮询
+        pollingTimers.set(taskId, setTimeout(poll, 2000));
+      } catch (e) {
+        console.error('[TaskQueue] Polling error:', e);
+        // 出错后继续轮询
+        pollingTimers.set(taskId, setTimeout(poll, 5000));
+      }
+    };
+    
+    // 立即开始轮询
+    poll();
+  }
 
   async function cancelTask(taskId: string): Promise<boolean> {
     try {
@@ -347,6 +486,7 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
     isDialogVisible,
     wsConnected,
     worksRefreshTrigger,
+    accountRefreshTrigger,
     // 验证码状态
     showCaptchaDialog,
     captchaTaskId,
@@ -362,6 +502,7 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
     getTaskConfig,
     connectWebSocket,
     disconnectWebSocket,
+    reconnectWebSocket,
     fetchTasks,
     createTask,
     cancelTask,
@@ -372,6 +513,7 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
     closeDialog,
     toggleDialog,
     clearCompletedTasks,
+    stopAllPolling,
     // 验证码方法
     submitCaptcha,
     cancelCaptcha,

+ 8 - 2
client/src/views/Accounts/index.vue

@@ -344,9 +344,15 @@ watch(() => taskStore.tasks, (newTasks, oldTasks) => {
   }
 }, { deep: true });
 
-// 监听账号刷新信号(当浏览器登录添加账号后触发)
+// 监听账号刷新信号(当浏览器登录添加账号后或账号同步任务完成时触发)
 watch(() => tabsStore.accountRefreshTrigger, () => {
-  console.log('[Accounts] Account refresh triggered, reloading list...');
+  console.log('[Accounts] Account refresh triggered (from tabs), reloading list...');
+  loadAccounts();
+});
+
+// 监听任务队列的账号刷新信号
+watch(() => taskStore.accountRefreshTrigger, () => {
+  console.log('[Accounts] Account refresh triggered (from task), reloading list...');
   loadAccounts();
 });
 

+ 12 - 2
client/src/views/Works/index.vue

@@ -69,7 +69,7 @@
           @click="openWorkDetail(work)"
         >
           <div class="work-cover">
-            <img :src="work.coverUrl" :alt="work.title" @error="handleImageError" />
+            <img :src="getSecureCoverUrl(work.coverUrl)" :alt="work.title" @error="handleImageError" />
             <span class="work-duration">{{ work.duration }}</span>
             <el-tag 
               class="work-status" 
@@ -194,7 +194,7 @@
       destroy-on-close
     >
       <div class="comments-drawer-header" v-if="commentsWork">
-        <img :src="commentsWork.coverUrl" class="work-thumb" @error="handleImageError" />
+        <img :src="getSecureCoverUrl(commentsWork.coverUrl)" class="work-thumb" @error="handleImageError" />
         <div class="work-brief">
           <div class="work-brief-title">{{ commentsWork.title || '无标题' }}</div>
           <div class="work-brief-meta">
@@ -478,6 +478,16 @@ function formatNumber(num: number) {
   return num?.toString() || '0';
 }
 
+// 将 HTTP 图片 URL 转换为 HTTPS(小红书等平台的图片 URL 可能是 HTTP)
+function getSecureCoverUrl(url: string): string {
+  if (!url) return '';
+  // 将 http:// 转换为 https://
+  if (url.startsWith('http://')) {
+    return url.replace('http://', 'https://');
+  }
+  return url;
+}
+
 function handleImageError(e: Event) {
   const img = e.target as HTMLImageElement;
   img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="%23f0f0f0" width="100" height="100"/><text x="50" y="55" text-anchor="middle" fill="%23999" font-size="12">无封面</text></svg>';

+ 342 - 0
server/python/README.md

@@ -0,0 +1,342 @@
+# 多平台视频发布服务
+
+基于 [matrix](https://github.com/kebenxiaoming/matrix) 项目,使用 Python + Playwright 实现多平台视频发布功能。
+
+## 支持平台
+
+| 平台 | 模块 | 发布方式 | 说明 |
+|------|------|----------|------|
+| 抖音 | `platforms/douyin.py` | Playwright | 浏览器自动化 |
+| 小红书 | `platforms/xiaohongshu.py` | API + Playwright | 优先使用 xhs SDK,更稳定 |
+| 视频号 | `platforms/weixin.py` | Playwright | 需要 Chrome 浏览器 |
+| 快手 | `platforms/kuaishou.py` | Playwright | 浏览器自动化 |
+
+## 项目结构
+
+```
+server/python/
+├── app.py                  # Flask 统一入口
+├── requirements.txt        # 依赖文件
+├── README.md              # 说明文档
+├── platforms/             # 平台发布模块
+│   ├── __init__.py        # 模块入口
+│   ├── base.py            # 发布器基类
+│   ├── douyin.py          # 抖音发布器
+│   ├── xiaohongshu.py     # 小红书发布器
+│   ├── weixin.py          # 视频号发布器
+│   └── kuaishou.py        # 快手发布器
+└── utils/                 # 工具模块
+    ├── __init__.py
+    └── helpers.py         # 工具函数
+```
+
+## 环境要求
+
+- Python 3.8 或更高版本
+- Windows 10+, macOS 12+, 或 Linux (Ubuntu 20.04+)
+
+## 安装步骤
+
+### 1. 创建虚拟环境
+
+#### Windows (PowerShell)
+
+```powershell
+# 进入 python 目录
+cd server\python
+
+# 创建虚拟环境
+python -m venv venv
+
+# 激活虚拟环境
+.\venv\Scripts\Activate.ps1
+
+# 如果遇到执行策略问题,先运行:
+# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+```
+
+#### Windows (CMD)
+
+```cmd
+cd server\python
+python -m venv venv
+venv\Scripts\activate.bat
+```
+
+#### Linux / macOS
+
+```bash
+cd server/python
+
+# 创建虚拟环境
+python3 -m venv venv
+
+# 激活虚拟环境
+source venv/bin/activate
+```
+
+### 2. 安装依赖
+
+```bash
+# 确保已激活虚拟环境
+pip install -r requirements.txt
+
+# 安装 Playwright 浏览器
+playwright install chromium
+
+# 如果在 Linux 服务器上,还需要安装系统依赖
+playwright install-deps chromium
+```
+
+### 3. 启动服务
+
+```bash
+# 默认启动(headless 模式,端口 5005)
+python app.py
+
+# 指定端口
+python app.py --port 8080
+
+# 显示浏览器窗口(调试用)
+python app.py --headless false
+
+# 调试模式
+python app.py --debug
+```
+
+## API 接口
+
+### 健康检查
+
+```
+GET /health
+
+响应:
+{
+  "status": "ok",
+  "xhs_sdk": true,
+  "supported_platforms": ["douyin", "xiaohongshu", "weixin", "kuaishou"],
+  "headless_mode": true
+}
+```
+
+### 发布视频
+
+```
+POST /publish
+Content-Type: application/json
+
+{
+  "platform": "douyin",              // douyin | xiaohongshu | weixin | kuaishou
+  "cookie": "cookie字符串或JSON数组",
+  "title": "视频标题",
+  "description": "视频描述(可选)",
+  "video_path": "E:/videos/test.mp4", // 视频绝对路径
+  "cover_path": "E:/images/cover.jpg", // 封面绝对路径(可选)
+  "tags": ["话题1", "话题2"],
+  "post_time": "2024-01-20 12:00:00",  // 定时发布(可选)
+  "location": "重庆市"                   // 位置(可选)
+}
+
+响应:
+{
+  "success": true,
+  "platform": "douyin",
+  "video_id": "xxx",
+  "video_url": "xxx",
+  "message": "发布成功"
+}
+```
+
+### 批量发布(多平台)
+
+```
+POST /publish/batch
+Content-Type: application/json
+
+{
+  "platforms": ["douyin", "xiaohongshu"],
+  "cookies": {
+    "douyin": "cookie字符串",
+    "xiaohongshu": "cookie字符串"
+  },
+  "title": "视频标题",
+  "video_path": "E:/videos/test.mp4",
+  "tags": ["话题1", "话题2"]
+}
+
+响应:
+{
+  "success": true,
+  "total": 2,
+  "success_count": 2,
+  "fail_count": 0,
+  "results": [
+    {"platform": "douyin", "success": true, "message": "发布成功"},
+    {"platform": "xiaohongshu", "success": true, "message": "发布成功"}
+  ]
+}
+```
+
+### 小红书签名
+
+```
+POST /sign
+Content-Type: application/json
+
+{
+  "uri": "API路径",
+  "data": "请求数据",
+  "a1": "a1 cookie",
+  "web_session": "web_session cookie"
+}
+
+响应:
+{
+  "x-s": "签名值",
+  "x-t": "时间戳"
+}
+```
+
+### 检查 Cookie
+
+```
+POST /check_cookie
+Content-Type: application/json
+
+{
+  "platform": "xiaohongshu",
+  "cookie": "cookie字符串"
+}
+
+响应:
+{
+  "valid": true,
+  "user_info": {
+    "user_id": "xxx",
+    "nickname": "昵称",
+    "avatar": "头像URL"
+  }
+}
+```
+
+## 扩展新平台
+
+如需添加新平台,请按以下步骤:
+
+1. 在 `platforms/` 目录创建新文件,如 `bilibili.py`
+2. 继承 `BasePublisher` 基类
+3. 实现 `publish` 方法
+4. 在 `platforms/__init__.py` 中注册
+
+示例:
+
+```python
+# platforms/bilibili.py
+from .base import BasePublisher, PublishParams, PublishResult
+
+class BilibiliPublisher(BasePublisher):
+    platform_name = "bilibili"
+    login_url = "https://account.bilibili.com/"
+    publish_url = "https://member.bilibili.com/platform/upload/video/frame"
+    cookie_domain = ".bilibili.com"
+    
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        # 实现发布逻辑
+        ...
+```
+
+```python
+# platforms/__init__.py 添加
+from .bilibili import BilibiliPublisher
+
+PLATFORM_MAP = {
+    ...
+    'bilibili': BilibiliPublisher,
+}
+```
+
+## 与 Node.js 服务集成
+
+在 Node.js 服务中设置环境变量:
+
+```bash
+# .env 文件
+PYTHON_PUBLISH_SERVICE_URL=http://localhost:5005
+```
+
+## 部署建议
+
+### PM2 管理
+
+```bash
+pm2 start app.py --name "publish-service" --interpreter python
+pm2 logs publish-service
+pm2 startup && pm2 save
+```
+
+### Supervisor (Linux)
+
+```ini
+[program:publish-service]
+directory=/path/to/server/python
+command=/path/to/venv/bin/python app.py
+autostart=true
+autorestart=true
+environment=HEADLESS="true"
+```
+
+### Docker
+
+```dockerfile
+FROM python:3.11-slim
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+RUN playwright install chromium && playwright install-deps chromium
+
+COPY . .
+EXPOSE 5005
+CMD ["python", "app.py"]
+```
+
+## 常见问题
+
+### 视频号需要 Chrome 浏览器
+
+视频号使用 Chromium 可能出现 H264 编码错误,已在代码中配置使用 Chrome:
+
+```python
+browser = await playwright.chromium.launch(headless=self.headless, channel="chrome")
+```
+
+### 小红书 SDK 安装
+
+```bash
+pip install xhs -i https://pypi.tuna.tsinghua.edu.cn/simple
+```
+
+### Cookie 格式
+
+支持两种格式:
+
+```json
+// JSON 数组格式
+[
+  {"name": "sessionid", "value": "xxx", "domain": ".douyin.com"},
+  {"name": "token", "value": "xxx", "domain": ".douyin.com"}
+]
+
+// 字符串格式
+"sessionid=xxx; token=xxx"
+```
+
+## 参考项目
+
+- [matrix](https://github.com/kebenxiaoming/matrix) - 视频矩阵内容分发系统
+- [xhs](https://github.com/ReaJason/xhs) - 小红书 Python SDK
+
+## License
+
+MIT

+ 371 - 0
server/python/app.py

@@ -0,0 +1,371 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+多平台视频发布服务 - 统一入口
+支持平台: 抖音、小红书、视频号、快手
+
+参考项目: matrix (https://github.com/kebenxiaoming/matrix)
+
+使用方式:
+    python app.py                    # 启动 HTTP 服务 (端口 5005)
+    python app.py --port 8080        # 指定端口
+    python app.py --headless false   # 显示浏览器窗口
+"""
+
+import asyncio
+import os
+import argparse
+import traceback
+from datetime import datetime
+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
+
+# 创建 Flask 应用
+app = Flask(__name__)
+CORS(app)
+
+# 全局配置
+HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
+
+# ==================== 签名相关(小红书专用) ====================
+
+@app.route("/sign", methods=["POST"])
+def sign_endpoint():
+    """小红书签名接口"""
+    try:
+        from platforms.xiaohongshu import XiaohongshuPublisher
+        
+        data = request.json
+        publisher = XiaohongshuPublisher(headless=True)
+        result = asyncio.run(publisher.get_sign(
+            data.get("uri", ""),
+            data.get("data"),
+            data.get("a1", ""),
+            data.get("web_session", "")
+        ))
+        return jsonify(result)
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({"error": str(e)}), 500
+
+
+# ==================== 统一发布接口 ====================
+
+@app.route("/publish", methods=["POST"])
+def publish_video():
+    """
+    统一发布接口
+    
+    请求体:
+    {
+        "platform": "douyin",           # douyin | xiaohongshu | weixin | kuaishou
+        "cookie": "cookie字符串或JSON",
+        "title": "视频标题",
+        "description": "视频描述(可选)",
+        "video_path": "视频文件绝对路径",
+        "cover_path": "封面图片绝对路径(可选)",
+        "tags": ["话题1", "话题2"],
+        "post_time": "定时发布时间(可选,格式:2024-01-20 12:00:00)",
+        "location": "位置(可选,默认:重庆市)"
+    }
+    
+    响应:
+    {
+        "success": true,
+        "platform": "douyin",
+        "video_id": "xxx",
+        "video_url": "xxx",
+        "message": "发布成功"
+    }
+    """
+    try:
+        data = request.json
+        
+        # 获取参数
+        platform = data.get("platform", "").lower()
+        cookie_str = data.get("cookie", "")
+        title = data.get("title", "")
+        description = data.get("description", "")
+        video_path = data.get("video_path", "")
+        cover_path = data.get("cover_path")
+        tags = data.get("tags", [])
+        post_time = data.get("post_time")
+        location = data.get("location", "重庆市")
+        
+        # 参数验证
+        if not platform:
+            return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
+        if platform not in PLATFORM_MAP:
+            return jsonify({
+                "success": False, 
+                "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
+            }), 400
+        if not cookie_str:
+            return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
+        if not title:
+            return jsonify({"success": False, "error": "缺少 title 参数"}), 400
+        if not 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
+        
+        # 解析发布时间
+        publish_date = parse_datetime(post_time) if post_time else None
+        
+        # 创建发布参数
+        params = PublishParams(
+            title=title,
+            video_path=video_path,
+            description=description,
+            cover_path=cover_path,
+            tags=tags,
+            publish_date=publish_date,
+            location=location
+        )
+        
+        print("=" * 60)
+        print(f"[Publish] 平台: {platform}")
+        print(f"[Publish] 标题: {title}")
+        print(f"[Publish] 视频: {video_path}")
+        print(f"[Publish] 封面: {cover_path}")
+        print(f"[Publish] 话题: {tags}")
+        print(f"[Publish] 定时: {publish_date}")
+        print("=" * 60)
+        
+        # 获取对应平台的发布器
+        PublisherClass = get_publisher(platform)
+        publisher = PublisherClass(headless=HEADLESS_MODE)
+        
+        # 执行发布
+        result = asyncio.run(publisher.run(cookie_str, params))
+        
+        return jsonify({
+            "success": result.success,
+            "platform": result.platform,
+            "video_id": result.video_id,
+            "video_url": result.video_url,
+            "message": result.message,
+            "error": result.error
+        })
+        
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({"success": False, "error": str(e)}), 500
+
+
+# ==================== 批量发布接口 ====================
+
+@app.route("/publish/batch", methods=["POST"])
+def publish_batch():
+    """
+    批量发布接口 - 发布到多个平台
+    
+    请求体:
+    {
+        "platforms": ["douyin", "xiaohongshu"],
+        "cookies": {
+            "douyin": "cookie字符串",
+            "xiaohongshu": "cookie字符串"
+        },
+        "title": "视频标题",
+        "video_path": "视频文件绝对路径",
+        ...
+    }
+    """
+    try:
+        data = request.json
+        
+        platforms = data.get("platforms", [])
+        cookies = data.get("cookies", {})
+        
+        if not platforms:
+            return jsonify({"success": False, "error": "缺少 platforms 参数"}), 400
+        
+        results = []
+        
+        for platform in platforms:
+            platform = platform.lower()
+            cookie_str = cookies.get(platform, "")
+            
+            if not cookie_str:
+                results.append({
+                    "platform": platform,
+                    "success": False,
+                    "error": f"缺少 {platform} 的 cookie"
+                })
+                continue
+            
+            try:
+                # 创建参数
+                params = PublishParams(
+                    title=data.get("title", ""),
+                    video_path=data.get("video_path", ""),
+                    description=data.get("description", ""),
+                    cover_path=data.get("cover_path"),
+                    tags=data.get("tags", []),
+                    publish_date=parse_datetime(data.get("post_time")),
+                    location=data.get("location", "重庆市")
+                )
+                
+                # 发布
+                PublisherClass = get_publisher(platform)
+                publisher = PublisherClass(headless=HEADLESS_MODE)
+                result = asyncio.run(publisher.run(cookie_str, params))
+                
+                results.append({
+                    "platform": result.platform,
+                    "success": result.success,
+                    "video_id": result.video_id,
+                    "message": result.message,
+                    "error": result.error
+                })
+            except Exception as e:
+                results.append({
+                    "platform": platform,
+                    "success": False,
+                    "error": str(e)
+                })
+        
+        # 统计成功/失败数量
+        success_count = sum(1 for r in results if r.get("success"))
+        
+        return jsonify({
+            "success": success_count > 0,
+            "total": len(platforms),
+            "success_count": success_count,
+            "fail_count": len(platforms) - success_count,
+            "results": results
+        })
+        
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({"success": False, "error": str(e)}), 500
+
+
+# ==================== Cookie 验证接口 ====================
+
+@app.route("/check_cookie", methods=["POST"])
+def check_cookie():
+    """检查 cookie 是否有效"""
+    try:
+        data = request.json
+        platform = data.get("platform", "").lower()
+        cookie_str = data.get("cookie", "")
+        
+        if not cookie_str:
+            return jsonify({"valid": False, "error": "缺少 cookie 参数"}), 400
+        
+        # 目前只支持小红书的 cookie 验证
+        if platform == "xiaohongshu":
+            try:
+                from platforms.xiaohongshu import XiaohongshuPublisher, XHS_SDK_AVAILABLE
+                
+                if XHS_SDK_AVAILABLE:
+                    from xhs import XhsClient
+                    publisher = XiaohongshuPublisher()
+                    xhs_client = XhsClient(cookie_str, sign=publisher.sign_sync)
+                    info = xhs_client.get_self_info()
+                    
+                    if info:
+                        return jsonify({
+                            "valid": True,
+                            "user_info": {
+                                "user_id": info.get("user_id"),
+                                "nickname": info.get("nickname"),
+                                "avatar": info.get("images")
+                            }
+                        })
+            except Exception as e:
+                return jsonify({"valid": False, "error": str(e)})
+        
+        # 其他平台返回格式正确但未验证
+        return jsonify({
+            "valid": True, 
+            "message": "Cookie 格式正确,但未进行在线验证"
+        })
+        
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({"valid": False, "error": str(e)})
+
+
+# ==================== 健康检查 ====================
+
+@app.route("/health", methods=["GET"])
+def health_check():
+    """健康检查"""
+    # 检查 xhs SDK 是否可用
+    xhs_available = False
+    try:
+        from platforms.xiaohongshu import XHS_SDK_AVAILABLE
+        xhs_available = XHS_SDK_AVAILABLE
+    except:
+        pass
+    
+    return jsonify({
+        "status": "ok",
+        "xhs_sdk": xhs_available,
+        "supported_platforms": list(PLATFORM_MAP.keys()),
+        "headless_mode": HEADLESS_MODE
+    })
+
+
+@app.route("/", methods=["GET"])
+def index():
+    """首页"""
+    return jsonify({
+        "name": "多平台视频发布服务",
+        "version": "1.0.0",
+        "endpoints": {
+            "GET /": "服务信息",
+            "GET /health": "健康检查",
+            "POST /publish": "发布视频",
+            "POST /publish/batch": "批量发布",
+            "POST /check_cookie": "检查 Cookie",
+            "POST /sign": "小红书签名"
+        },
+        "supported_platforms": list(PLATFORM_MAP.keys())
+    })
+
+
+# ==================== 命令行启动 ====================
+
+def main():
+    parser = argparse.ArgumentParser(description='多平台视频发布服务')
+    parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
+    parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
+    parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
+    parser.add_argument('--debug', action='store_true', help='调试模式')
+    
+    args = parser.parse_args()
+    
+    global HEADLESS_MODE
+    HEADLESS_MODE = args.headless.lower() == 'true'
+    
+    # 检查 xhs SDK
+    xhs_status = "未安装"
+    try:
+        from platforms.xiaohongshu import XHS_SDK_AVAILABLE
+        xhs_status = "已安装" if XHS_SDK_AVAILABLE else "未安装"
+    except:
+        pass
+    
+    print("=" * 60)
+    print("多平台视频发布服务")
+    print("=" * 60)
+    print(f"XHS SDK: {xhs_status}")
+    print(f"Headless 模式: {HEADLESS_MODE}")
+    print(f"支持平台: {', '.join(PLATFORM_MAP.keys())}")
+    print("=" * 60)
+    print(f"启动服务: http://{args.host}:{args.port}")
+    print("=" * 60)
+    
+    app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)
+
+
+if __name__ == '__main__':
+    main()

+ 36 - 0
server/python/platforms/__init__.py

@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""
+平台发布模块
+支持: 抖音、小红书、视频号、快手
+"""
+
+from .base import BasePublisher
+from .douyin import DouyinPublisher
+from .xiaohongshu import XiaohongshuPublisher
+from .weixin import WeixinPublisher
+from .kuaishou import KuaishouPublisher
+
+__all__ = [
+    'BasePublisher',
+    'DouyinPublisher',
+    'XiaohongshuPublisher',
+    'WeixinPublisher',
+    'KuaishouPublisher',
+]
+
+# 平台映射
+PLATFORM_MAP = {
+    'douyin': DouyinPublisher,
+    'xiaohongshu': XiaohongshuPublisher,
+    'weixin': WeixinPublisher,
+    'weixin_video': WeixinPublisher,  # 别名
+    'kuaishou': KuaishouPublisher,
+}
+
+
+def get_publisher(platform: str) -> type:
+    """获取平台发布器类"""
+    publisher_class = PLATFORM_MAP.get(platform.lower())
+    if not publisher_class:
+        raise ValueError(f"不支持的平台: {platform},支持的平台: {list(PLATFORM_MAP.keys())}")
+    return publisher_class

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

@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+"""
+平台发布基类
+提供通用的发布接口和工具方法
+"""
+
+import asyncio
+import json
+import os
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import List, Optional, Callable
+from playwright.async_api import async_playwright, Browser, BrowserContext, Page
+
+
+@dataclass
+class PublishParams:
+    """发布参数"""
+    title: str
+    video_path: str
+    description: str = ""
+    cover_path: Optional[str] = None
+    tags: List[str] = field(default_factory=list)
+    publish_date: Optional[datetime] = None
+    location: str = "重庆市"
+    
+    def __post_init__(self):
+        if not self.description:
+            self.description = self.title
+
+
+@dataclass
+class PublishResult:
+    """发布结果"""
+    success: bool
+    platform: str
+    video_id: str = ""
+    video_url: str = ""
+    message: str = ""
+    error: str = ""
+
+
+class BasePublisher(ABC):
+    """
+    平台发布基类
+    所有平台发布器都需要继承此类
+    """
+    
+    platform_name: str = "base"
+    login_url: str = ""
+    publish_url: str = ""
+    cookie_domain: str = ""
+    
+    def __init__(self, headless: bool = True):
+        self.headless = headless
+        self.browser: Optional[Browser] = None
+        self.context: Optional[BrowserContext] = None
+        self.page: Optional[Page] = None
+        self.on_progress: Optional[Callable[[int, str], None]] = None
+    
+    def set_progress_callback(self, callback: Callable[[int, str], None]):
+        """设置进度回调"""
+        self.on_progress = callback
+    
+    def report_progress(self, progress: int, message: str):
+        """报告进度"""
+        print(f"[{self.platform_name}] [{progress}%] {message}")
+        if self.on_progress:
+            self.on_progress(progress, message)
+    
+    @staticmethod
+    def parse_cookies(cookies_str: str) -> list:
+        """解析 cookie 字符串为列表"""
+        try:
+            cookies = json.loads(cookies_str)
+            if isinstance(cookies, list):
+                return cookies
+        except json.JSONDecodeError:
+            pass
+        
+        # 字符串格式: name=value; name2=value2
+        cookies = []
+        for item in cookies_str.split(';'):
+            item = item.strip()
+            if '=' in item:
+                name, value = item.split('=', 1)
+                cookies.append({
+                    'name': name.strip(),
+                    'value': value.strip(),
+                    'domain': '',
+                    'path': '/'
+                })
+        return cookies
+    
+    @staticmethod
+    def cookies_to_string(cookies: list) -> str:
+        """将 cookie 列表转换为字符串"""
+        return '; '.join([f"{c['name']}={c['value']}" for c in cookies])
+    
+    async def init_browser(self, storage_state: str = None):
+        """初始化浏览器"""
+        playwright = await async_playwright().start()
+        self.browser = await playwright.chromium.launch(headless=self.headless)
+        
+        if storage_state and os.path.exists(storage_state):
+            self.context = await self.browser.new_context(storage_state=storage_state)
+        else:
+            self.context = await self.browser.new_context()
+        
+        self.page = await self.context.new_page()
+        return self.page
+    
+    async def set_cookies(self, cookies: list):
+        """设置 cookies"""
+        if not self.context:
+            raise Exception("Browser context not initialized")
+        
+        # 设置默认域名
+        for cookie in cookies:
+            if 'domain' not in cookie or not cookie['domain']:
+                cookie['domain'] = self.cookie_domain
+        
+        await self.context.add_cookies(cookies)
+    
+    async def close_browser(self):
+        """关闭浏览器"""
+        if self.context:
+            await self.context.close()
+        if self.browser:
+            await self.browser.close()
+    
+    async def save_cookies(self, file_path: str):
+        """保存 cookies 到文件"""
+        if self.context:
+            await self.context.storage_state(path=file_path)
+    
+    async def wait_for_upload_complete(self, success_selector: str, timeout: int = 300):
+        """等待上传完成"""
+        if not self.page:
+            raise Exception("Page not initialized")
+        
+        for _ in range(timeout // 3):
+            try:
+                count = await self.page.locator(success_selector).count()
+                if count > 0:
+                    return True
+            except:
+                pass
+            await asyncio.sleep(3)
+            self.report_progress(30, "正在上传视频...")
+        
+        return False
+    
+    @abstractmethod
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        """
+        发布视频 - 子类必须实现
+        
+        Args:
+            cookies: cookie 字符串或 JSON
+            params: 发布参数
+            
+        Returns:
+            PublishResult: 发布结果
+        """
+        pass
+    
+    async def run(self, cookies: str, params: PublishParams) -> PublishResult:
+        """
+        运行发布任务
+        包装了 publish 方法,添加了异常处理和资源清理
+        """
+        try:
+            return await self.publish(cookies, params)
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error=str(e)
+            )
+        finally:
+            await self.close_browser()

+ 205 - 0
server/python/platforms/douyin.py

@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+"""
+抖音视频发布器
+参考: matrix/douyin_uploader/main.py
+"""
+
+import asyncio
+import os
+from datetime import datetime
+from .base import BasePublisher, PublishParams, PublishResult
+
+
+class DouyinPublisher(BasePublisher):
+    """
+    抖音视频发布器
+    使用 Playwright 自动化操作抖音创作者中心
+    """
+    
+    platform_name = "douyin"
+    login_url = "https://creator.douyin.com/"
+    publish_url = "https://creator.douyin.com/creator-micro/content/upload"
+    cookie_domain = ".douyin.com"
+    
+    async def set_schedule_time(self, publish_date: datetime):
+        """设置定时发布"""
+        if not self.page:
+            return
+        
+        # 选择定时发布
+        label_element = self.page.locator("label.radio-d4zkru:has-text('定时发布')")
+        await label_element.click()
+        await asyncio.sleep(1)
+        
+        # 输入时间
+        publish_date_str = publish_date.strftime("%Y-%m-%d %H:%M")
+        await self.page.locator('.semi-input[placeholder="日期和时间"]').click()
+        await self.page.keyboard.press("Control+KeyA")
+        await self.page.keyboard.type(str(publish_date_str))
+        await self.page.keyboard.press("Enter")
+        await asyncio.sleep(1)
+    
+    async def handle_upload_error(self, video_path: str):
+        """处理上传错误,重新上传"""
+        if not self.page:
+            return
+        
+        print(f"[{self.platform_name}] 视频出错了,重新上传中...")
+        await self.page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(video_path)
+    
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        """发布视频到抖音"""
+        self.report_progress(5, "正在初始化浏览器...")
+        
+        # 初始化浏览器
+        await self.init_browser()
+        
+        # 解析并设置 cookies
+        cookie_list = self.parse_cookies(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}")
+        
+        self.report_progress(10, "正在打开上传页面...")
+        
+        # 访问上传页面
+        await self.page.goto(self.publish_url)
+        await self.page.wait_for_url(self.publish_url, timeout=30000)
+        
+        self.report_progress(15, "正在选择视频文件...")
+        
+        # 点击上传区域
+        upload_div = self.page.locator("div[class*='container-drag']").first
+        async with self.page.expect_file_chooser() as fc_info:
+            await upload_div.click()
+        file_chooser = await fc_info.value
+        await file_chooser.set_files(params.video_path)
+        
+        # 等待跳转到发布页面
+        self.report_progress(20, "等待进入发布页面...")
+        for _ in range(60):
+            try:
+                await self.page.wait_for_url(
+                    "https://creator.douyin.com/creator-micro/content/post/video*",
+                    timeout=2000
+                )
+                break
+            except:
+                await asyncio.sleep(1)
+        
+        await asyncio.sleep(2)
+        self.report_progress(30, "正在填充标题和话题...")
+        
+        # 填写标题
+        title_input = self.page.get_by_text('作品标题').locator("..").locator(
+            "xpath=following-sibling::div[1]").locator("input")
+        if await title_input.count():
+            await title_input.fill(params.title[:30])
+        else:
+            # 备用方式
+            title_container = self.page.locator(".notranslate")
+            await title_container.click()
+            await self.page.keyboard.press("Control+KeyA")
+            await self.page.keyboard.press("Delete")
+            await self.page.keyboard.type(params.title)
+            await self.page.keyboard.press("Enter")
+        
+        # 添加话题标签
+        if params.tags:
+            css_selector = ".zone-container"
+            for tag in params.tags:
+                print(f"[{self.platform_name}] 添加话题: #{tag}")
+                await self.page.type(css_selector, "#" + tag)
+                await self.page.press(css_selector, "Space")
+        
+        self.report_progress(40, "等待视频上传完成...")
+        
+        # 等待视频上传完成
+        for _ in range(120):
+            try:
+                count = await self.page.locator("div").filter(has_text="重新上传").count()
+                if count > 0:
+                    print(f"[{self.platform_name}] 视频上传完毕")
+                    break
+                
+                # 检查上传错误
+                if await self.page.locator('div.progress-div > div:has-text("上传失败")').count():
+                    await self.handle_upload_error(params.video_path)
+                
+                await asyncio.sleep(3)
+            except:
+                await asyncio.sleep(3)
+        
+        self.report_progress(60, "处理视频设置...")
+        
+        # 关闭弹窗
+        known_btn = self.page.get_by_role("button", name="我知道了")
+        if await known_btn.count() > 0:
+            await known_btn.first.click()
+        
+        await asyncio.sleep(2)
+        
+        # 设置位置
+        try:
+            await self.page.locator('div.semi-select span:has-text("输入地理位置")').click()
+            await asyncio.sleep(1)
+            await self.page.keyboard.press("Control+KeyA")
+            await self.page.keyboard.press("Delete")
+            await self.page.keyboard.type(params.location)
+            await asyncio.sleep(1)
+            await self.page.locator('div[role="listbox"] [role="option"]').first.click()
+        except Exception as e:
+            print(f"[{self.platform_name}] 设置位置失败: {e}")
+        
+        # 开启头条/西瓜同步
+        try:
+            third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
+            if await self.page.locator(third_part_element).count():
+                class_name = await self.page.eval_on_selector(
+                    third_part_element, 'div => div.className')
+                if 'semi-switch-checked' not in class_name:
+                    await self.page.locator(third_part_element).locator(
+                        'input.semi-switch-native-control').click()
+        except:
+            pass
+        
+        # 定时发布
+        if params.publish_date:
+            self.report_progress(70, "设置定时发布...")
+            await self.set_schedule_time(params.publish_date)
+        
+        self.report_progress(80, "正在发布...")
+        
+        # 点击发布
+        for _ in range(30):
+            try:
+                publish_btn = self.page.get_by_role('button', name="发布", exact=True)
+                if await publish_btn.count():
+                    await publish_btn.click()
+                await self.page.wait_for_url(
+                    "https://creator.douyin.com/creator-micro/content/manage",
+                    timeout=5000
+                )
+                self.report_progress(100, "发布成功")
+                return PublishResult(
+                    success=True,
+                    platform=self.platform_name,
+                    message="发布成功"
+                )
+            except:
+                current_url = self.page.url
+                if "content/manage" in current_url:
+                    self.report_progress(100, "发布成功")
+                    return PublishResult(
+                        success=True,
+                        platform=self.platform_name,
+                        message="发布成功"
+                    )
+                await asyncio.sleep(1)
+        
+        raise Exception("发布超时")

+ 165 - 0
server/python/platforms/kuaishou.py

@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+"""
+快手视频发布器
+参考: matrix/ks_uploader/main.py
+"""
+
+import asyncio
+import os
+from datetime import datetime
+from .base import BasePublisher, PublishParams, PublishResult
+
+
+class KuaishouPublisher(BasePublisher):
+    """
+    快手视频发布器
+    使用 Playwright 自动化操作快手创作者中心
+    """
+    
+    platform_name = "kuaishou"
+    login_url = "https://cp.kuaishou.com/"
+    publish_url = "https://cp.kuaishou.com/article/publish/video"
+    cookie_domain = ".kuaishou.com"
+    
+    async def set_schedule_time(self, publish_date: datetime):
+        """设置定时发布"""
+        if not self.page:
+            return
+        
+        # 选择定时发布
+        label_element = self.page.locator("label.radio--4Gpx6:has-text('定时发布')")
+        await label_element.click()
+        await asyncio.sleep(1)
+        
+        # 输入时间
+        publish_date_str = publish_date.strftime("%Y-%m-%d %H:%M")
+        await self.page.locator('.semi-input[placeholder="日期和时间"]').click()
+        await self.page.keyboard.press("Control+KeyA")
+        await self.page.keyboard.type(str(publish_date_str))
+        await self.page.keyboard.press("Enter")
+        await asyncio.sleep(1)
+    
+    async def upload_cover(self, cover_path: str):
+        """上传封面图"""
+        if not self.page or not cover_path or not os.path.exists(cover_path):
+            return
+        
+        try:
+            await self.page.get_by_role("button", name="编辑封面").click()
+            await asyncio.sleep(1)
+            await self.page.get_by_role("tab", name="上传封面").click()
+            
+            preview_div = self.page.get_by_role("tabpanel", name="上传封面").locator("div").nth(1)
+            async with self.page.expect_file_chooser() as fc_info:
+                await preview_div.click()
+            preview_chooser = await fc_info.value
+            await preview_chooser.set_files(cover_path)
+            
+            await self.page.get_by_role("button", name="确认").click()
+            await asyncio.sleep(3)
+            
+            print(f"[{self.platform_name}] 封面上传成功")
+        except Exception as e:
+            print(f"[{self.platform_name}] 封面上传失败: {e}")
+    
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        """发布视频到快手"""
+        self.report_progress(5, "正在初始化浏览器...")
+        
+        # 初始化浏览器
+        await self.init_browser()
+        
+        # 解析并设置 cookies
+        cookie_list = self.parse_cookies(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}")
+        
+        self.report_progress(10, "正在打开上传页面...")
+        
+        # 访问上传页面
+        await self.page.goto(self.publish_url)
+        await self.page.wait_for_url(self.publish_url, timeout=30000)
+        
+        self.report_progress(15, "正在选择视频文件...")
+        
+        # 点击上传按钮
+        upload_btn = self.page.get_by_role("button", name="上传视频")
+        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)
+        
+        await asyncio.sleep(1)
+        
+        # 关闭可能的弹窗
+        known_btn = self.page.get_by_role("button", name="我知道了")
+        if await known_btn.count():
+            await known_btn.click()
+        
+        self.report_progress(20, "正在填充标题...")
+        
+        # 填写标题
+        await asyncio.sleep(1)
+        title_input = self.page.get_by_placeholder('添加合适的话题和描述,作品能获得更多推荐~')
+        if await title_input.count():
+            await title_input.click()
+            await title_input.fill(params.title[:30])
+        
+        self.report_progress(30, "等待视频上传完成...")
+        
+        # 等待上传完成
+        for _ in range(120):
+            try:
+                count = await self.page.locator('span:has-text("上传成功")').count()
+                if count > 0:
+                    print(f"[{self.platform_name}] 视频上传完毕")
+                    break
+                await asyncio.sleep(3)
+            except:
+                await asyncio.sleep(3)
+        
+        self.report_progress(50, "正在上传封面...")
+        
+        # 上传封面
+        await self.upload_cover(params.cover_path)
+        
+        # 定时发布(快手暂不支持或选择器有变化)
+        # if params.publish_date:
+        #     await self.set_schedule_time(params.publish_date)
+        
+        self.report_progress(80, "正在发布...")
+        
+        # 点击发布
+        for _ in range(30):
+            try:
+                publish_btn = self.page.get_by_role('button', name="发布", exact=True)
+                if await publish_btn.count():
+                    await publish_btn.click()
+                await self.page.wait_for_url(
+                    "https://cp.kuaishou.com/article/manage/video*",
+                    timeout=5000
+                )
+                self.report_progress(100, "发布成功")
+                return PublishResult(
+                    success=True,
+                    platform=self.platform_name,
+                    message="发布成功"
+                )
+            except:
+                current_url = self.page.url
+                if "manage/video" in current_url:
+                    self.report_progress(100, "发布成功")
+                    return PublishResult(
+                        success=True,
+                        platform=self.platform_name,
+                        message="发布成功"
+                    )
+                await asyncio.sleep(1)
+        
+        raise Exception("发布超时")

+ 290 - 0
server/python/platforms/weixin.py

@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+"""
+微信视频号发布器
+参考: matrix/tencent_uploader/main.py
+"""
+
+import asyncio
+import os
+from datetime import datetime
+from .base import BasePublisher, PublishParams, PublishResult
+
+
+def format_short_title(origin_title: str) -> str:
+    """
+    格式化短标题
+    - 移除特殊字符
+    - 长度限制在 6-16 字符
+    """
+    allowed_special_chars = "《》"":+?%°"
+    
+    filtered_chars = [
+        char if char.isalnum() or char in allowed_special_chars 
+        else ' ' if char == ',' else '' 
+        for char in origin_title
+    ]
+    formatted_string = ''.join(filtered_chars)
+    
+    if len(formatted_string) > 16:
+        formatted_string = formatted_string[:16]
+    elif len(formatted_string) < 6:
+        formatted_string += ' ' * (6 - len(formatted_string))
+    
+    return formatted_string
+
+
+class WeixinPublisher(BasePublisher):
+    """
+    微信视频号发布器
+    使用 Playwright 自动化操作视频号创作者中心
+    注意: 需要使用 Chrome 浏览器,否则可能出现 H264 编码错误
+    """
+    
+    platform_name = "weixin"
+    login_url = "https://channels.weixin.qq.com/platform"
+    publish_url = "https://channels.weixin.qq.com/platform/post/create"
+    cookie_domain = ".weixin.qq.com"
+    
+    async def init_browser(self, storage_state: str = None):
+        """初始化浏览器 - 使用 Chrome 浏览器"""
+        from playwright.async_api import async_playwright
+        
+        playwright = await async_playwright().start()
+        # 使用 Chrome 浏览器,避免 H264 编码问题
+        self.browser = await playwright.chromium.launch(
+            headless=self.headless, 
+            channel="chrome"
+        )
+        
+        if storage_state and os.path.exists(storage_state):
+            self.context = await self.browser.new_context(storage_state=storage_state)
+        else:
+            self.context = await self.browser.new_context()
+        
+        self.page = await self.context.new_page()
+        return self.page
+    
+    async def set_schedule_time(self, publish_date: datetime):
+        """设置定时发布"""
+        if not self.page:
+            return
+        
+        print(f"[{self.platform_name}] 设置定时发布...")
+        
+        # 点击定时选项
+        label_element = self.page.locator("label").filter(has_text="定时").nth(1)
+        await label_element.click()
+        
+        # 选择日期
+        await self.page.click('input[placeholder="请选择发表时间"]')
+        
+        publish_month = f"{publish_date.month:02d}"
+        current_month = f"{publish_month}月"
+        
+        # 检查月份
+        page_month = await self.page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")')
+        if page_month != current_month:
+            await self.page.click('button.weui-desktop-btn__icon__right')
+        
+        # 选择日期
+        elements = await self.page.query_selector_all('table.weui-desktop-picker__table a')
+        for element in elements:
+            class_name = await element.evaluate('el => el.className')
+            if 'weui-desktop-picker__disabled' in class_name:
+                continue
+            text = await element.inner_text()
+            if text.strip() == str(publish_date.day):
+                await element.click()
+                break
+        
+        # 输入时间
+        await self.page.click('input[placeholder="请选择时间"]')
+        await self.page.keyboard.press("Control+KeyA")
+        await self.page.keyboard.type(str(publish_date.hour))
+        
+        # 点击其他地方确认
+        await self.page.locator("div.input-editor").click()
+    
+    async def handle_upload_error(self, video_path: str):
+        """处理上传错误"""
+        if not self.page:
+            return
+        
+        print(f"[{self.platform_name}] 视频出错了,重新上传中...")
+        await self.page.locator('div.media-status-content div.tag-inner:has-text("删除")').click()
+        await self.page.get_by_role('button', name="删除", exact=True).click()
+        file_input = self.page.locator('input[type="file"]')
+        await file_input.set_input_files(video_path)
+    
+    async def add_title_tags(self, params: PublishParams):
+        """添加标题和话题"""
+        if not self.page:
+            return
+        
+        await self.page.locator("div.input-editor").click()
+        await self.page.keyboard.type(params.title)
+        
+        if params.tags:
+            await self.page.keyboard.press("Enter")
+            for tag in params.tags:
+                await self.page.keyboard.type("#" + tag)
+                await self.page.keyboard.press("Space")
+        
+        print(f"[{self.platform_name}] 成功添加标题和 {len(params.tags)} 个话题")
+    
+    async def add_short_title(self):
+        """添加短标题"""
+        if not self.page:
+            return
+        
+        try:
+            short_title_element = self.page.get_by_text("短标题", exact=True).locator("..").locator(
+                "xpath=following-sibling::div").locator('span input[type="text"]')
+            if await short_title_element.count():
+                # 获取已有内容作为短标题
+                pass
+        except:
+            pass
+    
+    async def upload_cover(self, cover_path: str):
+        """上传封面图"""
+        if not self.page or not cover_path or not os.path.exists(cover_path):
+            return
+        
+        try:
+            await asyncio.sleep(2)
+            preview_btn_info = await self.page.locator(
+                'div.finder-tag-wrap.btn:has-text("更换封面")').get_attribute('class')
+            
+            if "disabled" not in preview_btn_info:
+                await self.page.locator('div.finder-tag-wrap.btn:has-text("更换封面")').click()
+                await self.page.locator('div.single-cover-uploader-wrap > div.wrap').hover()
+                
+                # 删除现有封面
+                if await self.page.locator(".del-wrap > .svg-icon").count():
+                    await self.page.locator(".del-wrap > .svg-icon").click()
+                
+                # 上传新封面
+                preview_div = self.page.locator("div.single-cover-uploader-wrap > div.wrap")
+                async with self.page.expect_file_chooser() as fc_info:
+                    await preview_div.click()
+                preview_chooser = await fc_info.value
+                await preview_chooser.set_files(cover_path)
+                
+                await asyncio.sleep(2)
+                await self.page.get_by_role("button", name="确定").click()
+                await asyncio.sleep(1)
+                await self.page.get_by_role("button", name="确认").click()
+                
+                print(f"[{self.platform_name}] 封面上传成功")
+        except Exception as e:
+            print(f"[{self.platform_name}] 封面上传失败: {e}")
+    
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        """发布视频到视频号"""
+        self.report_progress(5, "正在初始化浏览器...")
+        
+        # 初始化浏览器(使用 Chrome)
+        await self.init_browser()
+        
+        # 解析并设置 cookies
+        cookie_list = self.parse_cookies(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}")
+        
+        self.report_progress(10, "正在打开上传页面...")
+        
+        # 访问上传页面
+        await self.page.goto(self.publish_url)
+        await self.page.wait_for_url(self.publish_url, timeout=30000)
+        
+        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)
+        
+        self.report_progress(20, "正在填充标题和话题...")
+        
+        # 添加标题和话题
+        await self.add_title_tags(params)
+        
+        self.report_progress(30, "等待视频上传完成...")
+        
+        # 等待上传完成
+        for _ in range(120):
+            try:
+                button_info = await self.page.get_by_role("button", name="发表").get_attribute('class')
+                if "weui-desktop-btn_disabled" not in button_info:
+                    print(f"[{self.platform_name}] 视频上传完毕")
+                    
+                    # 上传封面
+                    self.report_progress(50, "正在上传封面...")
+                    await self.upload_cover(params.cover_path)
+                    break
+                else:
+                    # 检查上传错误
+                    if await self.page.locator('div.status-msg.error').count():
+                        if await self.page.locator('div.media-status-content div.tag-inner:has-text("删除")').count():
+                            await self.handle_upload_error(params.video_path)
+                    
+                    await asyncio.sleep(3)
+            except:
+                await asyncio.sleep(3)
+        
+        self.report_progress(60, "处理视频设置...")
+        
+        # 添加短标题
+        try:
+            short_title_el = self.page.get_by_text("短标题", exact=True).locator("..").locator(
+                "xpath=following-sibling::div").locator('span input[type="text"]')
+            if await short_title_el.count():
+                short_title = format_short_title(params.title)
+                await short_title_el.fill(short_title)
+        except:
+            pass
+        
+        # 定时发布
+        if params.publish_date:
+            self.report_progress(70, "设置定时发布...")
+            await self.set_schedule_time(params.publish_date)
+        
+        self.report_progress(80, "正在发布...")
+        
+        # 点击发布
+        for _ in range(30):
+            try:
+                publish_btn = self.page.locator('div.form-btns button:has-text("发表")')
+                if await publish_btn.count():
+                    await publish_btn.click()
+                await self.page.wait_for_url(
+                    "https://channels.weixin.qq.com/platform/post/list",
+                    timeout=10000
+                )
+                self.report_progress(100, "发布成功")
+                return PublishResult(
+                    success=True,
+                    platform=self.platform_name,
+                    message="发布成功"
+                )
+            except:
+                current_url = self.page.url
+                if "post/list" in current_url:
+                    self.report_progress(100, "发布成功")
+                    return PublishResult(
+                        success=True,
+                        platform=self.platform_name,
+                        message="发布成功"
+                    )
+                await asyncio.sleep(1)
+        
+        raise Exception("发布超时")

+ 268 - 0
server/python/platforms/xiaohongshu.py

@@ -0,0 +1,268 @@
+# -*- coding: utf-8 -*-
+"""
+小红书视频发布器
+参考: matrix/xhs_uploader/main.py
+使用 xhs SDK API 方式发布,更稳定
+"""
+
+import asyncio
+import os
+import sys
+from pathlib import Path
+from .base import BasePublisher, PublishParams, PublishResult
+
+# 添加 matrix 项目路径,用于导入签名脚本
+MATRIX_PATH = Path(__file__).parent.parent.parent.parent / "matrix"
+sys.path.insert(0, str(MATRIX_PATH))
+
+# 尝试导入 xhs SDK
+try:
+    from xhs import XhsClient
+    XHS_SDK_AVAILABLE = True
+except ImportError:
+    print("[Warning] xhs 库未安装,请运行: pip install xhs")
+    XhsClient = None
+    XHS_SDK_AVAILABLE = False
+
+# 签名脚本路径
+STEALTH_JS_PATH = MATRIX_PATH / "xhs-api" / "js" / "stealth.min.js"
+
+
+class XiaohongshuPublisher(BasePublisher):
+    """
+    小红书视频发布器
+    优先使用 xhs SDK API 方式发布
+    """
+    
+    platform_name = "xiaohongshu"
+    login_url = "https://creator.xiaohongshu.com/"
+    publish_url = "https://creator.xiaohongshu.com/publish/publish"
+    cookie_domain = ".xiaohongshu.com"
+    
+    async def get_sign(self, uri: str, data=None, a1: str = "", web_session: str = ""):
+        """获取小红书 API 签名"""
+        from playwright.async_api import async_playwright
+        
+        try:
+            async with async_playwright() as playwright:
+                browser = await playwright.chromium.launch(headless=True)
+                browser_context = await browser.new_context()
+                
+                if STEALTH_JS_PATH.exists():
+                    await browser_context.add_init_script(path=str(STEALTH_JS_PATH))
+                
+                page = await browser_context.new_page()
+                await page.goto("https://www.xiaohongshu.com")
+                await asyncio.sleep(1)
+                await page.reload()
+                await asyncio.sleep(1)
+                
+                if a1:
+                    await browser_context.add_cookies([
+                        {'name': 'a1', 'value': a1, 'domain': ".xiaohongshu.com", 'path': "/"}
+                    ])
+                    await page.reload()
+                    await asyncio.sleep(0.5)
+                
+                encrypt_params = await page.evaluate(
+                    "([url, data]) => window._webmsxyw(url, data)", 
+                    [uri, data]
+                )
+                
+                await browser_context.close()
+                await browser.close()
+                
+                return {
+                    "x-s": encrypt_params["X-s"],
+                    "x-t": str(encrypt_params["X-t"])
+                }
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            raise Exception(f"签名失败: {e}")
+    
+    def sign_sync(self, uri, data=None, a1="", web_session=""):
+        """同步签名函数,供 XhsClient 使用"""
+        return asyncio.run(self.get_sign(uri, data, a1, web_session))
+    
+    async def publish_via_api(self, cookies: str, params: PublishParams) -> PublishResult:
+        """通过 API 发布视频"""
+        if not XHS_SDK_AVAILABLE:
+            raise Exception("xhs SDK 未安装,请运行: pip install xhs")
+        
+        self.report_progress(10, "正在通过 API 发布...")
+        
+        # 转换 cookie 格式
+        cookie_list = self.parse_cookies(cookies)
+        cookie_string = self.cookies_to_string(cookie_list) if cookie_list else cookies
+        
+        self.report_progress(20, "正在上传视频...")
+        
+        # 创建客户端
+        xhs_client = XhsClient(cookie_string, sign=self.sign_sync)
+        
+        # 发布视频
+        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
+        )
+        
+        self.report_progress(100, "发布成功")
+        
+        return PublishResult(
+            success=True,
+            platform=self.platform_name,
+            video_id=result.get("note_id", ""),
+            video_url=result.get("url", ""),
+            message="发布成功"
+        )
+    
+    async def publish(self, cookies: str, params: PublishParams) -> PublishResult:
+        """发布视频到小红书"""
+        # 检查视频文件
+        if not os.path.exists(params.video_path):
+            raise Exception(f"视频文件不存在: {params.video_path}")
+        
+        self.report_progress(5, "正在准备发布...")
+        
+        # 优先使用 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 方式...")
+        
+        # 回退到 Playwright 方式
+        return await self.publish_via_playwright(cookies, params)
+    
+    async def publish_via_playwright(self, cookies: str, params: PublishParams) -> PublishResult:
+        """通过 Playwright 发布视频(备用方式)"""
+        self.report_progress(10, "正在初始化浏览器...")
+        
+        await self.init_browser()
+        
+        cookie_list = self.parse_cookies(cookies)
+        await self.set_cookies(cookie_list)
+        
+        if not self.page:
+            raise Exception("Page not initialized")
+        
+        self.report_progress(15, "正在打开发布页面...")
+        
+        await self.page.goto(self.publish_url)
+        await asyncio.sleep(3)
+        
+        # 检查登录状态
+        if "login" in self.page.url or "passport" in self.page.url:
+            raise Exception("登录已过期,请重新登录")
+        
+        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
+        
+        # 上传视频
+        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
+        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
+        
+        if not upload_triggered:
+            raise Exception("无法上传视频文件")
+        
+        self.report_progress(40, "等待视频上传完成...")
+        
+        # 等待上传完成
+        for _ in range(100):
+            await asyncio.sleep(3)
+            # 检查标题输入框是否出现
+            title_input = await self.page.locator('input[placeholder*="标题"]').count()
+            if title_input > 0:
+                break
+        
+        self.report_progress(60, "正在填写笔记信息...")
+        
+        # 填写标题
+        title_selectors = [
+            'input[placeholder*="标题"]',
+            '[class*="title"] input',
+        ]
+        for selector in title_selectors:
+            title_input = self.page.locator(selector).first
+            if await title_input.count() > 0:
+                await title_input.fill(params.title[:20])
+                break
+        
+        # 填写描述和标签
+        if params.description or params.tags:
+            desc_selectors = [
+                '[class*="content-input"] [contenteditable="true"]',
+                '[class*="editor"] [contenteditable="true"]',
+            ]
+            for selector in desc_selectors:
+                desc_input = self.page.locator(selector).first
+                if await desc_input.count() > 0:
+                    await desc_input.click()
+                    if params.description:
+                        await self.page.keyboard.type(params.description, delay=30)
+                    if params.tags:
+                        await self.page.keyboard.press("Enter")
+                        for tag in params.tags:
+                            await self.page.keyboard.type(f"#{tag} ", delay=30)
+                    break
+        
+        self.report_progress(80, "正在发布...")
+        
+        await asyncio.sleep(2)
+        
+        # 点击发布
+        publish_selectors = [
+            'button.publishBtn',
+            '.publishBtn',
+            'button:has-text("发布")',
+        ]
+        
+        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)
+                    break
+        
+        await asyncio.sleep(5)
+        
+        self.report_progress(100, "发布完成")
+        
+        return PublishResult(
+            success=True,
+            platform=self.platform_name,
+            message="发布完成"
+        )

+ 21 - 0
server/python/requirements.txt

@@ -0,0 +1,21 @@
+# 多平台视频发布服务依赖
+# Python 3.8+
+
+# Web 框架
+flask>=2.0.0
+flask-cors>=3.0.0
+
+# 浏览器自动化
+playwright>=1.40.0
+
+# 小红书 SDK(可选,用于 API 方式发布,更稳定)
+xhs>=0.1.0
+
+# 图像处理
+Pillow>=9.0.0
+
+# 二维码生成(登录用)
+qrcode>=7.0
+
+# HTTP 请求
+requests>=2.28.0

+ 8 - 0
server/python/utils/__init__.py

@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+"""
+工具模块
+"""
+
+from .helpers import parse_datetime, validate_video_file
+
+__all__ = ['parse_datetime', 'validate_video_file']

+ 89 - 0
server/python/utils/helpers.py

@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+工具函数
+"""
+
+import os
+from datetime import datetime
+from typing import Optional
+
+
+def parse_datetime(date_str: str) -> Optional[datetime]:
+    """
+    解析日期时间字符串
+    支持多种格式
+    """
+    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
+    
+    # 检查文件大小(至少 1KB)
+    if os.path.getsize(video_path) < 1024:
+        return False
+    
+    return True
+
+
+def get_video_duration(video_path: str) -> Optional[float]:
+    """
+    获取视频时长(秒)
+    需要安装 ffprobe
+    """
+    try:
+        import subprocess
+        result = subprocess.run(
+            ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
+             '-of', 'default=noprint_wrappers=1:nokey=1', video_path],
+            capture_output=True,
+            text=True
+        )
+        return float(result.stdout.strip())
+    except:
+        return None
+
+
+def format_file_size(size_bytes: int) -> str:
+    """
+    格式化文件大小
+    """
+    for unit in ['B', 'KB', 'MB', 'GB']:
+        if size_bytes < 1024.0:
+            return f"{size_bytes:.2f} {unit}"
+        size_bytes /= 1024.0
+    return f"{size_bytes:.2f} TB"

+ 100 - 0
server/src/app.ts

@@ -4,6 +4,9 @@ import helmet from 'helmet';
 import morgan from 'morgan';
 import compression from 'compression';
 import { createServer } from 'http';
+import { createConnection } from 'net';
+import { exec } from 'child_process';
+import { promisify } from 'util';
 import { config } from './config/index.js';
 import { errorHandler } from './middleware/error.js';
 import { setupRoutes } from './routes/index.js';
@@ -14,6 +17,8 @@ import { logger } from './utils/logger.js';
 import { taskScheduler } from './scheduler/index.js';
 import { registerTaskExecutors } from './services/taskExecutors.js';
 
+const execAsync = promisify(exec);
+
 const app = express();
 const httpServer = createServer(app);
 
@@ -54,8 +59,103 @@ app.use(errorHandler);
 // WebSocket
 setupWebSocket(httpServer);
 
+// 检查端口是否被占用
+async function checkPortInUse(port: number): Promise<boolean> {
+  return new Promise((resolve) => {
+    const client = createConnection({ port }, () => {
+      client.end();
+      resolve(true); // 端口被占用
+    });
+    client.on('error', () => {
+      resolve(false); // 端口未被占用
+    });
+  });
+}
+
+// 获取占用端口的进程ID(跨平台)
+async function getProcessOnPort(port: number): Promise<number | null> {
+  const isWindows = process.platform === 'win32';
+  
+  try {
+    if (isWindows) {
+      const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
+      const lines = stdout.trim().split('\n');
+      if (lines.length > 0) {
+        const parts = lines[0].trim().split(/\s+/);
+        const pid = parseInt(parts[parts.length - 1], 10);
+        if (!isNaN(pid)) return pid;
+      }
+    } else {
+      const { stdout } = await execAsync(`lsof -i :${port} -t`);
+      const pid = parseInt(stdout.trim().split('\n')[0], 10);
+      if (!isNaN(pid)) return pid;
+    }
+  } catch {
+    // 命令执行失败,端口可能没有被占用
+  }
+  return null;
+}
+
+// 终止进程(跨平台)
+async function killProcess(pid: number): Promise<boolean> {
+  const isWindows = process.platform === 'win32';
+  
+  try {
+    if (isWindows) {
+      await execAsync(`taskkill /PID ${pid} /F`);
+    } else {
+      await execAsync(`kill -9 ${pid}`);
+    }
+    // 等待进程完全终止
+    await new Promise(resolve => setTimeout(resolve, 1000));
+    return true;
+  } catch (error) {
+    logger.error(`Failed to kill process ${pid}:`, error);
+    return false;
+  }
+}
+
+// 检查并释放端口
+async function ensurePortAvailable(port: number): Promise<void> {
+  const inUse = await checkPortInUse(port);
+  
+  if (!inUse) {
+    return;
+  }
+  
+  logger.warn(`Port ${port} is already in use, attempting to release...`);
+  
+  const pid = await getProcessOnPort(port);
+  
+  if (pid) {
+    logger.info(`Found process ${pid} using port ${port}, terminating...`);
+    const killed = await killProcess(pid);
+    
+    if (killed) {
+      logger.info(`Successfully terminated process ${pid}`);
+      // 再次检查端口
+      const stillInUse = await checkPortInUse(port);
+      if (stillInUse) {
+        throw new Error(`Port ${port} is still in use after killing process`);
+      }
+    } else {
+      throw new Error(`Failed to kill process ${pid} using port ${port}`);
+    }
+  } else {
+    throw new Error(`Port ${port} is in use but could not identify the process`);
+  }
+}
+
 // 启动服务
 async function bootstrap() {
+  // 确保端口可用
+  try {
+    await ensurePortAvailable(config.port);
+  } catch (error) {
+    logger.error('Port availability check failed:', error);
+    process.exit(1);
+  }
+  
   let dbConnected = false;
   let redisConnected = false;
 

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

@@ -12,6 +12,9 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 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';
+
 /**
  * 抖音平台适配器
  */
@@ -717,8 +720,79 @@ export class DouyinAdapter extends BasePlatformAdapter {
   }
   
   /**
+   * 检查 Python 发布服务是否可用
+   */
+  private async checkPythonServiceAvailable(): Promise<boolean> {
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+        method: 'GET',
+        signal: AbortSignal.timeout(3000),
+      });
+      if (response.ok) {
+        const data = await response.json();
+        return data.status === 'ok' && data.supported_platforms?.includes('douyin');
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 通过 Python 服务发布视频(参考 matrix 项目)
+   */
+  private async publishVideoViaPython(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Douyin Python] Starting publish via Python service...');
+    onProgress?.(5, '正在通过 Python 服务发布...');
+
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          platform: 'douyin',
+          cookie: cookies,
+          title: params.title,
+          description: params.description || params.title,
+          video_path: params.videoPath,
+          cover_path: params.coverPath,
+          tags: params.tags || [],
+          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+          location: params.location || '重庆市',
+        }),
+        signal: AbortSignal.timeout(600000), // 10分钟超时
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        onProgress?.(100, '发布成功');
+        logger.info('[Douyin Python] Publish successful');
+        return {
+          success: true,
+          videoId: `douyin_${Date.now()}`,
+          videoUrl: '',
+          message: '发布成功',
+        };
+      } else {
+        throw new Error(result.error || '发布失败');
+      }
+    } catch (error) {
+      logger.error('[Douyin Python] Publish failed:', error);
+      throw error;
+    }
+  }
+
+  /**
    * 发布视频
    * 参考 https://github.com/kebenxiaoming/matrix 项目实现
+   * 优先使用 Python 服务,如果不可用则回退到 Playwright 方式
    * @param onCaptchaRequired 验证码回调,返回用户输入的验证码
    * @param options.headless 是否使用无头模式,默认 true
    */
@@ -729,6 +803,21 @@ export class DouyinAdapter extends BasePlatformAdapter {
     onCaptchaRequired?: (captchaInfo: { taskId: string; phone?: string }) => Promise<string>,
     options?: { headless?: boolean }
   ): Promise<PublishResult> {
+    // 优先尝试使用 Python 服务
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      logger.info('[Douyin] Python service available, using Python method');
+      try {
+        return await this.publishVideoViaPython(cookies, params, onProgress);
+      } catch (pythonError) {
+        logger.warn('[Douyin] Python publish failed, falling back to Playwright:', pythonError);
+        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
+      }
+    } else {
+      logger.info('[Douyin] Python service not available, using Playwright method');
+    }
+
+    // 回退到 Playwright 方式
     const useHeadless = options?.headless ?? true;
     
     try {

+ 6 - 0
server/src/automation/platforms/index.ts

@@ -3,11 +3,15 @@ import { BasePlatformAdapter } from './base.js';
 import { DouyinAdapter } from './douyin.js';
 import { KuaishouAdapter } from './kuaishou.js';
 import { BilibiliAdapter } from './bilibili.js';
+import { XiaohongshuAdapter } from './xiaohongshu.js';
+import { WeixinAdapter } from './weixin.js';
 
 export { BasePlatformAdapter } from './base.js';
 export { DouyinAdapter } from './douyin.js';
 export { KuaishouAdapter } from './kuaishou.js';
 export { BilibiliAdapter } from './bilibili.js';
+export { XiaohongshuAdapter } from './xiaohongshu.js';
+export { WeixinAdapter } from './weixin.js';
 
 /**
  * 平台适配器注册表
@@ -16,6 +20,8 @@ const adapterRegistry: Map<PlatformType, new () => BasePlatformAdapter> = new Ma
   ['douyin', DouyinAdapter],
   ['kuaishou', KuaishouAdapter],
   ['bilibili', BilibiliAdapter],
+  ['xiaohongshu', XiaohongshuAdapter],
+  ['weixin_video', WeixinAdapter],
 ]);
 
 /**

+ 93 - 1
server/src/automation/platforms/kuaishou.ts

@@ -10,6 +10,9 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 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';
+
 /**
  * 快手平台适配器
  */
@@ -120,7 +123,96 @@ export class KuaishouAdapter extends BasePlatformAdapter {
     }
   }
   
-  async publishVideo(cookies: string, params: PublishParams): Promise<PublishResult> {
+  /**
+   * 检查 Python 发布服务是否可用
+   */
+  private async checkPythonServiceAvailable(): Promise<boolean> {
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+        method: 'GET',
+        signal: AbortSignal.timeout(3000),
+      });
+      if (response.ok) {
+        const data = await response.json();
+        return data.status === 'ok' && data.supported_platforms?.includes('kuaishou');
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 通过 Python 服务发布视频(参考 matrix 项目)
+   */
+  private async publishVideoViaPython(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Kuaishou Python] Starting publish via Python service...');
+    onProgress?.(5, '正在通过 Python 服务发布...');
+
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          platform: 'kuaishou',
+          cookie: cookies,
+          title: params.title,
+          description: params.description || params.title,
+          video_path: params.videoPath,
+          cover_path: params.coverPath,
+          tags: params.tags || [],
+          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+          location: params.location || '重庆市',
+        }),
+        signal: AbortSignal.timeout(600000),
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        onProgress?.(100, '发布成功');
+        logger.info('[Kuaishou Python] Publish successful');
+        return {
+          success: true,
+          videoId: `kuaishou_${Date.now()}`,
+          videoUrl: '',
+          message: '发布成功',
+        };
+      } else {
+        throw new Error(result.error || '发布失败');
+      }
+    } catch (error) {
+      logger.error('[Kuaishou Python] Publish failed:', error);
+      throw error;
+    }
+  }
+
+  async publishVideo(
+    cookies: string, 
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    // 优先尝试使用 Python 服务
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      logger.info('[Kuaishou] Python service available, using Python method');
+      try {
+        return await this.publishVideoViaPython(cookies, params, onProgress);
+      } catch (pythonError) {
+        logger.warn('[Kuaishou] Python publish failed, falling back to Playwright:', pythonError);
+        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
+      }
+    } else {
+      logger.info('[Kuaishou] Python service not available, using Playwright method');
+    }
+
+    // 回退到 Playwright 方式
     try {
       await this.initBrowser();
       await this.setCookies(cookies);

+ 330 - 0
server/src/automation/platforms/weixin.ts

@@ -0,0 +1,330 @@
+/// <reference lib="dom" />
+import { BasePlatformAdapter } from './base.js';
+import type {
+  AccountProfile,
+  PublishParams,
+  PublishResult,
+  DateRange,
+  AnalyticsData,
+  CommentData,
+} from './base.js';
+import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
+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';
+
+/**
+ * 微信视频号平台适配器
+ * 参考: matrix/tencent_uploader/main.py
+ */
+export class WeixinAdapter extends BasePlatformAdapter {
+  readonly platform: PlatformType = 'weixin_video';
+  readonly loginUrl = 'https://channels.weixin.qq.com/platform';
+  readonly publishUrl = 'https://channels.weixin.qq.com/platform/post/create';
+  
+  protected getCookieDomain(): string {
+    return '.weixin.qq.com';
+  }
+  
+  async getQRCode(): Promise<QRCodeInfo> {
+    try {
+      await this.initBrowser();
+      
+      if (!this.page) throw new Error('Page not initialized');
+      
+      // 访问登录页面
+      await this.page.goto('https://channels.weixin.qq.com/platform/login-for-iframe?dark_mode=true&host_type=1');
+      
+      // 点击二维码切换
+      await this.page.locator('.qrcode').click();
+      
+      // 获取二维码
+      const qrcodeImg = await this.page.locator('img.qrcode').getAttribute('src');
+      
+      if (!qrcodeImg) {
+        throw new Error('Failed to get QR code');
+      }
+      
+      return {
+        qrcodeUrl: qrcodeImg,
+        qrcodeKey: `weixin_${Date.now()}`,
+        expireTime: Date.now() + 300000,
+      };
+    } catch (error) {
+      logger.error('Weixin getQRCode error:', error);
+      throw error;
+    }
+  }
+  
+  async checkQRCodeStatus(qrcodeKey: string): Promise<LoginStatusResult> {
+    try {
+      if (!this.page) {
+        return { status: 'expired', message: '二维码已过期' };
+      }
+      
+      // 检查是否扫码成功
+      const maskDiv = this.page.locator('.mask').first();
+      const className = await maskDiv.getAttribute('class');
+      
+      if (className && className.includes('show')) {
+        // 等待登录完成
+        await this.page.waitForTimeout(3000);
+        
+        const cookies = await this.getCookies();
+        if (cookies && cookies.length > 10) {
+          await this.closeBrowser();
+          return { status: 'success', message: '登录成功', cookies };
+        }
+      }
+      
+      return { status: 'waiting', message: '等待扫码' };
+    } catch (error) {
+      logger.error('Weixin checkQRCodeStatus error:', error);
+      return { status: 'error', message: '检查状态失败' };
+    }
+  }
+  
+  async checkLoginStatus(cookies: string): Promise<boolean> {
+    try {
+      await this.initBrowser();
+      await this.setCookies(cookies);
+      
+      if (!this.page) throw new Error('Page not initialized');
+      
+      await this.page.goto(this.publishUrl);
+      await this.page.waitForLoadState('networkidle');
+      
+      // 检查是否需要登录
+      const needLogin = await this.page.$('div.title-name:has-text("视频号小店")');
+      await this.closeBrowser();
+      
+      return !needLogin;
+    } catch (error) {
+      logger.error('Weixin checkLoginStatus error:', error);
+      await this.closeBrowser();
+      return false;
+    }
+  }
+  
+  async getAccountInfo(cookies: string): Promise<AccountProfile> {
+    try {
+      await this.initBrowser();
+      await this.setCookies(cookies);
+      
+      if (!this.page) throw new Error('Page not initialized');
+      
+      await this.page.goto(this.loginUrl);
+      await this.page.waitForLoadState('networkidle');
+      
+      // 获取账号信息
+      const accountId = await this.page.$eval('span.finder-uniq-id', el => el.textContent?.trim() || '').catch(() => '');
+      const accountName = await this.page.$eval('h2.finder-nickname', el => el.textContent?.trim() || '').catch(() => '');
+      const avatarUrl = await this.page.$eval('img.avatar', el => el.getAttribute('src') || '').catch(() => '');
+      
+      await this.closeBrowser();
+      
+      return {
+        accountId: accountId || `weixin_${Date.now()}`,
+        accountName: accountName || '视频号账号',
+        avatarUrl,
+        fansCount: 0,
+        worksCount: 0,
+      };
+    } catch (error) {
+      logger.error('Weixin getAccountInfo error:', error);
+      await this.closeBrowser();
+      return {
+        accountId: `weixin_${Date.now()}`,
+        accountName: '视频号账号',
+        avatarUrl: '',
+        fansCount: 0,
+        worksCount: 0,
+      };
+    }
+  }
+  
+  /**
+   * 检查 Python 发布服务是否可用
+   */
+  private async checkPythonServiceAvailable(): Promise<boolean> {
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+        method: 'GET',
+        signal: AbortSignal.timeout(3000),
+      });
+      if (response.ok) {
+        const data = await response.json();
+        return data.status === 'ok' && data.supported_platforms?.includes('weixin');
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 通过 Python 服务发布视频
+   */
+  private async publishVideoViaPython(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Weixin Python] Starting publish via Python service...');
+    onProgress?.(5, '正在通过 Python 服务发布...');
+
+    try {
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          platform: 'weixin',
+          cookie: cookies,
+          title: params.title,
+          description: params.description || params.title,
+          video_path: params.videoPath,
+          cover_path: params.coverPath,
+          tags: params.tags || [],
+          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+          location: params.location || '重庆市',
+        }),
+        signal: AbortSignal.timeout(600000),
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        onProgress?.(100, '发布成功');
+        logger.info('[Weixin Python] Publish successful');
+        return {
+          success: true,
+          videoId: result.video_id || `weixin_${Date.now()}`,
+          videoUrl: result.video_url || '',
+          message: '发布成功',
+        };
+      } else {
+        throw new Error(result.error || '发布失败');
+      }
+    } catch (error) {
+      logger.error('[Weixin Python] Publish failed:', error);
+      throw error;
+    }
+  }
+  
+  async publishVideo(
+    cookies: string, 
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    // 优先尝试使用 Python 服务
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      logger.info('[Weixin] Python service available, using Python method');
+      try {
+        return await this.publishVideoViaPython(cookies, params, onProgress);
+      } catch (pythonError) {
+        logger.warn('[Weixin] Python publish failed, falling back to Playwright:', pythonError);
+        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
+      }
+    } else {
+      logger.info('[Weixin] Python service not available, using Playwright method');
+    }
+
+    // 回退到 Playwright 方式
+    try {
+      await this.initBrowser();
+      await this.setCookies(cookies);
+      
+      if (!this.page) throw new Error('Page not initialized');
+      
+      onProgress?.(10, '正在打开上传页面...');
+      
+      await this.page.goto(this.publishUrl);
+      await this.page.waitForLoadState('networkidle');
+      
+      onProgress?.(15, '正在选择视频文件...');
+      
+      // 上传视频
+      const uploadDiv = this.page.locator('div.upload-content');
+      const [fileChooser] = await Promise.all([
+        this.page.waitForEvent('filechooser'),
+        uploadDiv.click(),
+      ]);
+      await fileChooser.setFiles(params.videoPath);
+      
+      onProgress?.(20, '正在填写标题...');
+      
+      // 填写标题和话题
+      await this.page.locator('div.input-editor').click();
+      await this.page.keyboard.type(params.title);
+      
+      if (params.tags && params.tags.length > 0) {
+        await this.page.keyboard.press('Enter');
+        for (const tag of params.tags) {
+          await this.page.keyboard.type('#' + tag);
+          await this.page.keyboard.press('Space');
+        }
+      }
+      
+      onProgress?.(40, '等待视频上传完成...');
+      
+      // 等待上传完成
+      for (let i = 0; i < 120; i++) {
+        const buttonClass = await this.page.getByRole('button', { name: '发表' }).getAttribute('class');
+        if (buttonClass && !buttonClass.includes('disabled')) {
+          break;
+        }
+        await this.page.waitForTimeout(3000);
+      }
+      
+      onProgress?.(80, '正在发布...');
+      
+      // 点击发布
+      await this.page.locator('div.form-btns button:has-text("发表")').click();
+      
+      // 等待跳转
+      try {
+        await this.page.waitForURL('**/post/list', { timeout: 30000 });
+      } catch {
+        // 检查是否已经在列表页
+      }
+      
+      await this.closeBrowser();
+      
+      onProgress?.(100, '发布成功');
+      
+      return { success: true, message: '发布成功' };
+    } catch (error) {
+      logger.error('Weixin publishVideo error:', error);
+      await this.closeBrowser();
+      return {
+        success: false,
+        errorMessage: error instanceof Error ? error.message : '发布失败',
+      };
+    }
+  }
+  
+  async getComments(cookies: string, videoId: string): Promise<CommentData[]> {
+    logger.warn('Weixin getComments not implemented');
+    return [];
+  }
+  
+  async replyComment(cookies: string, videoId: string, commentId: string, content: string): Promise<boolean> {
+    logger.warn('Weixin replyComment not implemented');
+    return false;
+  }
+  
+  async getAnalytics(cookies: string, dateRange?: DateRange): Promise<AnalyticsData> {
+    logger.warn('Weixin getAnalytics not implemented');
+    return {
+      totalViews: 0,
+      totalLikes: 0,
+      totalComments: 0,
+      totalShares: 0,
+      periodViews: [],
+    };
+  }
+}

+ 1139 - 0
server/src/automation/platforms/xiaohongshu.ts

@@ -0,0 +1,1139 @@
+/// <reference lib="dom" />
+import { BasePlatformAdapter } from './base.js';
+import type {
+  AccountProfile,
+  PublishParams,
+  PublishResult,
+  DateRange,
+  AnalyticsData,
+  CommentData,
+  WorkItem,
+} from './base.js';
+import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
+import { logger } from '../../utils/logger.js';
+
+// 小红书 Python API 服务配置
+const XHS_PYTHON_SERVICE_URL = process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+
+/**
+ * 小红书平台适配器
+ */
+export class XiaohongshuAdapter extends BasePlatformAdapter {
+  readonly platform: PlatformType = 'xiaohongshu';
+  readonly loginUrl = 'https://creator.xiaohongshu.com/';
+  readonly publishUrl = 'https://creator.xiaohongshu.com/publish/publish';
+  readonly creatorHomeUrl = 'https://creator.xiaohongshu.com/creator/home';
+  readonly contentManageUrl = 'https://creator.xiaohongshu.com/creator/content';
+
+  protected getCookieDomain(): string {
+    return '.xiaohongshu.com';
+  }
+
+  /**
+   * 获取扫码登录二维码
+   */
+  async getQRCode(): Promise<QRCodeInfo> {
+    try {
+      await this.initBrowser();
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      // 访问创作者中心
+      await this.page.goto(this.loginUrl);
+
+      // 等待二维码出现
+      await this.waitForSelector('[class*="qrcode"] img, .qrcode-image img', 30000);
+
+      // 获取二维码图片
+      const qrcodeImg = await this.page.$('[class*="qrcode"] img, .qrcode-image img');
+      const qrcodeUrl = await qrcodeImg?.getAttribute('src');
+
+      if (!qrcodeUrl) {
+        throw new Error('Failed to get QR code');
+      }
+
+      const qrcodeKey = `xiaohongshu_${Date.now()}`;
+
+      return {
+        qrcodeUrl,
+        qrcodeKey,
+        expireTime: Date.now() + 300000, // 5分钟过期
+      };
+    } catch (error) {
+      logger.error('Xiaohongshu getQRCode error:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 检查扫码状态
+   */
+  async checkQRCodeStatus(qrcodeKey: string): Promise<LoginStatusResult> {
+    try {
+      if (!this.page) {
+        return { status: 'expired', message: '二维码已过期' };
+      }
+
+      // 检查是否登录成功(URL 变化)
+      const currentUrl = this.page.url();
+
+      if (currentUrl.includes('/creator/home') || currentUrl.includes('/publish')) {
+        // 登录成功,获取 cookie
+        const cookies = await this.getCookies();
+        await this.closeBrowser();
+
+        return {
+          status: 'success',
+          message: '登录成功',
+          cookies,
+        };
+      }
+
+      // 检查是否扫码
+      const scanTip = await this.page.$('[class*="scan-success"], [class*="scanned"]');
+      if (scanTip) {
+        return { status: 'scanned', message: '已扫码,请确认登录' };
+      }
+
+      return { status: 'waiting', message: '等待扫码' };
+    } catch (error) {
+      logger.error('Xiaohongshu checkQRCodeStatus error:', error);
+      return { status: 'error', message: '检查状态失败' };
+    }
+  }
+
+  /**
+   * 检查登录状态
+   */
+  async checkLoginStatus(cookies: string): Promise<boolean> {
+    try {
+      await this.initBrowser({ headless: true });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      // 访问创作者中心
+      await this.page.goto(this.creatorHomeUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      await this.page.waitForTimeout(3000);
+
+      const url = this.page.url();
+      logger.info(`Xiaohongshu checkLoginStatus URL: ${url}`);
+
+      // 如果被重定向到登录页面,说明未登录
+      const isLoginPage = url.includes('login') || url.includes('passport');
+
+      await this.closeBrowser();
+
+      return !isLoginPage;
+    } catch (error) {
+      logger.error('Xiaohongshu checkLoginStatus error:', error);
+      await this.closeBrowser();
+      return false;
+    }
+  }
+
+  /**
+   * 获取账号信息
+   */
+  async getAccountInfo(cookies: string): Promise<AccountProfile> {
+    try {
+      await this.initBrowser({ headless: true });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      let accountId = `xiaohongshu_${Date.now()}`;
+      let accountName = '小红书账号';
+      let avatarUrl = '';
+      let fansCount = 0;
+      let worksCount = 0;
+      let worksList: WorkItem[] = [];
+
+      // 用于捕获 API 响应
+      const capturedData: {
+        userInfo?: {
+          nickname?: string;
+          avatar?: string;
+          userId?: string;
+          redId?: string;
+          fans?: number;
+          notes?: number;
+        };
+      } = {};
+
+      // 设置 API 响应监听器
+      this.page.on('response', async (response) => {
+        const url = response.url();
+        try {
+          // 监听用户信息 API
+          if (url.includes('/api/galaxy/creator/home/personal_info') ||
+            url.includes('/api/sns/web/v1/user/selfinfo') ||
+            url.includes('/user/selfinfo')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] User info response:`, JSON.stringify(data).slice(0, 500));
+
+            const userInfo = data?.data?.user_info || data?.data || data;
+            if (userInfo) {
+              capturedData.userInfo = {
+                nickname: userInfo.nickname || userInfo.name || userInfo.userName,
+                avatar: userInfo.image || userInfo.avatar || userInfo.images,
+                userId: userInfo.user_id || userInfo.userId,
+                redId: userInfo.red_id || userInfo.redId,
+                fans: userInfo.fans || userInfo.fansCount,
+                notes: userInfo.notes || userInfo.noteCount,
+              };
+              logger.info(`[Xiaohongshu API] Captured user info:`, capturedData.userInfo);
+            }
+          }
+
+          // 监听创作者主页数据
+          if (url.includes('/api/galaxy/creator/home/home_page') ||
+            url.includes('/api/galaxy/creator/data')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] Creator home response:`, JSON.stringify(data).slice(0, 500));
+
+            if (data?.data) {
+              const homeData = data.data;
+              if (homeData.fans_count !== undefined) {
+                capturedData.userInfo = capturedData.userInfo || {};
+                capturedData.userInfo.fans = homeData.fans_count;
+              }
+              if (homeData.note_count !== undefined) {
+                capturedData.userInfo = capturedData.userInfo || {};
+                capturedData.userInfo.notes = homeData.note_count;
+              }
+            }
+          }
+        } catch {
+          // 忽略非 JSON 响应
+        }
+      });
+
+      // 访问创作者中心
+      logger.info('[Xiaohongshu] Navigating to creator center...');
+      await this.page.goto(this.creatorHomeUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      await this.page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = this.page.url();
+      if (currentUrl.includes('login') || currentUrl.includes('passport')) {
+        logger.warn('[Xiaohongshu] Cookie expired, needs login');
+        await this.closeBrowser();
+        return {
+          accountId,
+          accountName,
+          avatarUrl,
+          fansCount,
+          worksCount,
+        };
+      }
+
+      // 等待 API 响应
+      await this.page.waitForTimeout(3000);
+
+      // 使用捕获的数据
+      if (capturedData.userInfo) {
+        if (capturedData.userInfo.nickname) {
+          accountName = capturedData.userInfo.nickname;
+        }
+        if (capturedData.userInfo.avatar) {
+          avatarUrl = capturedData.userInfo.avatar;
+        }
+        if (capturedData.userInfo.userId) {
+          accountId = `xiaohongshu_${capturedData.userInfo.userId}`;
+        } else if (capturedData.userInfo.redId) {
+          accountId = `xiaohongshu_${capturedData.userInfo.redId}`;
+        }
+        if (capturedData.userInfo.fans) {
+          fansCount = capturedData.userInfo.fans;
+        }
+        if (capturedData.userInfo.notes) {
+          worksCount = capturedData.userInfo.notes;
+        }
+      }
+
+      // 尝试获取作品列表
+      try {
+        await this.page.goto(this.contentManageUrl, {
+          waitUntil: 'domcontentloaded',
+          timeout: 30000,
+        });
+        await this.page.waitForTimeout(3000);
+
+        worksList = await this.page.evaluate(() => {
+          const items: WorkItem[] = [];
+          const cards = document.querySelectorAll('[class*="note-item"], [class*="content-item"]');
+
+          cards.forEach((card) => {
+            try {
+              const coverImg = card.querySelector('img');
+              const coverUrl = coverImg?.src || '';
+
+              const titleEl = card.querySelector('[class*="title"], [class*="desc"]');
+              const title = titleEl?.textContent?.trim() || '无标题';
+
+              const timeEl = card.querySelector('[class*="time"], [class*="date"]');
+              const publishTime = timeEl?.textContent?.trim() || '';
+
+              const statusEl = card.querySelector('[class*="status"]');
+              const status = statusEl?.textContent?.trim() || '';
+
+              // 获取数据指标
+              const statsEl = card.querySelector('[class*="stats"], [class*="data"]');
+              const statsText = statsEl?.textContent || '';
+              
+              const likeMatch = statsText.match(/(\d+)\s*赞/);
+              const commentMatch = statsText.match(/(\d+)\s*评/);
+              const collectMatch = statsText.match(/(\d+)\s*藏/);
+
+              items.push({
+                title,
+                coverUrl,
+                duration: '',
+                publishTime,
+                status,
+                playCount: 0,
+                likeCount: likeMatch ? parseInt(likeMatch[1]) : 0,
+                commentCount: commentMatch ? parseInt(commentMatch[1]) : 0,
+                shareCount: collectMatch ? parseInt(collectMatch[1]) : 0,
+              });
+            } catch {}
+          });
+
+          return items;
+        });
+
+        logger.info(`[Xiaohongshu] Fetched ${worksList.length} works`);
+      } catch (e) {
+        logger.warn('[Xiaohongshu] Failed to fetch works list:', e);
+      }
+
+      await this.closeBrowser();
+
+      logger.info(`[Xiaohongshu] Account info: ${accountName}, ID: ${accountId}, Fans: ${fansCount}, Works: ${worksCount}`);
+
+      return {
+        accountId,
+        accountName,
+        avatarUrl,
+        fansCount,
+        worksCount,
+        worksList,
+      };
+    } catch (error) {
+      logger.error('Xiaohongshu getAccountInfo error:', error);
+      await this.closeBrowser();
+      return {
+        accountId: `xiaohongshu_${Date.now()}`,
+        accountName: '小红书账号',
+        avatarUrl: '',
+        fansCount: 0,
+        worksCount: 0,
+      };
+    }
+  }
+
+  /**
+   * 检查 Python API 服务是否可用
+   */
+  private async checkPythonServiceAvailable(): Promise<boolean> {
+    try {
+      const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/health`, {
+        method: 'GET',
+        signal: AbortSignal.timeout(3000),
+      });
+      if (response.ok) {
+        const data = await response.json();
+        return data.status === 'ok' && data.xhs_sdk === true;
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 通过 Python API 服务发布视频(推荐方式,更稳定)
+   * 参考: matrix 项目的小红书发布逻辑
+   */
+  private async publishVideoViaApi(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Xiaohongshu API] Starting publish via Python API service...');
+    onProgress?.(5, '正在通过 API 发布...');
+
+    try {
+      // 准备 cookie 字符串
+      let cookieStr = cookies;
+      
+      // 如果 cookies 是 JSON 数组格式,转换为字符串格式
+      try {
+        const cookieArray = JSON.parse(cookies);
+        if (Array.isArray(cookieArray)) {
+          cookieStr = cookieArray.map((c: { name: string; value: string }) => `${c.name}=${c.value}`).join('; ');
+        }
+      } catch {
+        // 已经是字符串格式
+      }
+
+      onProgress?.(10, '正在上传视频...');
+
+      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,
+        }),
+        signal: AbortSignal.timeout(300000), // 5分钟超时
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        onProgress?.(100, '发布成功');
+        logger.info('[Xiaohongshu API] Publish successful:', result.data);
+        return {
+          success: true,
+          videoId: result.data?.note_id || `xhs_${Date.now()}`,
+          videoUrl: result.data?.url || '',
+          message: '发布成功',
+        };
+      } else {
+        throw new Error(result.error || '发布失败');
+      }
+    } catch (error) {
+      logger.error('[Xiaohongshu API] Publish failed:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 发布视频/笔记
+   * 优先使用 Python API 服务(更稳定),如果不可用则回退到 Playwright 方式
+   */
+  async publishVideo(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; phone?: string }) => Promise<string>,
+    options?: { headless?: boolean }
+  ): Promise<PublishResult> {
+    // 优先尝试使用 Python API 服务
+    const apiAvailable = await this.checkPythonServiceAvailable();
+    if (apiAvailable) {
+      logger.info('[Xiaohongshu] Python API service available, using API method');
+      try {
+        return await this.publishVideoViaApi(cookies, params, onProgress);
+      } catch (apiError) {
+        logger.warn('[Xiaohongshu] API publish failed, falling back to Playwright:', apiError);
+        onProgress?.(0, 'API发布失败,正在切换到浏览器模式...');
+      }
+    } else {
+      logger.info('[Xiaohongshu] Python API service not available, using Playwright method');
+    }
+
+    // 回退到 Playwright 方式
+    const useHeadless = options?.headless ?? true;
+
+    try {
+      await this.initBrowser({ headless: useHeadless });
+      await this.setCookies(cookies);
+
+      if (!useHeadless) {
+        logger.info('[Xiaohongshu Publish] Running in HEADFUL mode');
+        onProgress?.(1, '已打开浏览器窗口,请注意查看...');
+      }
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      // 检查视频文件是否存在
+      const fs = await import('fs');
+      if (!fs.existsSync(params.videoPath)) {
+        throw new Error(`视频文件不存在: ${params.videoPath}`);
+      }
+
+      onProgress?.(5, '正在打开发布页面...');
+      logger.info(`[Xiaohongshu Publish] Starting upload for: ${params.videoPath}`);
+
+      // 访问发布页面
+      await this.page.goto(this.publishUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 60000,
+      });
+
+      await this.page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = this.page.url();
+      if (currentUrl.includes('login') || currentUrl.includes('passport')) {
+        throw new Error('登录已过期,请重新登录');
+      }
+
+      logger.info(`[Xiaohongshu Publish] Page loaded: ${currentUrl}`);
+
+      onProgress?.(10, '正在选择视频文件...');
+
+      // 确保在"上传视频"标签页
+      try {
+        const videoTab = this.page.locator('div.tab:has-text("上传视频"), span:has-text("上传视频")').first();
+        if (await videoTab.count() > 0) {
+          await videoTab.click();
+          await this.page.waitForTimeout(1000);
+          logger.info('[Xiaohongshu Publish] Clicked video tab');
+        }
+      } catch {}
+
+      // 上传视频文件 - 小红书需要点击"上传视频"按钮触发文件选择
+      let uploadTriggered = false;
+      
+      // 方法1: 点击"上传视频"按钮触发 file chooser
+      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) {
+          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;
+            }
+          } catch (e) {
+            logger.warn(`[Xiaohongshu Publish] Button click failed for ${selector}`);
+          }
+        }
+      } catch (e) {
+        logger.warn('[Xiaohongshu Publish] Upload button method failed:', e);
+      }
+
+      // 方法2: 点击上传区域(拖拽区域)
+      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}`);
+              const [fileChooser] = await Promise.all([
+                this.page.waitForEvent('filechooser', { timeout: 15000 }),
+                uploadArea.click(),
+              ]);
+              await fileChooser.setFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info('[Xiaohongshu Publish] File selected via file chooser (area click)');
+              break;
+            }
+          }
+        } catch (e) {
+          logger.warn('[Xiaohongshu Publish] Upload area method failed:', e);
+        }
+      }
+
+      // 方法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) {
+          try {
+            const fileInput = await this.page.$(selector);
+            if (fileInput) {
+              await fileInput.setInputFiles(params.videoPath);
+              uploadTriggered = true;
+              logger.info(`[Xiaohongshu Publish] File set via direct input: ${selector}`);
+              
+              // 直接设置后需要等待一下,让页面响应
+              await this.page.waitForTimeout(2000);
+              
+              // 检查页面是否有变化
+              const hasChange = await this.page.locator('[class*="video-preview"], video, [class*="progress"], [class*="upload-success"]').count() > 0;
+              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}`);
+          }
+        }
+      }
+
+      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}`);
+        throw new Error('无法上传视频文件');
+      }
+
+      onProgress?.(15, '视频上传中...');
+
+      // 等待视频上传完成
+      const maxWaitTime = 300000; // 5分钟
+      const startTime = Date.now();
+
+      while (Date.now() - startTime < maxWaitTime) {
+        await this.page.waitForTimeout(3000);
+
+        // 检查当前URL是否变化(上传成功后可能跳转)
+        const newUrl = this.page.url();
+        if (newUrl !== currentUrl && !newUrl.includes('upload')) {
+          logger.info(`[Xiaohongshu Publish] URL changed to: ${newUrl}`);
+        }
+
+        // 检查上传进度
+        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.4), `视频上传中: ${progress}%`);
+            logger.info(`[Xiaohongshu Publish] Upload progress: ${progress}%`);
+          }
+        }
+
+        // 检查是否上传完成 - 扩展检测范围
+        const uploadCompleteSelectors = [
+          '[class*="upload-success"]',
+          '[class*="video-preview"]',
+          'video',
+          '[class*="cover"]', // 封面设置区域
+          'input[placeholder*="标题"]', // 标题输入框出现
+          '[class*="title"] input',
+          '[class*="editor"]', // 编辑器区域
+        ];
+        
+        for (const selector of uploadCompleteSelectors) {
+          const count = await this.page.locator(selector).count();
+          if (count > 0) {
+            logger.info(`[Xiaohongshu Publish] Video upload completed, found: ${selector}`);
+            break;
+          }
+        }
+        
+        // 如果标题输入框出现,说明可以开始填写了
+        const titleInput = await this.page.locator('input[placeholder*="标题"]').count();
+        if (titleInput > 0) {
+          logger.info('[Xiaohongshu Publish] Title input found, upload must be complete');
+          break;
+        }
+
+        // 检查是否上传失败
+        const failText = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (failText && failText.includes('失败')) {
+          throw new Error(`视频上传失败: ${failText}`);
+        }
+        
+        // 检查是否还在初始上传页面
+        const stillOnUploadPage = await this.page.locator('div:has-text("拖拽视频到此")').count();
+        if (stillOnUploadPage > 0 && Date.now() - startTime > 10000) {
+          logger.warn('[Xiaohongshu Publish] Still on upload page after 10s, retrying upload...');
+          // 可能需要重新触发上传
+          break;
+        }
+      }
+
+      onProgress?.(55, '正在填写笔记信息...');
+
+      // 填写标题
+      logger.info('[Xiaohongshu Publish] Filling title...');
+      const titleSelectors = [
+        'input[placeholder*="标题"]',
+        '[class*="title"] input',
+        'textarea[placeholder*="标题"]',
+      ];
+
+      for (const selector of titleSelectors) {
+        const titleInput = this.page.locator(selector).first();
+        if (await titleInput.count() > 0) {
+          await titleInput.fill(params.title.slice(0, 20)); // 小红书标题限制20字
+          logger.info(`[Xiaohongshu Publish] Title filled via: ${selector}`);
+          break;
+        }
+      }
+
+      // 填写描述/正文
+      if (params.description) {
+        logger.info('[Xiaohongshu Publish] Filling description...');
+        const descSelectors = [
+          '[class*="content-input"] [contenteditable="true"]',
+          'textarea[placeholder*="正文"]',
+          '[class*="editor"] [contenteditable="true"]',
+          '#post-textarea',
+        ];
+
+        for (const selector of descSelectors) {
+          const descInput = this.page.locator(selector).first();
+          if (await descInput.count() > 0) {
+            await descInput.click();
+            await this.page.keyboard.type(params.description, { delay: 30 });
+            logger.info(`[Xiaohongshu Publish] Description filled via: ${selector}`);
+            break;
+          }
+        }
+      }
+
+      onProgress?.(65, '正在添加话题标签...');
+
+      // 添加话题标签 - 注意不要触发话题选择器弹窗
+      // 小红书会自动识别 # 开头的话题,不需要从弹窗选择
+      if (params.tags && params.tags.length > 0) {
+        // 找到正文输入框
+        const descSelectors = [
+          '[class*="content-input"] [contenteditable="true"]',
+          '[class*="editor"] [contenteditable="true"]',
+          '#post-textarea',
+        ];
+        
+        for (const selector of descSelectors) {
+          const descInput = this.page.locator(selector).first();
+          if (await descInput.count() > 0) {
+            await descInput.click();
+            // 添加空行后再添加标签
+            await this.page.keyboard.press('Enter');
+            for (const tag of params.tags) {
+              await this.page.keyboard.type(`#${tag} `, { delay: 30 });
+            }
+            logger.info(`[Xiaohongshu Publish] Tags added: ${params.tags.join(', ')}`);
+            break;
+          }
+        }
+        
+        await this.page.waitForTimeout(500);
+      }
+
+      onProgress?.(75, '等待处理完成...');
+      
+      // 等待视频处理完成,检查是否有"上传成功"标识
+      await this.page.waitForTimeout(2000);
+      
+      // 检查当前页面是否还在编辑状态
+      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 });
+        throw new Error('页面状态异常,请重试');
+      }
+
+      onProgress?.(85, '正在发布...');
+
+      // 滚动到页面底部,确保发布按钮可见
+      logger.info('[Xiaohongshu Publish] Scrolling to bottom...');
+      await this.page.evaluate(() => {
+        window.scrollTo(0, document.body.scrollHeight);
+      });
+      await this.page.waitForTimeout(1000);
+
+      // 点击发布按钮
+      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}`);
+      
+      let publishClicked = false;
+      
+      // 方法1: 使用 Playwright locator 点击(模拟真实鼠标点击)
+      const publishBtnSelectors = [
+        'button.publishBtn',
+        '.publishBtn',
+        'button.d-button.red',
+      ];
+
+      for (const selector of publishBtnSelectors) {
+        try {
+          const btn = this.page.locator(selector).first();
+          const count = await btn.count();
+          logger.info(`[Xiaohongshu Publish] Checking selector ${selector}: count=${count}`);
+          if (count > 0 && await btn.isVisible()) {
+            // 确保按钮在视口内
+            await btn.scrollIntoViewIfNeeded();
+            await this.page.waitForTimeout(500);
+            
+            // 获取按钮位置并使用鼠标点击
+            const box = await btn.boundingBox();
+            if (box) {
+              // 使用 page.mouse.click 模拟真实鼠标点击
+              const x = box.x + box.width / 2;
+              const y = box.y + box.height / 2;
+              logger.info(`[Xiaohongshu Publish] Clicking at position: (${x}, ${y})`);
+              await this.page.mouse.click(x, y);
+              publishClicked = true;
+              logger.info(`[Xiaohongshu Publish] Publish button clicked via mouse.click: ${selector}`);
+              break;
+            }
+          }
+        } catch (e) {
+          logger.warn(`[Xiaohongshu Publish] Failed with selector ${selector}:`, e);
+        }
+      }
+      
+      // 方法2: 使用 Playwright locator.click() 配合 force 选项
+      if (!publishClicked) {
+        try {
+          const btn = this.page.locator('button.publishBtn').first();
+          if (await btn.count() > 0) {
+            logger.info('[Xiaohongshu Publish] Trying locator.click with force...');
+            await btn.click({ force: true, timeout: 5000 });
+            publishClicked = true;
+            logger.info('[Xiaohongshu Publish] Publish button clicked via locator.click(force)');
+          }
+        } catch (e) {
+          logger.warn('[Xiaohongshu Publish] locator.click(force) failed:', e);
+        }
+      }
+      
+      // 方法3: 使用 getByRole
+      if (!publishClicked) {
+        try {
+          const publishBtn = this.page.getByRole('button', { name: '发布', exact: true });
+          if (await publishBtn.count() > 0) {
+            const buttons = await publishBtn.all();
+            for (const btn of buttons) {
+              if (await btn.isVisible() && await btn.isEnabled()) {
+                // 使用鼠标点击
+                const box = await btn.boundingBox();
+                if (box) {
+                  const x = box.x + box.width / 2;
+                  const y = box.y + box.height / 2;
+                  await this.page.mouse.click(x, y);
+                  publishClicked = true;
+                  logger.info('[Xiaohongshu Publish] Publish button clicked via getByRole');
+                  break;
+                }
+              }
+            }
+          }
+        } catch (e) {
+          logger.warn('[Xiaohongshu Publish] getByRole failed:', e);
+        }
+      }
+
+      // 如果还是没找到,尝试用 evaluate 直接查找和点击
+      if (!publishClicked) {
+        logger.info('[Xiaohongshu Publish] Trying evaluate method...');
+        try {
+          publishClicked = await this.page.evaluate(() => {
+            // 查找所有包含"发布"文字的按钮
+            const buttons = Array.from(document.querySelectorAll('button, div[role="button"]'));
+            for (const btn of buttons) {
+              const text = btn.textContent?.trim();
+              // 找到只包含"发布"两个字的按钮(排除"发布笔记"等)
+              if (text === '发布' && (btn as HTMLElement).offsetParent !== null) {
+                (btn as HTMLElement).click();
+                return true;
+              }
+            }
+            return false;
+          });
+          if (publishClicked) {
+            logger.info('[Xiaohongshu Publish] Publish button clicked via evaluate');
+          }
+        } catch (e) {
+          logger.warn('[Xiaohongshu Publish] evaluate failed:', e);
+        }
+      }
+
+      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}`);
+        } catch {}
+        throw new Error('未找到发布按钮');
+      }
+
+      onProgress?.(90, '等待发布完成...');
+
+      // 等待发布结果
+      const publishMaxWait = 120000; // 2分钟
+      const publishStartTime = Date.now();
+
+      while (Date.now() - publishStartTime < publishMaxWait) {
+        await this.page.waitForTimeout(3000);
+        const currentUrl = this.page.url();
+
+        // 检查是否跳转到内容管理页面
+        if (currentUrl.includes('/content') || currentUrl.includes('/creator/home')) {
+          logger.info('[Xiaohongshu Publish] Publish success! Redirected to content page');
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: currentUrl,
+          };
+        }
+
+        // 检查成功提示
+        const successToast = await this.page.locator('[class*="success"]:has-text("成功"), [class*="toast"]:has-text("发布成功")').count();
+        if (successToast > 0) {
+          logger.info('[Xiaohongshu Publish] Found success toast');
+          await this.page.waitForTimeout(2000);
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        // 检查错误提示
+        const errorToast = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (errorToast && (errorToast.includes('失败') || errorToast.includes('错误'))) {
+          throw new Error(`发布失败: ${errorToast}`);
+        }
+
+        const elapsed = Math.floor((Date.now() - publishStartTime) / 1000);
+        onProgress?.(90 + Math.min(9, Math.floor(elapsed / 15)), `等待发布完成 (${elapsed}s)...`);
+      }
+
+      // 超时,截图调试
+      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}`);
+      } catch {}
+
+      throw new Error('发布超时,请手动检查是否发布成功');
+
+    } catch (error) {
+      logger.error('[Xiaohongshu Publish] Error:', error);
+      await this.closeBrowser();
+      return {
+        success: false,
+        errorMessage: error instanceof Error ? error.message : '发布失败',
+      };
+    }
+  }
+
+  /**
+   * 获取评论列表
+   */
+  async getComments(cookies: string, videoId: string): Promise<CommentData[]> {
+    try {
+      await this.initBrowser({ headless: true });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      const comments: CommentData[] = [];
+
+      // 设置 API 响应监听器
+      this.page.on('response', async (response) => {
+        const url = response.url();
+        try {
+          // 监听评论列表 API
+          if (url.includes('/api/sns/web/v2/comment/page') ||
+            url.includes('/api/galaxy/creator/comment')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] Comments response:`, JSON.stringify(data).slice(0, 500));
+
+            const commentList = data?.data?.comments || data?.comments || [];
+            for (const comment of commentList) {
+              comments.push({
+                commentId: comment.id || comment.comment_id || '',
+                authorId: comment.user_info?.user_id || comment.user_id || '',
+                authorName: comment.user_info?.nickname || comment.nickname || '',
+                authorAvatar: comment.user_info?.image || comment.avatar || '',
+                content: comment.content || '',
+                likeCount: comment.like_count || 0,
+                commentTime: comment.create_time || comment.time || '',
+                parentCommentId: comment.target_comment_id || undefined,
+              });
+            }
+          }
+        } catch {}
+      });
+
+      // 访问评论管理页面
+      await this.page.goto(`${this.contentManageUrl}?tab=comment`, {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      await this.page.waitForTimeout(5000);
+
+      await this.closeBrowser();
+      return comments;
+
+    } catch (error) {
+      logger.error('Xiaohongshu getComments error:', error);
+      await this.closeBrowser();
+      return [];
+    }
+  }
+
+  /**
+   * 回复评论
+   */
+  async replyComment(cookies: string, commentId: string, content: string): Promise<boolean> {
+    try {
+      await this.initBrowser({ headless: true });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      // 访问评论管理页面
+      await this.page.goto(`${this.contentManageUrl}?tab=comment`, {
+        waitUntil: 'networkidle',
+        timeout: 30000,
+      });
+
+      await this.page.waitForTimeout(2000);
+
+      // 找到对应评论并点击回复
+      const commentItem = this.page.locator(`[data-comment-id="${commentId}"], [data-id="${commentId}"]`).first();
+      if (await commentItem.count() > 0) {
+        const replyBtn = commentItem.locator('[class*="reply"], button:has-text("回复")').first();
+        if (await replyBtn.count() > 0) {
+          await replyBtn.click();
+          await this.page.waitForTimeout(500);
+        }
+      }
+
+      // 输入回复内容
+      const replyInput = this.page.locator('[class*="reply-input"] textarea, [class*="comment-input"] textarea').first();
+      if (await replyInput.count() > 0) {
+        await replyInput.fill(content);
+        await this.page.waitForTimeout(500);
+
+        // 点击发送
+        const sendBtn = this.page.locator('button:has-text("发送"), button:has-text("回复")').first();
+        if (await sendBtn.count() > 0) {
+          await sendBtn.click();
+          await this.page.waitForTimeout(2000);
+        }
+      }
+
+      await this.closeBrowser();
+      return true;
+
+    } catch (error) {
+      logger.error('Xiaohongshu replyComment error:', error);
+      await this.closeBrowser();
+      return false;
+    }
+  }
+
+  /**
+   * 获取数据统计
+   */
+  async getAnalytics(cookies: string, dateRange: DateRange): Promise<AnalyticsData> {
+    try {
+      await this.initBrowser({ headless: true });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      const analytics: AnalyticsData = {
+        fansCount: 0,
+        fansIncrease: 0,
+        viewsCount: 0,
+        likesCount: 0,
+        commentsCount: 0,
+        sharesCount: 0,
+      };
+
+      // 设置 API 响应监听器
+      this.page.on('response', async (response) => {
+        const url = response.url();
+        try {
+          if (url.includes('/api/galaxy/creator/data') ||
+            url.includes('/api/galaxy/creator/home')) {
+            const data = await response.json();
+            if (data?.data) {
+              const d = data.data;
+              analytics.fansCount = d.fans_count || analytics.fansCount;
+              analytics.fansIncrease = d.fans_increase || analytics.fansIncrease;
+              analytics.viewsCount = d.view_count || d.read_count || analytics.viewsCount;
+              analytics.likesCount = d.like_count || analytics.likesCount;
+              analytics.commentsCount = d.comment_count || analytics.commentsCount;
+              analytics.sharesCount = d.collect_count || analytics.sharesCount;
+            }
+          }
+        } catch {}
+      });
+
+      // 访问数据中心
+      await this.page.goto('https://creator.xiaohongshu.com/creator/data', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      await this.page.waitForTimeout(5000);
+      await this.closeBrowser();
+
+      return analytics;
+
+    } catch (error) {
+      logger.error('Xiaohongshu getAnalytics error:', error);
+      await this.closeBrowser();
+      return {
+        fansCount: 0,
+        fansIncrease: 0,
+        viewsCount: 0,
+        likesCount: 0,
+        commentsCount: 0,
+        sharesCount: 0,
+      };
+    }
+  }
+}

+ 23 - 3
server/src/services/CommentService.ts

@@ -227,7 +227,7 @@ export class CommentService {
     for (const account of accounts) {
       try {
         // 只处理支持的平台
-        if (account.platform !== 'douyin') {
+        if (account.platform !== 'douyin' && account.platform !== 'xiaohongshu') {
           logger.info(`Skipping unsupported platform: ${account.platform}`);
           continue;
         }
@@ -259,9 +259,29 @@ export class CommentService {
           }
         }
         
-        // 获取评论数据
+        // 获取评论数据 - 根据平台类型调用不同方法
         logger.info(`Syncing comments for account ${account.id} (${account.platform})...`);
-        const workComments = await headlessBrowserService.fetchDouyinCommentsViaApi(cookies);
+        let workComments: Array<{
+          videoId: string;
+          videoTitle: string;
+          comments: Array<{
+            commentId: string;
+            authorId: string;
+            authorName: string;
+            authorAvatar: string;
+            content: string;
+            likeCount: number;
+            commentTime: string;
+            replyCount?: number;
+            parentCommentId?: string;
+          }>;
+        }> = [];
+        
+        if (account.platform === 'douyin') {
+          workComments = await headlessBrowserService.fetchDouyinCommentsViaApi(cookies);
+        } else if (account.platform === 'xiaohongshu') {
+          workComments = await headlessBrowserService.fetchXiaohongshuCommentsViaApi(cookies);
+        }
         
         // 获取该账号的所有作品,用于关联
         const workRepository = AppDataSource.getRepository(Work);

+ 737 - 9
server/src/services/HeadlessBrowserService.ts

@@ -309,6 +309,9 @@ class HeadlessBrowserService {
         case 'kuaishou':
           accountInfo = await this.fetchKuaishouAccountInfo(page, context, cookies);
           break;
+        case 'xiaohongshu':
+          accountInfo = await this.fetchXiaohongshuAccountInfo(page, context, cookies);
+          break;
         default:
           accountInfo = this.getDefaultAccountInfo(platform);
       }
@@ -815,6 +818,517 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 获取小红书账号信息 - 通过 API 方式获取
+   */
+  private async fetchXiaohongshuAccountInfo(
+    page: Page,
+    _context: BrowserContext,
+    cookies: CookieData[]
+  ): Promise<AccountInfo> {
+    let accountId = `xiaohongshu_${Date.now()}`;
+    let accountName = '小红书账号';
+    let avatarUrl = '';
+    let fansCount = 0;
+    let worksCount = 0;
+
+    // 用于存储捕获的数据
+    const capturedData: {
+      userInfo?: {
+        nickname?: string;
+        avatar?: string;
+        userId?: string;
+        redId?: string;
+        fans?: number;
+        notes?: number;
+      };
+    } = {};
+
+    try {
+      // 从 Cookie 获取用户 ID
+      const userIdCookie = cookies.find(c =>
+        c.name === 'customer_id' || c.name === 'user_id' || c.name === 'web_session'
+      );
+      if (userIdCookie?.value) {
+        accountId = `xiaohongshu_${userIdCookie.value.slice(0, 20)}`;
+      }
+
+      // 设置 API 响应监听器
+      page.on('response', async (response) => {
+        const url = response.url();
+        try {
+          // 监听用户信息 API
+          if (url.includes('/api/galaxy/creator/home/personal_info') ||
+            url.includes('/api/sns/web/v1/user/selfinfo') ||
+            url.includes('/user/selfinfo')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] User info response:`, JSON.stringify(data).slice(0, 500));
+
+            // 解析用户信息
+            const userInfo = data?.data?.user_info || data?.data || data;
+            if (userInfo) {
+              capturedData.userInfo = {
+                nickname: userInfo.nickname || userInfo.name || userInfo.userName,
+                avatar: userInfo.image || userInfo.avatar || userInfo.images,
+                userId: userInfo.user_id || userInfo.userId,
+                redId: userInfo.red_id || userInfo.redId,
+                fans: userInfo.fans || userInfo.fansCount,
+                notes: userInfo.notes || userInfo.noteCount,
+              };
+              logger.info(`[Xiaohongshu API] Captured user info:`, capturedData.userInfo);
+            }
+          }
+
+          // 监听创作者主页数据
+          if (url.includes('/api/galaxy/creator/home/home_page') ||
+            url.includes('/api/galaxy/creator/data')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] Creator home response:`, JSON.stringify(data).slice(0, 500));
+
+            if (data?.data) {
+              const homeData = data.data;
+              // 获取粉丝数和笔记数
+              if (homeData.fans_count !== undefined) {
+                capturedData.userInfo = capturedData.userInfo || {};
+                capturedData.userInfo.fans = homeData.fans_count;
+              }
+              if (homeData.note_count !== undefined) {
+                capturedData.userInfo = capturedData.userInfo || {};
+                capturedData.userInfo.notes = homeData.note_count;
+              }
+            }
+          }
+        } catch {
+          // 忽略非 JSON 响应
+        }
+      });
+
+      // 导航到小红书创作者中心
+      logger.info('[Xiaohongshu] Navigating to creator center...');
+      await page.goto('https://creator.xiaohongshu.com/creator/home', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      // 等待页面加载
+      await page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = page.url();
+      if (currentUrl.includes('login') || currentUrl.includes('passport')) {
+        logger.warn('[Xiaohongshu] Cookie expired, needs login');
+        return this.getDefaultAccountInfo('xiaohongshu');
+      }
+
+      // 等待 API 响应
+      await page.waitForTimeout(3000);
+
+      // 如果 API 没有捕获到数据,尝试从页面提取
+      if (!capturedData.userInfo?.nickname) {
+        logger.info('[Xiaohongshu] API did not return data, extracting from page...');
+
+        // 尝试获取用户名
+        const nameSelectors = [
+          '[class*="nickname"]',
+          '[class*="user-name"]',
+          '[class*="userName"]',
+          '.user-info .name',
+          '[class*="creator"] [class*="name"]',
+        ];
+
+        for (const selector of nameSelectors) {
+          const el = await page.$(selector);
+          if (el) {
+            const text = await el.textContent();
+            if (text?.trim() && text.trim().length < 50) {
+              accountName = text.trim();
+              logger.info(`[Xiaohongshu] Found name from page: ${accountName}`);
+              break;
+            }
+          }
+        }
+
+        // 尝试获取头像
+        const avatarSelectors = [
+          '[class*="avatar"] img',
+          '[class*="user-avatar"] img',
+          '.user-info img',
+          '[class*="creator"] img[src*="sns"]',
+        ];
+
+        for (const selector of avatarSelectors) {
+          const el = await page.$(selector);
+          if (el) {
+            const src = await el.getAttribute('src');
+            if (src && src.startsWith('http')) {
+              avatarUrl = src;
+              logger.info(`[Xiaohongshu] Found avatar from page: ${avatarUrl.slice(0, 50)}...`);
+              break;
+            }
+          }
+        }
+
+        // 尝试获取粉丝数
+        const statsText = await page.textContent('body');
+        const fansMatch = statsText?.match(/粉丝[::\s]*(\d+(?:\.\d+)?[万亿]?)/);
+        if (fansMatch) {
+          fansCount = this.parseChineseNumber(fansMatch[1]);
+          logger.info(`[Xiaohongshu] Found fans count: ${fansCount}`);
+        }
+
+        const notesMatch = statsText?.match(/笔记[::\s]*(\d+)/);
+        if (notesMatch) {
+          worksCount = parseInt(notesMatch[1], 10);
+          logger.info(`[Xiaohongshu] Found notes count: ${worksCount}`);
+        }
+      }
+
+      // 使用捕获的数据
+      if (capturedData.userInfo) {
+        if (capturedData.userInfo.nickname) {
+          accountName = capturedData.userInfo.nickname;
+        }
+        if (capturedData.userInfo.avatar) {
+          avatarUrl = capturedData.userInfo.avatar;
+        }
+        if (capturedData.userInfo.userId) {
+          accountId = `xiaohongshu_${capturedData.userInfo.userId}`;
+        } else if (capturedData.userInfo.redId) {
+          accountId = `xiaohongshu_${capturedData.userInfo.redId}`;
+        }
+        if (capturedData.userInfo.fans) {
+          fansCount = capturedData.userInfo.fans;
+        }
+        if (capturedData.userInfo.notes) {
+          worksCount = capturedData.userInfo.notes;
+        }
+      }
+
+      logger.info(`[Xiaohongshu] Account info: id=${accountId}, name=${accountName}, fans=${fansCount}, works=${worksCount}`);
+
+      // 获取作品列表 - 通过监听 API 接口
+      const worksList: WorkItem[] = [];
+      try {
+        logger.info('[Xiaohongshu] Navigating to note manager page to fetch works...');
+
+        // 存储所有捕获的笔记数据
+        const allNotesData: Array<{
+          noteId: string;
+          title: string;
+          coverUrl: string;
+          status: number;
+          publishTime: string;
+          type: string;
+          duration: number;
+          likeCount: number;
+          commentCount: number;
+          collectCount: number;
+          viewCount: number;
+          shareCount: number;
+        }> = [];
+
+        let currentPage = 0;
+        let hasMorePages = true;
+        const maxPages = 20; // 最多获取20页,防止无限循环
+
+        // 设置 API 响应监听器 - 在导航之前绑定
+        let apiResponseReceived = false;
+        let totalNotesCount = 0; // 从 tags 中获取的总作品数
+
+        const notesApiHandler = async (response: import('playwright').Response) => {
+          const url = response.url();
+          try {
+            // 监听小红书笔记列表 API
+            // API: https://edith.xiaohongshu.com/web_api/sns/v5/creator/note/user/posted?tab=0&page=X
+            if (url.includes('/web_api/sns/v5/creator/note/user/posted') ||
+              url.includes('/api/sns/v5/creator/note/user/posted') ||
+              url.includes('creator/note/user/posted')) {
+              const data = await response.json();
+              logger.info(`[Xiaohongshu API] Notes list response: success=${data?.success}, code=${data?.code}, notes count=${data?.data?.notes?.length || 0}`);
+
+              if ((data?.success || data?.code === 0) && data?.data) {
+                apiResponseReceived = true;
+
+                // 从 tags 中获取总作品数
+                // tags 数组中 id="special.note_time_desc" 的项("所有笔记")包含总数
+                if (data.data.tags && Array.isArray(data.data.tags)) {
+                  const allNotesTag = data.data.tags.find((tag: { id?: string; notes_count?: number }) =>
+                    tag.id === 'special.note_time_desc'
+                  );
+                  if (allNotesTag?.notes_count !== undefined) {
+                    totalNotesCount = allNotesTag.notes_count;
+                    logger.info(`[Xiaohongshu API] Total notes count from tags: ${totalNotesCount}`);
+                  }
+                }
+
+                const notes = data.data.notes || [];
+                for (const note of notes) {
+                  // 根据 API 返回格式解析数据
+                  // images_list 是数组,第一个元素包含 url
+                  // 将 http:// 转换为 https:// 以确保图片能正常加载
+                  let coverUrl = note.images_list?.[0]?.url || '';
+                  if (coverUrl.startsWith('http://')) {
+                    coverUrl = coverUrl.replace('http://', 'https://');
+                  }
+                  const duration = note.video_info?.duration || 0;
+
+                  logger.info(`[Xiaohongshu API] Note: id=${note.id}, title="${note.display_title}", cover=${coverUrl ? coverUrl.slice(0, 60) + '...' : 'none'}`);
+
+                  allNotesData.push({
+                    noteId: note.id || '',
+                    title: note.display_title || '',
+                    coverUrl: coverUrl,
+                    status: note.tab_status || 1, // 1=已发布
+                    publishTime: note.time || '',
+                    type: note.type || 'normal', // video/normal
+                    duration: duration,
+                    likeCount: note.likes || 0,
+                    commentCount: note.comments_count || 0,
+                    collectCount: note.collected_count || 0,
+                    viewCount: note.view_count || 0,
+                    shareCount: note.shared_count || 0,
+                  });
+                }
+
+                // 检查是否还有更多页面
+                // page=-1 表示没有更多数据
+                if (data.data.page === -1 || notes.length === 0) {
+                  hasMorePages = false;
+                  logger.info(`[Xiaohongshu API] No more pages (page indicator: ${data.data.page})`);
+                }
+              } else {
+                hasMorePages = false;
+              }
+            }
+          } catch (e) {
+            // 只在有相关 URL 时打印警告
+            if (url.includes('creator/note')) {
+              logger.warn('[Xiaohongshu API] Failed to parse notes response:', e);
+            }
+          }
+        };
+
+        // 先绑定监听器
+        page.on('response', notesApiHandler);
+        logger.info('[Xiaohongshu] API listener registered, navigating to note manager...');
+
+        // 导航到笔记管理页面
+        await page.goto('https://creator.xiaohongshu.com/new/note-manager', {
+          waitUntil: 'networkidle',  // 等待网络空闲,确保 API 已响应
+          timeout: 30000,
+        });
+
+        // 等待初始页面加载和 API 响应
+        await page.waitForTimeout(5000);
+        logger.info(`[Xiaohongshu] After initial wait: apiResponseReceived=${apiResponseReceived}, notesCount=${allNotesData.length}`);
+
+        // 如果监听器没有捕获到数据,尝试直接调用 API
+        if (allNotesData.length === 0) {
+          logger.info('[Xiaohongshu] No notes captured via listener, trying direct API call...');
+
+          try {
+            // 直接在页面上下文中调用 API
+            const apiResponse = await page.evaluate(async () => {
+              const response = await fetch('https://edith.xiaohongshu.com/web_api/sns/v5/creator/note/user/posted?tab=0&page=0', {
+                method: 'GET',
+                credentials: 'include',
+                headers: {
+                  'Accept': 'application/json',
+                },
+              });
+              return await response.json();
+            });
+
+            logger.info(`[Xiaohongshu] Direct API call result: success=${apiResponse?.success}, code=${apiResponse?.code}`);
+
+            if ((apiResponse?.success || apiResponse?.code === 0) && apiResponse?.data) {
+              // 从 tags 中获取总作品数
+              if (apiResponse.data.tags && Array.isArray(apiResponse.data.tags)) {
+                const allNotesTag = apiResponse.data.tags.find((tag: { id?: string; notes_count?: number }) =>
+                  tag.id === 'special.note_time_desc'
+                );
+                if (allNotesTag?.notes_count !== undefined) {
+                  totalNotesCount = allNotesTag.notes_count;
+                  logger.info(`[Xiaohongshu API Direct] Total notes count from tags: ${totalNotesCount}`);
+                }
+              }
+
+              const notes = apiResponse.data.notes || [];
+              for (const note of notes) {
+                // 将 http:// 转换为 https://
+                let coverUrl = note.images_list?.[0]?.url || '';
+                if (coverUrl.startsWith('http://')) {
+                  coverUrl = coverUrl.replace('http://', 'https://');
+                }
+                const duration = note.video_info?.duration || 0;
+
+                logger.info(`[Xiaohongshu API Direct] Note: id=${note.id}, cover=${coverUrl ? coverUrl.slice(0, 60) + '...' : 'none'}`);
+
+                allNotesData.push({
+                  noteId: note.id || '',
+                  title: note.display_title || '',
+                  coverUrl: coverUrl,
+                  status: note.tab_status || 1,
+                  publishTime: note.time || '',
+                  type: note.type || 'normal',
+                  duration: duration,
+                  likeCount: note.likes || 0,
+                  commentCount: note.comments_count || 0,
+                  collectCount: note.collected_count || 0,
+                  viewCount: note.view_count || 0,
+                  shareCount: note.shared_count || 0,
+                });
+              }
+
+              // 获取更多页面
+              let pageNum = 1;
+              let lastPage = apiResponse.data.page;
+              while (lastPage !== -1 && pageNum < maxPages) {
+                const nextResponse = await page.evaluate(async (p) => {
+                  const response = await fetch(`https://edith.xiaohongshu.com/web_api/sns/v5/creator/note/user/posted?tab=0&page=${p}`, {
+                    method: 'GET',
+                    credentials: 'include',
+                  });
+                  return await response.json();
+                }, pageNum);
+
+                if (nextResponse?.data?.notes?.length > 0) {
+                  for (const note of nextResponse.data.notes) {
+                    // 将 http:// 转换为 https://
+                    let coverUrl = note.images_list?.[0]?.url || '';
+                    if (coverUrl.startsWith('http://')) {
+                      coverUrl = coverUrl.replace('http://', 'https://');
+                    }
+                    const duration = note.video_info?.duration || 0;
+
+                    allNotesData.push({
+                      noteId: note.id || '',
+                      title: note.display_title || '',
+                      coverUrl: coverUrl,
+                      status: note.tab_status || 1,
+                      publishTime: note.time || '',
+                      type: note.type || 'normal',
+                      duration: duration,
+                      likeCount: note.likes || 0,
+                      commentCount: note.comments_count || 0,
+                      collectCount: note.collected_count || 0,
+                      viewCount: note.view_count || 0,
+                      shareCount: note.shared_count || 0,
+                    });
+                  }
+                  pageNum++;
+                  lastPage = nextResponse.data.page;
+                  if (lastPage === -1) break;
+                } else {
+                  break;
+                }
+              }
+            }
+          } catch (apiError) {
+            logger.warn('[Xiaohongshu] Direct API call failed:', apiError);
+          }
+        }
+
+        // 如果还是没有数据,尝试滚动加载
+        if (allNotesData.length === 0) {
+          logger.info('[Xiaohongshu] Still no notes, trying scroll to trigger API...');
+          await page.waitForTimeout(2000);
+        }
+
+        // 滚动加载更多页面(如果通过监听器获取的数据)
+        while (hasMorePages && currentPage < maxPages && allNotesData.length > 0 && apiResponseReceived) {
+          currentPage++;
+          const previousCount = allNotesData.length;
+
+          // 滚动到页面底部触发加载更多
+          await page.evaluate(() => {
+            window.scrollTo(0, document.body.scrollHeight);
+          });
+
+          // 等待新数据加载
+          await page.waitForTimeout(2000);
+
+          // 如果没有新数据,退出循环
+          if (allNotesData.length === previousCount) {
+            logger.info(`[Xiaohongshu] No new notes loaded after scroll, stopping at page ${currentPage}`);
+            break;
+          }
+
+          logger.info(`[Xiaohongshu] Page ${currentPage}: total ${allNotesData.length} notes`);
+        }
+
+        // 移除监听器
+        page.off('response', notesApiHandler);
+
+        logger.info(`[Xiaohongshu] Total notes captured: ${allNotesData.length}`);
+
+        // 转换为 WorkItem 格式
+        for (const note of allNotesData) {
+          // 转换时长为 mm:ss 格式
+          const minutes = Math.floor(note.duration / 60);
+          const seconds = note.duration % 60;
+          const durationStr = note.duration > 0
+            ? `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+            : '';
+
+          // 转换状态
+          let statusStr = 'published';
+          if (note.status === 0) statusStr = 'draft';
+          else if (note.status === 2) statusStr = 'reviewing';
+          else if (note.status === 3) statusStr = 'rejected';
+
+          worksList.push({
+            videoId: note.noteId,
+            title: note.title || '无标题',
+            coverUrl: note.coverUrl,
+            duration: durationStr,
+            publishTime: note.publishTime,
+            status: statusStr,
+            playCount: note.viewCount,
+            likeCount: note.likeCount,
+            commentCount: note.commentCount,
+            shareCount: note.shareCount,
+          });
+        }
+
+        logger.info(`[Xiaohongshu] Fetched ${worksList.length} works via API`);
+
+        // 更新作品数:优先使用从 API tags 获取的总数
+        if (totalNotesCount > 0) {
+          worksCount = totalNotesCount;
+          logger.info(`[Xiaohongshu] Using total notes count from API: ${worksCount}`);
+        } else if (worksList.length > 0) {
+          worksCount = worksList.length;
+          logger.info(`[Xiaohongshu] Using works list length: ${worksCount}`);
+        }
+      } catch (worksError) {
+        logger.warn('[Xiaohongshu] Failed to fetch works list:', worksError);
+      }
+
+      logger.info(`[Xiaohongshu] Final account info: id=${accountId}, name=${accountName}, fans=${fansCount}, works=${worksCount}`);
+      return { accountId, accountName, avatarUrl, fansCount, worksCount, worksList };
+    } catch (error) {
+      logger.warn('[Xiaohongshu] Failed to fetch account info:', error);
+    }
+
+    return { accountId, accountName, avatarUrl, fansCount, worksCount };
+  }
+
+  /**
+   * 解析中文数字(如 1.2万 -> 12000)
+   */
+  private parseChineseNumber(str: string): number {
+    if (!str) return 0;
+
+    let num = parseFloat(str);
+    if (str.includes('万')) {
+      num *= 10000;
+    } else if (str.includes('亿')) {
+      num *= 100000000;
+    }
+    return Math.floor(num);
+  }
+
+  /**
    * 获取平台配置
    */
   private getPlatformConfig(platform: PlatformType) {
@@ -1647,27 +2161,28 @@ class HeadlessBrowserService {
 
   /**
    * 直接调用评论 API 获取数据(支持分页获取所有评论)
-   * 优先使用创作者评论 API(更快),失败时才使用视频页面 API
+   * 优先使用视频页面 API(获取全部评论),失败时才使用创作者 API
    */
   private async fetchCommentsDirectApi(page: Page, awemeId: string): Promise<CommentItem[]> {
-    // 优先尝试创作者评论 API(不需要导航,更快
-    logger.info(`[DirectAPI] Fetching comments for ${awemeId} via creator API...`);
-    let comments = await this.fetchCreatorCommentsDirectApi(page, awemeId);
+    // 优先尝试视频页面 API(获取全部评论
+    logger.info(`[DirectAPI] Fetching ALL comments for ${awemeId} via video page API...`);
+    let comments = await this.fetchVideoPageCommentsApi(page, awemeId);
 
     if (comments.length > 0) {
-      logger.info(`[DirectAPI] Got ${comments.length} comments via creator API`);
+      logger.info(`[DirectAPI] Got ${comments.length} comments via video page API`);
       return comments;
     }
 
-    // 如果创作者 API 失败,尝试视频页面 API
-    logger.info(`[DirectAPI] Creator API returned 0 comments, trying video page API...`);
-    comments = await this.fetchVideoPageCommentsApi(page, awemeId);
+    // 如果视频页面 API 失败,尝试创作者评论 API(可能只返回部分评论)
+    logger.info(`[DirectAPI] Video page API returned 0 comments, trying creator API...`);
+    comments = await this.fetchCreatorCommentsDirectApi(page, awemeId);
 
     return comments;
   }
 
   /**
-   * 通过视频页面获取评论(备用方案,较慢)
+   * 通过视频页面获取全部评论(主要方案)
+   * 这个 API 能获取视频的所有评论,不仅仅是创作者回复过的
    */
   private async fetchVideoPageCommentsApi(page: Page, awemeId: string): Promise<CommentItem[]> {
     const comments: CommentItem[] = [];
@@ -2422,6 +2937,219 @@ class HeadlessBrowserService {
       return allWorkComments;
     }
   }
+
+  /**
+   * 获取小红书评论
+   */
+  async fetchXiaohongshuCommentsViaApi(cookies: CookieData[]): Promise<WorkComments[]> {
+    const browser = await chromium.launch({
+      headless: true,
+      args: ['--no-sandbox', '--disable-setuid-sandbox'],
+    });
+
+    const allWorkComments: WorkComments[] = [];
+
+    try {
+      const context = await browser.newContext({
+        viewport: { width: 1920, height: 1080 },
+        userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+      });
+
+      // 设置 Cookie
+      const playwrightCookies = cookies.map(c => ({
+        name: c.name,
+        value: c.value,
+        domain: c.domain || '.xiaohongshu.com',
+        path: c.path || '/',
+      }));
+      await context.addCookies(playwrightCookies);
+      logger.info(`[Xiaohongshu Comments] Set ${playwrightCookies.length} cookies`);
+
+      const page = await context.newPage();
+
+      // 用于捕获评论数据
+      const capturedComments: Map<string, CommentItem[]> = new Map();
+      const capturedNotes: Array<{
+        noteId: string;
+        title: string;
+        coverUrl: string;
+      }> = [];
+
+      // 设置 API 响应监听器
+      page.on('response', async (response) => {
+        const url = response.url();
+        try {
+          // 监听笔记列表 API
+          if (url.includes('/api/galaxy/creator/content/note_list') ||
+            url.includes('/api/galaxy/creator/notes')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] Notes list: ${JSON.stringify(data).slice(0, 500)}`);
+            const notes = data?.data?.notes || data?.data?.list || [];
+            for (const note of notes) {
+              capturedNotes.push({
+                noteId: note.note_id || note.id || '',
+                title: note.title || note.desc || '',
+                coverUrl: note.cover?.url || note.cover || '',
+              });
+            }
+          }
+
+          // 监听评论列表 API
+          if (url.includes('/api/sns/web/v2/comment/page') ||
+            url.includes('/api/galaxy/creator/comment') ||
+            url.includes('/api/sns/v1/note/comment')) {
+            const data = await response.json();
+            logger.info(`[Xiaohongshu API] Comments: ${JSON.stringify(data).slice(0, 500)}`);
+
+            const comments: CommentItem[] = [];
+            const commentList = data?.data?.comments || data?.comments || [];
+
+            for (const comment of commentList) {
+              comments.push({
+                commentId: comment.id || comment.comment_id || `xhs_${Date.now()}`,
+                authorId: comment.user_info?.user_id || comment.user_id || '',
+                authorName: comment.user_info?.nickname || comment.nickname || '',
+                authorAvatar: comment.user_info?.image || comment.avatar || '',
+                content: comment.content || '',
+                likeCount: comment.like_count || 0,
+                commentTime: comment.create_time || comment.time || '',
+                parentCommentId: comment.target_comment_id || undefined,
+              });
+
+              // 处理子评论
+              const subComments = comment.sub_comments || comment.replies || [];
+              for (const sub of subComments) {
+                comments.push({
+                  commentId: sub.id || sub.comment_id || `xhs_sub_${Date.now()}`,
+                  authorId: sub.user_info?.user_id || sub.user_id || '',
+                  authorName: sub.user_info?.nickname || sub.nickname || '',
+                  authorAvatar: sub.user_info?.image || sub.avatar || '',
+                  content: sub.content || '',
+                  likeCount: sub.like_count || 0,
+                  commentTime: sub.create_time || sub.time || '',
+                  parentCommentId: comment.id || comment.comment_id || undefined,
+                });
+              }
+            }
+
+            // 尝试从 URL 获取笔记 ID
+            const noteIdMatch = url.match(/note_id=([^&]+)/) || url.match(/noteId=([^&]+)/);
+            const noteId = noteIdMatch?.[1] || `note_${Date.now()}`;
+
+            if (comments.length > 0) {
+              const existing = capturedComments.get(noteId) || [];
+              capturedComments.set(noteId, [...existing, ...comments]);
+            }
+          }
+        } catch { }
+      });
+
+      // 导航到评论管理页面
+      logger.info('[Xiaohongshu Comments] Navigating to comment management...');
+      await page.goto('https://creator.xiaohongshu.com/creator/comment', {
+        waitUntil: 'domcontentloaded',
+        timeout: 60000,
+      });
+
+      await page.waitForTimeout(5000);
+
+      // 检查是否需要登录
+      const currentUrl = page.url();
+      if (currentUrl.includes('login') || currentUrl.includes('passport')) {
+        logger.warn('[Xiaohongshu Comments] Cookie expired, need re-login');
+        await browser.close();
+        return allWorkComments;
+      }
+
+      // 尝试加载更多评论
+      for (let i = 0; i < 5; i++) {
+        await page.evaluate(() => {
+          window.scrollBy(0, 500);
+        });
+        await page.waitForTimeout(1000);
+      }
+
+      // 等待 API 响应
+      await page.waitForTimeout(3000);
+
+      // 将捕获的评论转换为 WorkComments 格式
+      for (const [noteId, comments] of capturedComments) {
+        const noteInfo = capturedNotes.find(n => n.noteId === noteId);
+        allWorkComments.push({
+          videoId: noteId,
+          videoTitle: noteInfo?.title || `笔记 ${noteId.slice(0, 10)}`,
+          videoCoverUrl: noteInfo?.coverUrl || '',
+          comments,
+        });
+      }
+
+      // 如果没有从 API 获取到评论,尝试从页面提取
+      if (allWorkComments.length === 0) {
+        logger.info('[Xiaohongshu Comments] No comments from API, extracting from page...');
+
+        const pageComments = await page.evaluate(() => {
+          const result: Array<{
+            commentId: string;
+            authorName: string;
+            authorAvatar: string;
+            content: string;
+            likeCount: number;
+            commentTime: string;
+          }> = [];
+
+          const commentItems = document.querySelectorAll('[class*="comment-item"], [class*="comment-card"]');
+          commentItems.forEach((item, index) => {
+            try {
+              const authorEl = item.querySelector('[class*="author"], [class*="name"]');
+              const avatarEl = item.querySelector('img');
+              const contentEl = item.querySelector('[class*="content"]');
+              const timeEl = item.querySelector('[class*="time"]');
+              const likeEl = item.querySelector('[class*="like"] span');
+
+              result.push({
+                commentId: `xhs_page_${index}`,
+                authorName: authorEl?.textContent?.trim() || '',
+                authorAvatar: avatarEl?.src || '',
+                content: contentEl?.textContent?.trim() || '',
+                likeCount: parseInt(likeEl?.textContent || '0') || 0,
+                commentTime: timeEl?.textContent?.trim() || '',
+              });
+            } catch { }
+          });
+
+          return result;
+        });
+
+        if (pageComments.length > 0) {
+          allWorkComments.push({
+            videoId: 'page_comments',
+            videoTitle: '页面评论',
+            videoCoverUrl: '',
+            comments: pageComments.map(c => ({
+              ...c,
+              authorId: '',
+            })),
+          });
+        }
+      }
+
+      await page.close();
+      await context.close();
+      await browser.close();
+
+      const totalComments = allWorkComments.reduce((sum, w) => sum + w.comments.length, 0);
+      logger.info(`[Xiaohongshu Comments] Total: fetched ${totalComments} comments from ${allWorkComments.length} works`);
+
+      return allWorkComments;
+
+    } catch (error) {
+      logger.error('[Xiaohongshu Comments] Error:', error);
+      try {
+        await browser.close();
+      } catch { }
+      return allWorkComments;
+    }
+  }
 }
 
 export const headlessBrowserService = new HeadlessBrowserService();

+ 5 - 2
server/src/services/PublishService.ts

@@ -10,6 +10,8 @@ import type {
 } from '@media-manager/shared';
 import { wsManager } from '../websocket/index.js';
 import { DouyinAdapter } from '../automation/platforms/douyin.js';
+import { XiaohongshuAdapter } from '../automation/platforms/xiaohongshu.js';
+import { BasePlatformAdapter } from '../automation/platforms/base.js';
 import { logger } from '../utils/logger.js';
 import path from 'path';
 import { config } from '../config/index.js';
@@ -28,11 +30,12 @@ export class PublishService {
   private accountRepository = AppDataSource.getRepository(PlatformAccount);
   
   // 平台适配器映射
-  private adapters: Map<PlatformType, DouyinAdapter> = new Map();
+  private adapters: Map<PlatformType, BasePlatformAdapter> = new Map();
   
   constructor() {
-    // 初始化抖音适配器
+    // 初始化平台适配器
     this.adapters.set('douyin', new DouyinAdapter());
+    this.adapters.set('xiaohongshu', new XiaohongshuAdapter());
   }
   
   /**

+ 12 - 4
server/src/websocket/index.ts

@@ -20,7 +20,8 @@ class WebSocketManager {
   setup(server: HttpServer): void {
     this.wss = new WebSocketServer({ server, path: '/ws' });
 
-    this.wss.on('connection', (ws: AuthenticatedWebSocket) => {
+    this.wss.on('connection', (ws: AuthenticatedWebSocket, req) => {
+      logger.info(`[WS] New connection from ${req.socket.remoteAddress}, URL: ${req.url}`);
       ws.isAlive = true;
 
       ws.on('pong', () => {
@@ -28,19 +29,22 @@ class WebSocketManager {
       });
 
       ws.on('message', (data) => {
+        logger.info(`[WS] Received message: ${data.toString().slice(0, 200)}`);
         this.handleMessage(ws, data.toString());
       });
 
-      ws.on('close', () => {
+      ws.on('close', (code, reason) => {
+        logger.info(`[WS] Connection closed, code: ${code}, reason: ${reason?.toString()}`);
         this.removeClient(ws);
       });
 
       ws.on('error', (error) => {
-        logger.error('WebSocket error:', error);
+        logger.error('[WS] Connection error:', error);
         this.removeClient(ws);
       });
 
       // 发送连接成功消息,等待认证
+      logger.info('[WS] Sending connection message...');
       this.send(ws, { type: 'connection', payload: { message: 'Connected, please authenticate' } });
     });
 
@@ -101,7 +105,10 @@ class WebSocketManager {
   }
 
   private handleAuth(ws: AuthenticatedWebSocket, token?: string): void {
+    logger.info(`[WS] Authenticating, token: ${token ? token.slice(0, 20) + '...' : 'none'}`);
+    
     if (!token) {
+      logger.warn('[WS] Auth failed: no token');
       this.send(ws, { type: WS_EVENTS.AUTH_ERROR, payload: { message: 'Token required' } });
       return;
     }
@@ -121,8 +128,9 @@ class WebSocketManager {
         payload: { userId: decoded.userId, message: 'Authenticated' } 
       });
 
-      logger.info(`WebSocket authenticated for user ${decoded.userId}`);
+      logger.info(`[WS] Auth success for user ${decoded.userId}, total connections: ${this.clients.get(decoded.userId)!.size}`);
     } catch (error) {
+      logger.warn('[WS] Auth failed: invalid token', error);
       this.send(ws, { type: WS_EVENTS.AUTH_ERROR, payload: { message: 'Invalid token' } });
     }
   }