ソースを参照

Merge branch 'ai_qwen'

Ethanfly 1 ヶ月 前
コミット
5caadb7a2b

+ 173 - 1
client/dist-electron/main.js

@@ -1,5 +1,5 @@
 "use strict";
-const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage } = require("electron");
+const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require("electron");
 const { join } = require("path");
 require("fs");
 let mainWindow = null;
@@ -124,6 +124,7 @@ if (!gotTheLock) {
   app.whenReady().then(() => {
     createTray();
     createWindow();
+    setupWebviewSessions();
     app.on("activate", () => {
       if (BrowserWindow.getAllWindows().length === 0) {
         createWindow();
@@ -133,6 +134,23 @@ if (!gotTheLock) {
     });
   });
 }
+function setupWebviewSessions() {
+  app.on("web-contents-created", (_event, contents) => {
+    if (contents.getType() === "webview") {
+      contents.setUserAgent(
+        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+      );
+      contents.session.setPermissionRequestHandler((_webContents, permission, callback) => {
+        callback(true);
+      });
+      contents.session.webRequest.onBeforeSendHeaders((details, callback) => {
+        delete details.requestHeaders["X-DevTools-Emulate-Network-Conditions-Client-Id"];
+        if (!details.requestHeaders["Origin"] && !details.requestHeaders["origin"]) ;
+        callback({ requestHeaders: details.requestHeaders });
+      });
+    }
+  });
+}
 app.on("window-all-closed", () => {
 });
 app.on("before-quit", () => {
@@ -210,4 +228,158 @@ ipcMain.handle("set-webview-cookies", async (_event, partition, cookies) => {
     return false;
   }
 });
+ipcMain.handle("capture-webview-page", async (_event, webContentsId) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("找不到 webContents:", webContentsId);
+      return null;
+    }
+    const image = await wc.capturePage();
+    if (!image || image.isEmpty()) {
+      console.warn("截图为空");
+      return null;
+    }
+    const buffer = image.toJPEG(80);
+    return buffer.toString("base64");
+  } catch (error) {
+    console.error("截图失败:", error);
+    return null;
+  }
+});
+ipcMain.handle("webview-send-mouse-click", async (_event, webContentsId, x, y) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("找不到 webContents:", webContentsId);
+      return false;
+    }
+    wc.sendInputEvent({
+      type: "mouseMove",
+      x: Math.round(x),
+      y: Math.round(y)
+    });
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    wc.sendInputEvent({
+      type: "mouseDown",
+      x: Math.round(x),
+      y: Math.round(y),
+      button: "left",
+      clickCount: 1
+    });
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    wc.sendInputEvent({
+      type: "mouseUp",
+      x: Math.round(x),
+      y: Math.round(y),
+      button: "left",
+      clickCount: 1
+    });
+    console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
+    return true;
+  } catch (error) {
+    console.error("发送点击事件失败:", error);
+    return false;
+  }
+});
+ipcMain.handle("webview-send-text-input", async (_event, webContentsId, text) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("找不到 webContents:", webContentsId);
+      return false;
+    }
+    for (const char of text) {
+      wc.sendInputEvent({
+        type: "char",
+        keyCode: char
+      });
+      await new Promise((resolve) => setTimeout(resolve, 30));
+    }
+    console.log(`[webview-send-text-input] Typed: ${text}`);
+    return true;
+  } catch (error) {
+    console.error("发送输入事件失败:", error);
+    return false;
+  }
+});
+ipcMain.handle("webview-get-element-position", async (_event, webContentsId, selector) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("找不到 webContents:", webContentsId);
+      return null;
+    }
+    const result = await wc.executeJavaScript(`
+      (function() {
+        const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
+        if (!el) return null;
+        const rect = el.getBoundingClientRect();
+        return {
+          x: rect.left + rect.width / 2,
+          y: rect.top + rect.height / 2,
+          width: rect.width,
+          height: rect.height
+        };
+      })()
+    `);
+    return result;
+  } catch (error) {
+    console.error("获取元素位置失败:", error);
+    return null;
+  }
+});
+ipcMain.handle("webview-click-by-text", async (_event, webContentsId, text) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("找不到 webContents:", webContentsId);
+      return false;
+    }
+    const position = await wc.executeJavaScript(`
+      (function() {
+        const searchText = '${text.replace(/'/g, "\\'")}';
+        
+        // 查找可点击元素
+        const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
+        for (const el of clickables) {
+          if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
+            const rect = el.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0) {
+              return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
+            }
+          }
+        }
+        
+        // 查找所有包含文本的元素
+        const allElements = document.querySelectorAll('*');
+        for (const el of allElements) {
+          const text = el.innerText?.trim();
+          if (text && text.length < 100 && text.includes(searchText)) {
+            const rect = el.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
+              return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
+            }
+          }
+        }
+        
+        return null;
+      })()
+    `);
+    if (!position) {
+      console.warn(`[webview-click-by-text] 未找到包含 "${text}" 的元素`);
+      return false;
+    }
+    wc.sendInputEvent({ type: "mouseMove", x: Math.round(position.x), y: Math.round(position.y) });
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    wc.sendInputEvent({ type: "mouseDown", x: Math.round(position.x), y: Math.round(position.y), button: "left", clickCount: 1 });
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    wc.sendInputEvent({ type: "mouseUp", x: Math.round(position.x), y: Math.round(position.y), button: "left", clickCount: 1 });
+    console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
+    return true;
+  } catch (error) {
+    console.error("通过文本点击失败:", error);
+    return false;
+  }
+});
 //# sourceMappingURL=main.js.map

ファイルの差分が大きいため隠しています
+ 0 - 0
client/dist-electron/main.js.map


ファイルの差分が大きいため隠しています
+ 8 - 1
client/dist-electron/preload.js


+ 233 - 9
client/electron/main.ts

@@ -1,5 +1,5 @@
 // 使用 CommonJS 格式
-const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage } = require('electron');
+const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require('electron');
 const { join } = require('path');
 const fs = require('fs');
 
@@ -11,14 +11,14 @@ const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
 
 // 获取图标路径
 function getIconPath() {
-  return VITE_DEV_SERVER_URL 
+  return VITE_DEV_SERVER_URL
     ? join(__dirname, '../public/icons/icon-256.png')
     : join(__dirname, '../dist/icons/icon-256.png');
 }
 
 // 获取托盘图标路径
 function getTrayIconPath() {
-  return VITE_DEV_SERVER_URL 
+  return VITE_DEV_SERVER_URL
     ? join(__dirname, '../public/icons/tray-icon.png')
     : join(__dirname, '../dist/icons/tray-icon.png');
 }
@@ -33,7 +33,7 @@ function createTrayIcon(): typeof nativeImage.prototype {
 function createTray() {
   const trayIcon = createTrayIcon();
   tray = new Tray(trayIcon);
-  
+
   const contextMenu = Menu.buildFromTemplate([
     {
       label: '显示主窗口',
@@ -59,10 +59,10 @@ function createTray() {
       }
     }
   ]);
-  
+
   tray.setToolTip('多平台媒体管理系统');
   tray.setContextMenu(contextMenu);
-  
+
   // 点击托盘图标显示窗口
   tray.on('click', () => {
     if (mainWindow) {
@@ -74,7 +74,7 @@ function createTray() {
       }
     }
   });
-  
+
   // 双击托盘图标显示窗口
   tray.on('double-click', () => {
     if (mainWindow) {
@@ -87,7 +87,7 @@ function createTray() {
 function createWindow() {
   // 隐藏默认菜单栏
   Menu.setApplicationMenu(null);
-  
+
   const iconPath = getIconPath();
 
   mainWindow = new BrowserWindow({
@@ -133,7 +133,7 @@ function createWindow() {
     if (!isQuitting) {
       event.preventDefault();
       mainWindow?.hide();
-      
+
       // 显示托盘通知(仅首次)
       if (tray && !app.isPackaged) {
         // 开发模式下可以显示通知
@@ -164,6 +164,9 @@ if (!gotTheLock) {
     createTray();
     createWindow();
 
+    // 配置 webview session,允许第三方 cookies 和跨域请求
+    setupWebviewSessions();
+
     app.on('activate', () => {
       if (BrowserWindow.getAllWindows().length === 0) {
         createWindow();
@@ -174,6 +177,39 @@ if (!gotTheLock) {
   });
 }
 
+// 配置 webview sessions
+function setupWebviewSessions() {
+  // 监听新的 webContents 创建
+  app.on('web-contents-created', (_event: unknown, contents: typeof webContents.prototype) => {
+    // 为 webview 类型的 webContents 配置
+    if (contents.getType() === 'webview') {
+      // 设置 User-Agent(模拟 Chrome 浏览器)
+      contents.setUserAgent(
+        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+      );
+
+      // 允许所有的权限请求(如摄像头、地理位置等)
+      contents.session.setPermissionRequestHandler((_webContents: unknown, permission: string, callback: (granted: boolean) => void) => {
+        // 允许所有权限请求
+        callback(true);
+      });
+
+      // 配置 webRequest 修改请求头,移除可能暴露 Electron 的特征
+      contents.session.webRequest.onBeforeSendHeaders((details: { requestHeaders: Record<string, string> }, callback: (response: { requestHeaders: Record<string, string> }) => void) => {
+        // 移除可能暴露 Electron 的请求头
+        delete details.requestHeaders['X-DevTools-Emulate-Network-Conditions-Client-Id'];
+
+        // 确保有正常的 Origin 和 Referer
+        if (!details.requestHeaders['Origin'] && !details.requestHeaders['origin']) {
+          // 不添加 Origin,让浏览器自动处理
+        }
+
+        callback({ requestHeaders: details.requestHeaders });
+      });
+    }
+  });
+}
+
 // 阻止默认的 window-all-closed 行为,保持托盘运行
 app.on('window-all-closed', () => {
   // 不退出应用,保持托盘运行
@@ -278,3 +314,191 @@ ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string,
     return false;
   }
 });
+
+// 截取 webview 页面截图(用于 AI 分析)
+ipcMain.handle('capture-webview-page', async (_event: unknown, webContentsId: number) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('找不到 webContents:', webContentsId);
+      return null;
+    }
+
+    const image = await wc.capturePage();
+    if (!image || image.isEmpty()) {
+      console.warn('截图为空');
+      return null;
+    }
+
+    // 转换为 JPEG 格式的 Base64
+    const buffer = image.toJPEG(80);
+    return buffer.toString('base64');
+  } catch (error) {
+    console.error('截图失败:', error);
+    return null;
+  }
+});
+
+// 向 webview 发送鼠标点击事件
+ipcMain.handle('webview-send-mouse-click', async (_event: unknown, webContentsId: number, x: number, y: number) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('找不到 webContents:', webContentsId);
+      return false;
+    }
+
+    // 发送鼠标移动事件
+    wc.sendInputEvent({
+      type: 'mouseMove',
+      x: Math.round(x),
+      y: Math.round(y),
+    });
+
+    // 短暂延迟后发送点击事件
+    await new Promise(resolve => setTimeout(resolve, 50));
+
+    // 发送鼠标按下事件
+    wc.sendInputEvent({
+      type: 'mouseDown',
+      x: Math.round(x),
+      y: Math.round(y),
+      button: 'left',
+      clickCount: 1,
+    });
+
+    // 短暂延迟后发送鼠标抬起事件
+    await new Promise(resolve => setTimeout(resolve, 50));
+
+    wc.sendInputEvent({
+      type: 'mouseUp',
+      x: Math.round(x),
+      y: Math.round(y),
+      button: 'left',
+      clickCount: 1,
+    });
+
+    console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
+    return true;
+  } catch (error) {
+    console.error('发送点击事件失败:', error);
+    return false;
+  }
+});
+
+// 向 webview 发送键盘输入事件
+ipcMain.handle('webview-send-text-input', async (_event: unknown, webContentsId: number, text: string) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('找不到 webContents:', webContentsId);
+      return false;
+    }
+
+    // 逐字符输入
+    for (const char of text) {
+      wc.sendInputEvent({
+        type: 'char',
+        keyCode: char,
+      });
+      await new Promise(resolve => setTimeout(resolve, 30));
+    }
+
+    console.log(`[webview-send-text-input] Typed: ${text}`);
+    return true;
+  } catch (error) {
+    console.error('发送输入事件失败:', error);
+    return false;
+  }
+});
+
+// 获取 webview 页面元素位置
+ipcMain.handle('webview-get-element-position', async (_event: unknown, webContentsId: number, selector: string) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('找不到 webContents:', webContentsId);
+      return null;
+    }
+
+    const result = await wc.executeJavaScript(`
+      (function() {
+        const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
+        if (!el) return null;
+        const rect = el.getBoundingClientRect();
+        return {
+          x: rect.left + rect.width / 2,
+          y: rect.top + rect.height / 2,
+          width: rect.width,
+          height: rect.height
+        };
+      })()
+    `);
+
+    return result;
+  } catch (error) {
+    console.error('获取元素位置失败:', error);
+    return null;
+  }
+});
+
+// 通过文本内容查找并点击元素
+ipcMain.handle('webview-click-by-text', async (_event: unknown, webContentsId: number, text: string) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('找不到 webContents:', webContentsId);
+      return false;
+    }
+
+    // 查找包含指定文本的可点击元素的位置
+    const position = await wc.executeJavaScript(`
+      (function() {
+        const searchText = '${text.replace(/'/g, "\\'")}';
+        
+        // 查找可点击元素
+        const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
+        for (const el of clickables) {
+          if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
+            const rect = el.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0) {
+              return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
+            }
+          }
+        }
+        
+        // 查找所有包含文本的元素
+        const allElements = document.querySelectorAll('*');
+        for (const el of allElements) {
+          const text = el.innerText?.trim();
+          if (text && text.length < 100 && text.includes(searchText)) {
+            const rect = el.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
+              return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
+            }
+          }
+        }
+        
+        return null;
+      })()
+    `);
+
+    if (!position) {
+      console.warn(`[webview-click-by-text] 未找到包含 "${text}" 的元素`);
+      return false;
+    }
+
+    // 发送点击事件
+    wc.sendInputEvent({ type: 'mouseMove', x: Math.round(position.x), y: Math.round(position.y) });
+    await new Promise(resolve => setTimeout(resolve, 50));
+    wc.sendInputEvent({ type: 'mouseDown', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
+    await new Promise(resolve => setTimeout(resolve, 50));
+    wc.sendInputEvent({ type: 'mouseUp', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
+
+    console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
+    return true;
+  } catch (error) {
+    console.error('通过文本点击失败:', error);
+    return false;
+  }
+});

+ 19 - 0
client/electron/preload.ts

@@ -33,6 +33,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
     ipcRenderer.invoke('clear-webview-cookies', partition),
   setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) =>
     ipcRenderer.invoke('set-webview-cookies', partition, cookies),
+
+  // Webview 截图(用于 AI 分析)
+  captureWebviewPage: (webContentsId: number) =>
+    ipcRenderer.invoke('capture-webview-page', webContentsId),
+
+  // Webview 自动操作
+  webviewSendMouseClick: (webContentsId: number, x: number, y: number) =>
+    ipcRenderer.invoke('webview-send-mouse-click', webContentsId, x, y),
+  webviewSendTextInput: (webContentsId: number, text: string) =>
+    ipcRenderer.invoke('webview-send-text-input', webContentsId, text),
+  webviewGetElementPosition: (webContentsId: number, selector: string) =>
+    ipcRenderer.invoke('webview-get-element-position', webContentsId, selector),
+  webviewClickByText: (webContentsId: number, text: string) =>
+    ipcRenderer.invoke('webview-click-by-text', webContentsId, text),
 });
 
 // 类型声明
@@ -53,6 +67,11 @@ declare global {
       getWebviewCookies: (partition: string, url: string) => Promise<Electron.Cookie[]>;
       clearWebviewCookies: (partition: string) => Promise<boolean>;
       setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) => Promise<boolean>;
+      captureWebviewPage: (webContentsId: number) => Promise<string | null>;
+      webviewSendMouseClick: (webContentsId: number, x: number, y: number) => Promise<boolean>;
+      webviewSendTextInput: (webContentsId: number, text: string) => Promise<boolean>;
+      webviewGetElementPosition: (webContentsId: number, selector: string) => Promise<{ x: number; y: number; width: number; height: number } | null>;
+      webviewClickByText: (webContentsId: number, text: string) => Promise<boolean>;
     };
   }
 }

+ 13 - 4
client/package.json

@@ -63,15 +63,24 @@
       }
     ],
     "win": {
-      "target": ["nsis", "portable"],
+      "target": [
+        "nsis",
+        "portable"
+      ],
       "icon": "build/icon.png"
     },
     "mac": {
-      "target": ["dmg", "zip"],
+      "target": [
+        "dmg",
+        "zip"
+      ],
       "icon": "build/icon.png"
     },
     "linux": {
-      "target": ["AppImage", "deb"],
+      "target": [
+        "AppImage",
+        "deb"
+      ],
       "icon": "build/icon.png"
     },
     "nsis": {
@@ -80,4 +89,4 @@
       "createDesktopShortcut": true
     }
   }
-}
+}

+ 110 - 0
client/src/api/ai.ts

@@ -0,0 +1,110 @@
+import request from './request';
+
+// 登录状态分析结果
+export interface LoginStatusAnalysis {
+  isLoggedIn: boolean;
+  hasVerification: boolean;
+  verificationType?: 'captcha' | 'sms' | 'qrcode' | 'face' | 'slider' | 'other' | null;
+  verificationDescription?: string | null;
+  pageDescription?: string;
+  suggestedAction?: string | null;
+}
+
+// 账号信息提取结果
+export interface AccountInfoExtraction {
+  found: boolean;
+  accountName?: string | null;
+  accountId?: string | null;
+  avatarDescription?: string | null;
+  fansCount?: number | null;
+  worksCount?: number | null;
+  otherInfo?: string | null;
+  navigationGuide?: string | null;
+}
+
+// 页面操作指导结果
+export interface PageOperationGuide {
+  hasAction: boolean;
+  actionType?: 'click' | 'input' | 'scroll' | 'wait' | 'navigate' | null;
+  targetDescription?: string | null;
+  targetSelector?: string | null;
+  targetPosition?: { x: number; y: number } | null;
+  inputText?: string | null;
+  explanation: string;
+}
+
+// 发布状态分析结果
+export interface PublishStatusAnalysis {
+  status: 'uploading' | 'processing' | 'success' | 'failed' | 'need_captcha' | 'need_action';
+  captchaType?: 'image' | 'sms' | 'slider' | 'other' | null;
+  captchaDescription?: string | null;
+  errorMessage?: string | null;
+  nextAction?: {
+    actionType: 'click' | 'input' | 'wait';
+    targetDescription: string;
+    targetSelector?: string | null;
+  } | null;
+  pageDescription: string;
+  confidence: number;
+}
+
+export const aiApi = {
+  // 检查 AI 服务状态
+  getStatus(): Promise<{
+    available: boolean;
+    models: Record<string, string> | null;
+  }> {
+    return request.get('/api/ai/status');
+  },
+
+  // 分析登录状态(用于浏览器登录辅助)
+  analyzeLoginStatus(imageBase64: string, platform: string): Promise<LoginStatusAnalysis> {
+    return request.post('/api/ai/vision/login-status', {
+      imageBase64,
+      platform,
+    }, { timeout: 60000 }); // 60秒超时
+  },
+
+  // 从截图中提取账号信息
+  extractAccountInfo(imageBase64: string, platform: string): Promise<AccountInfoExtraction> {
+    return request.post('/api/ai/vision/extract-account', {
+      imageBase64,
+      platform,
+    }, { timeout: 60000 });
+  },
+
+  // 获取页面操作指导(AI 指导自动操作 - 基于截图)
+  getOperationGuide(imageBase64: string, platform: string, goal: string): Promise<PageOperationGuide> {
+    return request.post('/api/ai/vision/operation-guide', {
+      imageBase64,
+      platform,
+      goal,
+    }, { timeout: 60000 });
+  },
+
+  // 获取页面操作指导(AI 指导自动操作 - 基于 HTML,更精确)
+  getHtmlOperationGuide(html: string, platform: string, goal: string): Promise<PageOperationGuide> {
+    return request.post('/api/ai/html/operation-guide', {
+      html,
+      platform,
+      goal,
+    }, { timeout: 90000 }); // HTML 分析可能需要更长时间
+  },
+
+  // 分析发布页面状态(检测验证码、发布结果等)
+  analyzePublishStatus(imageBase64: string, platform: string): Promise<PublishStatusAnalysis> {
+    return request.post('/api/ai/publish/analyze-status', {
+      imageBase64,
+      platform,
+    }, { timeout: 60000 });
+  },
+
+  // 分析发布页面 HTML 获取操作指导
+  getPublishOperationGuide(html: string, platform: string, currentStatus: string): Promise<PageOperationGuide> {
+    return request.post('/api/ai/publish/operation-guide', {
+      html,
+      platform,
+      currentStatus,
+    }, { timeout: 90000 });
+  },
+};

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

@@ -9,6 +9,7 @@ declare module 'vue' {
   export interface GlobalComponents {
     BrowserTab: typeof import('./components/BrowserTab.vue')['default']
     CaptchaDialog: typeof import('./components/CaptchaDialog.vue')['default']
+    Close: typeof import('@element-plus/icons-vue')['Close']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']

ファイルの差分が大きいため隠しています
+ 1005 - 2
client/src/components/BrowserTab.vue


+ 9 - 1
client/vite.config.ts

@@ -18,7 +18,14 @@ export default defineConfig(({ command }) => {
       },
     },
     plugins: [
-      vue(),
+      vue({
+        template: {
+          compilerOptions: {
+            // 将 webview 标记为自定义元素(Electron 特有标签)
+            isCustomElement: (tag) => tag === 'webview',
+          },
+        },
+      }),
       AutoImport({
         imports: ['vue', 'vue-router', 'pinia'],
         resolvers: [ElementPlusResolver()],
@@ -39,6 +46,7 @@ export default defineConfig(({ command }) => {
                 'Monitor', 'TrendCharts', 'ChatDotRound', 'ArrowDown', 'ArrowLeft',
                 'ArrowRight', 'VideoPlay', 'CircleCheck', 'CircleClose', 'Lock',
                 'Picture', 'Plus', 'Search', 'Edit', 'Download', 'MoreFilled',
+                'MagicStick',
               ];
               if (iconNames.includes(name)) {
                 return { name, from: '@element-plus/icons-vue' };

+ 40 - 4
server/env.example

@@ -91,12 +91,48 @@ MAX_IMAGE_SIZE=10
 ENCRYPTION_KEY=your-encryption-key-32-chars-long!
 
 # ----------------------------------------
-# AI 配置 (可选,用于智能功能)
+# AI 配置 - 阿里云百炼千问大模型 (可选,用于智能功能)
 # ----------------------------------------
-# OpenAI API Key
-OPENAI_API_KEY=
+# 阿里云百炼 API Key (以 sk- 开头)
+# 获取地址: https://bailian.console.aliyun.com/
+DASHSCOPE_API_KEY=
+
+# API Base URL (可选,默认为北京区域)
+# 北京: https://dashscope.aliyuncs.com/compatible-mode/v1
+# 新加坡: https://dashscope-intl.aliyuncs.com/compatible-mode/v1
+# 弗吉尼亚: https://dashscope-us.aliyuncs.com/compatible-mode/v1
+DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+
+# 默认对话模型 (可选)
+# 可选值: qwen-plus, qwen-max, qwen-turbo, qwen3-235b-a22b 等
+AI_DEFAULT_MODEL=qwen-plus
+
+# 对话模型 - 用于一般对话
+AI_CHAT_MODEL=qwen-plus
+
+# 快速模型 - 用于简单任务,响应更快
+AI_FAST_MODEL=qwen-turbo
+
+# 推理模型 - 用于复杂逻辑推理
+AI_REASONING_MODEL=qwq-plus
 
-# OpenAI API Base URL (可用于自定义 API 端点)
+# 视觉理解模型 - 用于图像分析
+AI_VISION_MODEL=qwen-vl-plus
+
+# 代码模型 - 用于代码生成和分析
+AI_CODER_MODEL=qwen-coder-plus
+
+# 嵌入模型 - 用于文本向量化
+AI_EMBEDDING_MODEL=text-embedding-v3
+
+# 请求超时时间 (毫秒)
+AI_TIMEOUT=60000
+
+# 最大重试次数
+AI_MAX_RETRIES=3
+
+# 兼容 OpenAI 配置 (如果设置了 DASHSCOPE_API_KEY,以下配置将被忽略)
+OPENAI_API_KEY=
 OPENAI_BASE_URL=https://api.openai.com/v1
 
 # ========================================

+ 116 - 1
server/python/app.py

@@ -275,7 +275,10 @@ def publish_video():
             "message": result.message,
             "error": result.error,
             "need_captcha": result.need_captcha,
-            "captcha_type": result.captcha_type
+            "captcha_type": result.captcha_type,
+            "screenshot_base64": result.screenshot_base64,
+            "page_url": result.page_url,
+            "status": result.status
         }
         
         # 如果需要验证码,打印明确的日志
@@ -289,6 +292,118 @@ def publish_video():
         return jsonify({"success": False, "error": str(e)}), 500
 
 
+# ==================== AI 辅助发布接口 ====================
+
+# 存储活跃的发布会话
+active_publish_sessions = {}
+
+@app.route("/publish/ai-assisted", methods=["POST"])
+def publish_ai_assisted():
+    """
+    AI 辅助发布接口
+    
+    与普通发布接口的区别:
+    1. 发布过程中会返回截图供 AI 分析
+    2. 如果检测到需要验证码,返回截图和状态,等待外部处理
+    3. 支持继续发布(输入验证码后)
+    
+    请求体:
+    {
+        "platform": "douyin",
+        "cookie": "cookie字符串",
+        "title": "视频标题",
+        "video_path": "视频文件路径",
+        ...
+        "return_screenshot": true  // 是否返回截图
+    }
+    
+    响应:
+    {
+        "success": true/false,
+        "status": "success|failed|need_captcha|processing",
+        "screenshot_base64": "...",  // 当前页面截图
+        "page_url": "...",
+        ...
+    }
+    """
+    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", "重庆市")
+        return_screenshot = data.get("return_screenshot", True)
+        
+        # 参数验证
+        if not platform:
+            return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
+        if platform not in PLATFORM_MAP:
+            return jsonify({"success": False, "error": f"不支持的平台: {platform}"}), 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 or not os.path.exists(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"[AI Publish] 平台: {platform}")
+        print(f"[AI Publish] 标题: {title}")
+        print(f"[AI Publish] 视频: {video_path}")
+        print("=" * 60)
+        
+        # 获取对应平台的发布器
+        PublisherClass = get_publisher(platform)
+        publisher = PublisherClass(headless=HEADLESS_MODE)
+        
+        # 执行发布
+        result = asyncio.run(publisher.run(cookie_str, params))
+        
+        response_data = {
+            "success": result.success,
+            "platform": result.platform,
+            "video_id": result.video_id,
+            "video_url": result.video_url,
+            "message": result.message,
+            "error": result.error,
+            "need_captcha": result.need_captcha,
+            "captcha_type": result.captcha_type,
+            "status": result.status or ("success" if result.success else "failed"),
+            "page_url": result.page_url
+        }
+        
+        # 如果请求返回截图
+        if return_screenshot and result.screenshot_base64:
+            response_data["screenshot_base64"] = result.screenshot_base64
+        
+        return jsonify(response_data)
+        
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({"success": False, "error": str(e), "status": "error"}), 500
+
+
 # ==================== 批量发布接口 ====================
 
 @app.route("/publish/batch", methods=["POST"])

+ 88 - 1
server/python/platforms/base.py

@@ -41,6 +41,9 @@ class PublishResult:
     error: str = ""
     need_captcha: bool = False  # 是否需要验证码
     captcha_type: str = ""  # 验证码类型: phone, slider, image
+    screenshot_base64: str = ""  # 页面截图(Base64)
+    page_url: str = ""  # 当前页面 URL
+    status: str = ""  # 状态: uploading, processing, success, failed, need_captcha, need_action
 
 
 @dataclass
@@ -245,7 +248,91 @@ class BasePublisher(ABC):
         """保存 cookies 到文件"""
         if self.context:
             await self.context.storage_state(path=file_path)
-    
+
+    async def capture_screenshot(self) -> str:
+        """截取当前页面截图,返回 Base64 编码"""
+        import base64
+        if not self.page:
+            return ""
+        try:
+            screenshot_bytes = await self.page.screenshot(type="jpeg", quality=80)
+            return base64.b64encode(screenshot_bytes).decode('utf-8')
+        except Exception as e:
+            print(f"[{self.platform_name}] 截图失败: {e}")
+            return ""
+
+    async def get_page_url(self) -> str:
+        """获取当前页面 URL"""
+        if not self.page:
+            return ""
+        try:
+            return self.page.url
+        except:
+            return ""
+
+    async def check_publish_status(self) -> dict:
+        """
+        检查发布状态
+        返回: {status, screenshot_base64, page_url, message}
+        """
+        if not self.page:
+            return {"status": "error", "message": "页面未初始化"}
+        
+        try:
+            screenshot = await self.capture_screenshot()
+            page_url = await self.get_page_url()
+            
+            # 检查常见的成功/失败标志
+            page_content = await self.page.content()
+            
+            # 检查成功标志
+            success_keywords = ['发布成功', '上传成功', '发表成功', '提交成功']
+            for keyword in success_keywords:
+                if keyword in page_content:
+                    return {
+                        "status": "success",
+                        "screenshot_base64": screenshot,
+                        "page_url": page_url,
+                        "message": "发布成功"
+                    }
+            
+            # 检查验证码标志
+            captcha_keywords = ['验证码', '身份验证', '请完成验证', '滑动验证', '图形验证']
+            for keyword in captcha_keywords:
+                if keyword in page_content:
+                    return {
+                        "status": "need_captcha",
+                        "screenshot_base64": screenshot,
+                        "page_url": page_url,
+                        "message": f"检测到{keyword}"
+                    }
+            
+            # 检查失败标志
+            fail_keywords = ['发布失败', '上传失败', '提交失败', '操作失败']
+            for keyword in fail_keywords:
+                if keyword in page_content:
+                    return {
+                        "status": "failed",
+                        "screenshot_base64": screenshot,
+                        "page_url": page_url,
+                        "message": keyword
+                    }
+            
+            # 默认返回处理中
+            return {
+                "status": "processing",
+                "screenshot_base64": screenshot,
+                "page_url": page_url,
+                "message": "处理中"
+            }
+        except Exception as e:
+            return {
+                "status": "error",
+                "screenshot_base64": "",
+                "page_url": "",
+                "message": str(e)
+            }
+
     async def wait_for_upload_complete(self, success_selector: str, timeout: int = 300):
         """等待上传完成"""
         if not self.page:

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

@@ -172,12 +172,17 @@ class DouyinPublisher(BasePublisher):
         captcha_result = await self.check_captcha()
         if captcha_result['need_captcha']:
             print(f"[{self.platform_name}] 检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
+            screenshot_base64 = await self.capture_screenshot()
+            page_url = await self.get_page_url()
             return PublishResult(
                 success=False,
                 platform=self.platform_name,
                 error=f"需要{captcha_result['captcha_type']}验证码",
                 need_captcha=True,
-                captcha_type=captcha_result['captcha_type']
+                captcha_type=captcha_result['captcha_type'],
+                screenshot_base64=screenshot_base64,
+                page_url=page_url,
+                status='need_captcha'
             )
         
         self.report_progress(15, "正在选择视频文件...")
@@ -293,16 +298,18 @@ class DouyinPublisher(BasePublisher):
                 captcha_result = await self.check_captcha()
                 if captcha_result['need_captcha']:
                     print(f"[{self.platform_name}] 发布过程中检测到需要验证码: {captcha_result['captcha_type']}", flush=True)
-                    # 保存截图供调试
-                    screenshot_path = f"debug_captcha_{self.platform_name}_{i}.png"
-                    await self.page.screenshot(path=screenshot_path, full_page=True)
-                    print(f"[{self.platform_name}] 验证码截图保存到: {screenshot_path}", flush=True)
+                    # 获取截图供 AI 分析
+                    screenshot_base64 = await self.capture_screenshot()
+                    page_url = await self.get_page_url()
                     return PublishResult(
                         success=False,
                         platform=self.platform_name,
                         error=f"发布过程中需要{captcha_result['captcha_type']}验证码",
                         need_captcha=True,
-                        captcha_type=captcha_result['captcha_type']
+                        captcha_type=captcha_result['captcha_type'],
+                        screenshot_base64=screenshot_base64,
+                        page_url=page_url,
+                        status='need_captcha'
                     )
                 
                 publish_btn = self.page.get_by_role('button', name="发布", exact=True)
@@ -319,14 +326,17 @@ class DouyinPublisher(BasePublisher):
                     captcha_result = await self.check_captcha()
                     if captcha_result['need_captcha']:
                         print(f"[{self.platform_name}] 点击发布后需要验证码: {captcha_result['captcha_type']}", flush=True)
-                        screenshot_path = f"debug_captcha_after_publish_{self.platform_name}.png"
-                        await self.page.screenshot(path=screenshot_path, full_page=True)
+                        screenshot_base64 = await self.capture_screenshot()
+                        page_url = await self.get_page_url()
                         return PublishResult(
                             success=False,
                             platform=self.platform_name,
                             error=f"发布需要{captcha_result['captcha_type']}验证码",
                             need_captcha=True,
-                            captcha_type=captcha_result['captcha_type']
+                            captcha_type=captcha_result['captcha_type'],
+                            screenshot_base64=screenshot_base64,
+                            page_url=page_url,
+                            status='need_captcha'
                         )
                 
                 await self.page.wait_for_url(
@@ -335,10 +345,15 @@ class DouyinPublisher(BasePublisher):
                 )
                 self.report_progress(100, "发布成功")
                 print(f"[{self.platform_name}] 发布成功! 已跳转到内容管理页面")
+                screenshot_base64 = await self.capture_screenshot()
+                page_url = await self.get_page_url()
                 return PublishResult(
                     success=True,
                     platform=self.platform_name,
-                    message="发布成功"
+                    message="发布成功",
+                    screenshot_base64=screenshot_base64,
+                    page_url=page_url,
+                    status='success'
                 )
             except Exception as e:
                 current_url = self.page.url
@@ -347,10 +362,14 @@ class DouyinPublisher(BasePublisher):
                 if "content/manage" in current_url:
                     self.report_progress(100, "发布成功")
                     print(f"[{self.platform_name}] 发布成功! 已在内容管理页面")
+                    screenshot_base64 = await self.capture_screenshot()
                     return PublishResult(
                         success=True,
                         platform=self.platform_name,
-                        message="发布成功"
+                        message="发布成功",
+                        screenshot_base64=screenshot_base64,
+                        page_url=current_url,
+                        status='success'
                     )
                 
                 # 检查是否有错误提示
@@ -366,11 +385,19 @@ class DouyinPublisher(BasePublisher):
                 
                 await asyncio.sleep(1)
         
-        # 发布超时,保存截图
-        screenshot_path = f"debug_publish_timeout_{self.platform_name}.png"
-        await self.page.screenshot(path=screenshot_path, full_page=True)
-        print(f"[{self.platform_name}] 发布超时,截图保存到: {screenshot_path}")
-        raise Exception(f"发布超时(截图: {screenshot_path})")
+        # 发布超时,返回截图供 AI 分析
+        print(f"[{self.platform_name}] 发布超时,获取截图供 AI 分析...")
+        screenshot_base64 = await self.capture_screenshot()
+        page_url = await self.get_page_url()
+        
+        return PublishResult(
+            success=False,
+            platform=self.platform_name,
+            error="发布超时,请检查发布状态",
+            screenshot_base64=screenshot_base64,
+            page_url=page_url,
+            status='need_action'
+        )
     
     async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:
         """获取抖音作品列表"""

+ 49 - 16
server/python/platforms/xiaohongshu.py

@@ -217,9 +217,17 @@ class XiaohongshuPublisher(BasePublisher):
         
         # 检查登录状态
         if "login" in current_url or "passport" in current_url:
-            screenshot_path = f"debug_login_required_{self.platform_name}.png"
-            await self.page.screenshot(path=screenshot_path)
-            raise Exception(f"登录已过期,请重新登录(截图: {screenshot_path})")
+            screenshot_base64 = await self.capture_screenshot()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="登录已过期,请重新登录",
+                screenshot_base64=screenshot_base64,
+                page_url=current_url,
+                status='need_captcha',
+                need_captcha=True,
+                captcha_type='login'
+            )
         
         self.report_progress(20, "正在上传视频...")
         
@@ -263,9 +271,16 @@ class XiaohongshuPublisher(BasePublisher):
                 print(f"[{self.platform_name}] 方法2失败: {e}")
         
         if not upload_triggered:
-            screenshot_path = f"debug_upload_failed_{self.platform_name}.png"
-            await self.page.screenshot(path=screenshot_path)
-            raise Exception(f"无法上传视频文件(截图: {screenshot_path})")
+            screenshot_base64 = await self.capture_screenshot()
+            page_url = await self.get_page_url()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="无法上传视频文件",
+                screenshot_base64=screenshot_base64,
+                page_url=page_url,
+                status='need_action'
+            )
         
         self.report_progress(40, "等待视频上传完成...")
         print(f"[{self.platform_name}] 等待视频上传和处理...")
@@ -290,9 +305,16 @@ class XiaohongshuPublisher(BasePublisher):
                 break
         
         if not upload_complete:
-            screenshot_path = f"debug_upload_timeout_{self.platform_name}.png"
-            await self.page.screenshot(path=screenshot_path)
-            raise Exception(f"视频上传超时(截图: {screenshot_path})")
+            screenshot_base64 = await self.capture_screenshot()
+            page_url = await self.get_page_url()
+            return PublishResult(
+                success=False,
+                platform=self.platform_name,
+                error="视频上传超时",
+                screenshot_base64=screenshot_base64,
+                page_url=page_url,
+                status='need_action'
+            )
         
         await asyncio.sleep(2)
         
@@ -447,25 +469,36 @@ class XiaohongshuPublisher(BasePublisher):
                 if "发布失败" in str(e):
                     raise
         
-        # 如果没有明确的成功标志,保存截图
+        # 如果没有明确的成功标志,返回截图供 AI 分析
         if not publish_success:
             final_url = self.page.url
             print(f"[{self.platform_name}] 发布结果不确定,当前 URL: {final_url}")
-            screenshot_path = f"debug_publish_result_{self.platform_name}.png"
-            await self.page.screenshot(path=screenshot_path, full_page=True)
-            print(f"[{self.platform_name}] 截图保存到: {screenshot_path}")
+            screenshot_base64 = await self.capture_screenshot()
+            print(f"[{self.platform_name}] 已获取截图供 AI 分析")
             
-            # 如果 URL 还是发布页面,可能发布失败
+            # 如果 URL 还是发布页面,可能需要继续操作
             if "publish/publish" in final_url:
-                raise Exception(f"发布可能失败,仍停留在发布页面(截图: {screenshot_path})")
+                return PublishResult(
+                    success=False,
+                    platform=self.platform_name,
+                    error="发布结果待确认,请查看截图",
+                    screenshot_base64=screenshot_base64,
+                    page_url=final_url,
+                    status='need_action'
+                )
         
         self.report_progress(100, "发布完成")
         print(f"[{self.platform_name}] Playwright 方式发布完成!")
+        screenshot_base64 = await self.capture_screenshot()
+        page_url = await self.get_page_url()
         
         return PublishResult(
             success=True,
             platform=self.platform_name,
-            message="发布完成"
+            message="发布完成",
+            screenshot_base64=screenshot_base64,
+            page_url=page_url,
+            status='success'
         )
     
     async def get_works(self, cookies: str, page: int = 0, page_size: int = 20) -> WorksResult:

+ 1310 - 170
server/src/ai/index.ts

@@ -1,74 +1,693 @@
 import OpenAI from 'openai';
 import { config } from '../config/index.js';
 import { logger } from '../utils/logger.js';
+import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
 
 /**
- * AI 辅助服务
+ * 消息角色类型
  */
-export class AIService {
+export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
+
+/**
+ * 聊天消息接口
+ */
+export interface ChatMessage {
+  role: MessageRole;
+  content: string;
+  name?: string;
+  tool_call_id?: string;
+}
+
+/**
+ * 聊天补全选项
+ */
+export interface ChatCompletionOptions {
+  model?: string;
+  messages: ChatMessage[];
+  temperature?: number;
+  maxTokens?: number;
+  topP?: number;
+  stream?: boolean;
+  tools?: ChatCompletionTool[];
+  toolChoice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
+  responseFormat?: { type: 'text' | 'json_object' };
+  stop?: string | string[];
+  seed?: number;
+}
+
+/**
+ * 流式响应回调
+ */
+export type StreamCallback = (chunk: string, done: boolean) => void;
+
+/**
+ * 嵌入向量选项
+ */
+export interface EmbeddingOptions {
+  model?: string;
+  input: string | string[];
+  dimensions?: number;
+}
+
+/**
+ * 视觉理解选项
+ */
+export interface VisionOptions {
+  model?: string;
+  prompt: string;
+  imageUrl?: string;
+  imageBase64?: string;
+  maxTokens?: number;
+}
+
+/**
+ * 函数定义
+ */
+export interface FunctionDefinition {
+  name: string;
+  description: string;
+  parameters: Record<string, unknown>;
+}
+
+/**
+ * 工具调用结果
+ */
+export interface ToolCallResult {
+  id: string;
+  function: {
+    name: string;
+    arguments: string;
+  };
+}
+
+/**
+ * AI 服务响应
+ */
+export interface AIResponse {
+  content: string;
+  toolCalls?: ToolCallResult[];
+  usage?: {
+    promptTokens: number;
+    completionTokens: number;
+    totalTokens: number;
+  };
+  finishReason?: string;
+}
+
+/**
+ * 登录状态分析结果
+ */
+export interface LoginStatusAnalysis {
+  isLoggedIn: boolean;
+  hasVerification: boolean;
+  verificationType?: 'captcha' | 'sms' | 'qrcode' | 'face' | 'slider' | 'other';
+  verificationDescription?: string;
+  pageDescription: string;
+  suggestedAction?: string;
+}
+
+/**
+ * 账号信息提取结果
+ */
+export interface AccountInfoExtraction {
+  found: boolean;
+  accountName?: string;
+  accountId?: string;
+  avatarDescription?: string;
+  fansCount?: string;
+  worksCount?: string;
+  otherInfo?: string;
+  navigationGuide?: string;
+}
+
+/**
+ * 页面操作指导结果
+ */
+export interface PageOperationGuide {
+  hasAction: boolean;
+  actionType?: 'click' | 'input' | 'scroll' | 'wait' | 'navigate';
+  targetDescription?: string;
+  targetSelector?: string;
+  targetPosition?: { x: number; y: number };
+  inputText?: string;
+  explanation: string;
+}
+
+/**
+ * 发布状态分析结果
+ */
+export interface PublishStatusAnalysis {
+  status: 'uploading' | 'processing' | 'success' | 'failed' | 'need_captcha' | 'need_action';
+  captchaType?: 'image' | 'sms' | 'slider' | 'other';
+  captchaDescription?: string;
+  errorMessage?: string;
+  nextAction?: {
+    actionType: 'click' | 'input' | 'wait';
+    targetDescription: string;
+    targetSelector?: string;
+  };
+  pageDescription: string;
+  confidence: number; // 0-100 表示 AI 对判断的信心程度
+}
+
+/**
+ * 阿里云百炼千问大模型 AI 服务类
+ * 
+ * 支持功能:
+ * - 聊天补全(Chat Completion)
+ * - 流式输出(Streaming)
+ * - 函数调用(Function Calling)
+ * - 视觉理解(Vision)
+ * - 文本嵌入(Embeddings)
+ * - 多模型支持
+ * - 自动重试机制
+ */
+export class QwenAIService {
   private client: OpenAI | null = null;
-  
+  private models: typeof config.ai.models;
+
   constructor() {
+    this.models = config.ai.models;
     if (config.ai.apiKey) {
       this.client = new OpenAI({
         apiKey: config.ai.apiKey,
         baseURL: config.ai.baseUrl,
+        timeout: config.ai.timeout,
+        maxRetries: config.ai.maxRetries,
       });
+      logger.info('QwenAIService initialized', { baseUrl: config.ai.baseUrl });
+    } else {
+      logger.warn('QwenAIService: API key not configured');
     }
   }
-  
+
   /**
    * 检查 AI 服务是否可用
    */
   isAvailable(): boolean {
     return !!this.client;
   }
-  
+
   /**
-   * 生成视频标题
+   * 获取可用模型列表
    */
-  async generateTitle(params: {
-    description: string;
-    platform: string;
-    maxLength?: number;
-  }): Promise<string[]> {
+  getAvailableModels(): typeof config.ai.models {
+    return this.models;
+  }
+
+  /**
+   * 确保服务可用
+   */
+  private ensureAvailable(): void {
     if (!this.client) {
-      throw new Error('AI service not configured');
+      throw new Error('AI service not configured. Please set DASHSCOPE_API_KEY environment variable.');
     }
-    
-    const { description, platform, maxLength = 50 } = params;
-    
+  }
+
+  // ==================== 核心 API 方法 ====================
+
+  /**
+   * 聊天补全 - 基础方法
+   * @param options 聊天选项
+   * @returns AI 响应
+   */
+  async chatCompletion(options: ChatCompletionOptions): Promise<AIResponse> {
+    this.ensureAvailable();
+
+    const {
+      model = this.models.chat,
+      messages,
+      temperature = 0.7,
+      maxTokens = 2000,
+      topP = 0.9,
+      tools,
+      toolChoice,
+      responseFormat,
+      stop,
+      seed,
+    } = options;
+
+    const startTime = Date.now();
+    const requestId = `chat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+    logger.info(`[AI] ========== Chat Completion Request ==========`);
+    logger.info(`[AI] Request ID: ${requestId}`);
+    logger.info(`[AI] Model: ${model}`);
+    logger.info(`[AI] Messages: ${messages.length} 条`);
+    logger.info(`[AI] Temperature: ${temperature}, MaxTokens: ${maxTokens}`);
+    if (tools) logger.info(`[AI] Tools: ${tools.length} 个函数`);
+    if (responseFormat) logger.info(`[AI] Response Format: ${responseFormat.type}`);
+
     try {
-      const response = await this.client.chat.completions.create({
-        model: 'gpt-3.5-turbo',
-        messages: [
-          {
-            role: 'system',
-            content: `你是一个专业的自媒体运营专家,擅长为${platform}平台创作吸引人的标题。标题长度不超过${maxLength}个字符。`,
+      const response = await this.client!.chat.completions.create({
+        model,
+        messages: messages as ChatCompletionMessageParam[],
+        temperature,
+        max_tokens: maxTokens,
+        top_p: topP,
+        tools,
+        tool_choice: toolChoice,
+        response_format: responseFormat,
+        stop,
+        seed,
+      });
+
+      const duration = Date.now() - startTime;
+      const choice = response.choices[0];
+
+      logger.info(`[AI] ========== Chat Completion Response ==========`);
+      logger.info(`[AI] Request ID: ${requestId}`);
+      logger.info(`[AI] Duration: ${duration}ms`);
+      logger.info(`[AI] Finish Reason: ${choice?.finish_reason}`);
+      if (response.usage) {
+        logger.info(`[AI] Tokens - Prompt: ${response.usage.prompt_tokens}, Completion: ${response.usage.completion_tokens}, Total: ${response.usage.total_tokens}`);
+      }
+      logger.info(`[AI] Response Length: ${choice?.message?.content?.length || 0} 字符`);
+      logger.info(`[AI] ==============================================`);
+
+      return {
+        content: choice?.message?.content || '',
+        toolCalls: choice?.message?.tool_calls?.map(tc => ({
+          id: tc.id,
+          function: {
+            name: tc.function.name,
+            arguments: tc.function.arguments,
           },
+        })),
+        usage: response.usage ? {
+          promptTokens: response.usage.prompt_tokens,
+          completionTokens: response.usage.completion_tokens,
+          totalTokens: response.usage.total_tokens,
+        } : undefined,
+        finishReason: choice?.finish_reason || undefined,
+      };
+    } catch (error) {
+      const duration = Date.now() - startTime;
+      logger.error(`[AI] ========== Chat Completion Error ==========`);
+      logger.error(`[AI] Request ID: ${requestId}`);
+      logger.error(`[AI] Duration: ${duration}ms`);
+      logger.error(`[AI] Error:`, error);
+      logger.error(`[AI] ============================================`);
+      throw this.handleError(error);
+    }
+  }
+
+  /**
+   * 流式聊天补全
+   * @param options 聊天选项
+   * @param callback 流式回调
+   * @returns 完整的响应内容
+   */
+  async chatCompletionStream(
+    options: Omit<ChatCompletionOptions, 'stream'>,
+    callback: StreamCallback
+  ): Promise<string> {
+    this.ensureAvailable();
+
+    const {
+      model = this.models.chat,
+      messages,
+      temperature = 0.7,
+      maxTokens = 2000,
+      topP = 0.9,
+      stop,
+    } = options;
+
+    const startTime = Date.now();
+    const requestId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+    logger.info(`[AI] ========== Stream Chat Request ==========`);
+    logger.info(`[AI] Request ID: ${requestId}`);
+    logger.info(`[AI] Model: ${model}`);
+    logger.info(`[AI] Messages: ${messages.length} 条`);
+    logger.info(`[AI] Temperature: ${temperature}, MaxTokens: ${maxTokens}`);
+
+    try {
+      const stream = await this.client!.chat.completions.create({
+        model,
+        messages: messages as ChatCompletionMessageParam[],
+        temperature,
+        max_tokens: maxTokens,
+        top_p: topP,
+        stop,
+        stream: true,
+      });
+
+      let fullContent = '';
+
+      for await (const chunk of stream) {
+        const delta = chunk.choices[0]?.delta?.content || '';
+        if (delta) {
+          fullContent += delta;
+          callback(delta, false);
+        }
+      }
+
+      callback('', true);
+
+      const duration = Date.now() - startTime;
+      logger.info(`[AI] ========== Stream Chat Response ==========`);
+      logger.info(`[AI] Request ID: ${requestId}`);
+      logger.info(`[AI] Duration: ${duration}ms`);
+      logger.info(`[AI] Response Length: ${fullContent.length} 字符`);
+      logger.info(`[AI] =============================================`);
+
+      return fullContent;
+    } catch (error) {
+      const duration = Date.now() - startTime;
+      logger.error(`[AI] ========== Stream Chat Error ==========`);
+      logger.error(`[AI] Request ID: ${requestId}`);
+      logger.error(`[AI] Duration: ${duration}ms`);
+      logger.error(`[AI] Error:`, error);
+      logger.error(`[AI] =========================================`);
+      throw this.handleError(error);
+    }
+  }
+
+  /**
+   * 简单对话 - 便捷方法
+   * @param prompt 用户提示
+   * @param systemPrompt 系统提示(可选)
+   * @param model 模型(可选)
+   * @returns AI 回复内容
+   */
+  async chat(prompt: string, systemPrompt?: string, model?: string): Promise<string> {
+    const messages: ChatMessage[] = [];
+
+    if (systemPrompt) {
+      messages.push({ role: 'system', content: systemPrompt });
+    }
+    messages.push({ role: 'user', content: prompt });
+
+    const response = await this.chatCompletion({
+      model: model || this.models.chat,
+      messages,
+    });
+
+    return response.content;
+  }
+
+  /**
+   * 快速对话 - 使用快速模型
+   */
+  async quickChat(prompt: string, systemPrompt?: string): Promise<string> {
+    return this.chat(prompt, systemPrompt, this.models.fast);
+  }
+
+  /**
+   * 推理对话 - 使用推理模型(适合复杂逻辑问题)
+   */
+  async reasoningChat(prompt: string, systemPrompt?: string): Promise<string> {
+    return this.chat(prompt, systemPrompt, this.models.reasoning);
+  }
+
+  /**
+   * 代码生成/分析 - 使用代码模型
+   */
+  async codeChat(prompt: string, systemPrompt?: string): Promise<string> {
+    const defaultCodeSystemPrompt = '你是一个专业的编程助手,擅长代码编写、分析和调试。请提供清晰、高效的代码解决方案。';
+    return this.chat(prompt, systemPrompt || defaultCodeSystemPrompt, this.models.coder);
+  }
+
+  // ==================== 函数调用 ====================
+
+  /**
+   * 带函数调用的对话
+   * @param messages 消息列表
+   * @param functions 函数定义列表
+   * @param toolChoice 工具选择策略
+   */
+  async chatWithFunctions(
+    messages: ChatMessage[],
+    functions: FunctionDefinition[],
+    toolChoice: 'auto' | 'none' | string = 'auto'
+  ): Promise<AIResponse> {
+    const tools: ChatCompletionTool[] = functions.map(fn => ({
+      type: 'function' as const,
+      function: {
+        name: fn.name,
+        description: fn.description,
+        parameters: fn.parameters,
+      },
+    }));
+
+    const choice = toolChoice === 'auto' || toolChoice === 'none'
+      ? toolChoice
+      : { type: 'function' as const, function: { name: toolChoice } };
+
+    return this.chatCompletion({
+      messages,
+      tools,
+      toolChoice: choice,
+    });
+  }
+
+  /**
+   * 执行函数调用循环
+   * @param messages 初始消息
+   * @param functions 函数定义
+   * @param functionExecutor 函数执行器
+   * @param maxIterations 最大迭代次数
+   */
+  async runFunctionLoop(
+    messages: ChatMessage[],
+    functions: FunctionDefinition[],
+    functionExecutor: (name: string, args: Record<string, unknown>) => Promise<string>,
+    maxIterations: number = 10
+  ): Promise<string> {
+    const conversationMessages = [...messages];
+
+    for (let i = 0; i < maxIterations; i++) {
+      const response = await this.chatWithFunctions(conversationMessages, functions);
+
+      if (!response.toolCalls || response.toolCalls.length === 0) {
+        return response.content;
+      }
+
+      // 添加助手消息(包含工具调用)
+      conversationMessages.push({
+        role: 'assistant',
+        content: response.content || '',
+      });
+
+      // 执行每个工具调用
+      for (const toolCall of response.toolCalls) {
+        try {
+          const args = JSON.parse(toolCall.function.arguments);
+          const result = await functionExecutor(toolCall.function.name, args);
+
+          conversationMessages.push({
+            role: 'tool',
+            content: result,
+            tool_call_id: toolCall.id,
+          });
+        } catch (error) {
+          conversationMessages.push({
+            role: 'tool',
+            content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
+            tool_call_id: toolCall.id,
+          });
+        }
+      }
+    }
+
+    throw new Error('Function loop exceeded maximum iterations');
+  }
+
+  // ==================== 视觉理解 ====================
+
+  /**
+   * 图像理解
+   * @param options 视觉选项
+   */
+  async analyzeImage(options: VisionOptions): Promise<string> {
+    this.ensureAvailable();
+
+    const { model = this.models.vision, prompt, imageUrl, imageBase64, maxTokens = 1000 } = options;
+
+    if (!imageUrl && !imageBase64) {
+      throw new Error('Either imageUrl or imageBase64 must be provided');
+    }
+
+    const imageContent = imageUrl
+      ? { type: 'image_url' as const, image_url: { url: imageUrl } }
+      : { type: 'image_url' as const, image_url: { url: `data:image/jpeg;base64,${imageBase64}` } };
+
+    const startTime = Date.now();
+    const requestId = `vision_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    const imageSize = imageBase64 ? Math.round(imageBase64.length / 1024) : 0;
+
+    logger.info(`[AI] ========== Vision Analysis Request ==========`);
+    logger.info(`[AI] Request ID: ${requestId}`);
+    logger.info(`[AI] Model: ${model}`);
+    logger.info(`[AI] Image: ${imageUrl ? 'URL' : `Base64 (${imageSize}KB)`}`);
+    logger.info(`[AI] Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
+    logger.info(`[AI] MaxTokens: ${maxTokens}`);
+
+    try {
+      const response = await this.client!.chat.completions.create({
+        model,
+        messages: [
           {
             role: 'user',
-            content: `请根据以下视频内容描述,生成5个不同风格的标题:\n${description}`,
+            content: [
+              imageContent,
+              { type: 'text', text: prompt },
+            ],
           },
         ],
-        temperature: 0.8,
-        max_tokens: 500,
+        max_tokens: maxTokens,
       });
-      
+
+      const duration = Date.now() - startTime;
       const content = response.choices[0]?.message?.content || '';
-      const titles = content
-        .split('\n')
-        .filter(line => line.trim())
-        .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
-        .filter(title => title.length > 0 && title.length <= maxLength);
-      
-      return titles.slice(0, 5);
+
+      logger.info(`[AI] ========== Vision Analysis Response ==========`);
+      logger.info(`[AI] Request ID: ${requestId}`);
+      logger.info(`[AI] Duration: ${duration}ms`);
+      logger.info(`[AI] Finish Reason: ${response.choices[0]?.finish_reason}`);
+      if (response.usage) {
+        logger.info(`[AI] Tokens - Prompt: ${response.usage.prompt_tokens}, Completion: ${response.usage.completion_tokens}, Total: ${response.usage.total_tokens}`);
+      }
+      logger.info(`[AI] Response Length: ${content.length} 字符`);
+      logger.info(`[AI] Response Preview: ${content.substring(0, 150)}${content.length > 150 ? '...' : ''}`);
+      logger.info(`[AI] ================================================`);
+
+      return content;
     } catch (error) {
-      logger.error('AI generateTitle error:', error);
-      throw error;
+      const duration = Date.now() - startTime;
+      logger.error(`[AI] ========== Vision Analysis Error ==========`);
+      logger.error(`[AI] Request ID: ${requestId}`);
+      logger.error(`[AI] Duration: ${duration}ms`);
+      logger.error(`[AI] Error:`, error);
+      logger.error(`[AI] =============================================`);
+      throw this.handleError(error);
     }
   }
-  
+
+  // ==================== 文本嵌入 ====================
+
+  /**
+   * 生成文本嵌入向量
+   * @param options 嵌入选项
+   */
+  async createEmbedding(options: EmbeddingOptions): Promise<number[][]> {
+    this.ensureAvailable();
+
+    const { model = this.models.embedding, input, dimensions } = options;
+
+    const startTime = Date.now();
+    const requestId = `embed_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    const inputCount = Array.isArray(input) ? input.length : 1;
+
+    logger.info(`[AI] ========== Embedding Request ==========`);
+    logger.info(`[AI] Request ID: ${requestId}`);
+    logger.info(`[AI] Model: ${model}`);
+    logger.info(`[AI] Input Count: ${inputCount}`);
+    if (dimensions) logger.info(`[AI] Dimensions: ${dimensions}`);
+
+    try {
+      const response = await this.client!.embeddings.create({
+        model,
+        input,
+        dimensions,
+      });
+
+      const duration = Date.now() - startTime;
+      logger.info(`[AI] ========== Embedding Response ==========`);
+      logger.info(`[AI] Request ID: ${requestId}`);
+      logger.info(`[AI] Duration: ${duration}ms`);
+      logger.info(`[AI] Vectors: ${response.data.length}`);
+      logger.info(`[AI] Vector Dimension: ${response.data[0]?.embedding?.length || 0}`);
+      logger.info(`[AI] ==========================================`);
+
+      return response.data.map(item => item.embedding);
+    } catch (error) {
+      const duration = Date.now() - startTime;
+      logger.error(`[AI] ========== Embedding Error ==========`);
+      logger.error(`[AI] Request ID: ${requestId}`);
+      logger.error(`[AI] Duration: ${duration}ms`);
+      logger.error(`[AI] Error:`, error);
+      logger.error(`[AI] ======================================`);
+      throw this.handleError(error);
+    }
+  }
+
+  /**
+   * 计算文本相似度
+   * @param text1 文本1
+   * @param text2 文本2
+   * @returns 相似度分数 (0-1)
+   */
+  async calculateSimilarity(text1: string, text2: string): Promise<number> {
+    const embeddings = await this.createEmbedding({ input: [text1, text2] });
+    return this.cosineSimilarity(embeddings[0], embeddings[1]);
+  }
+
+  // ==================== JSON 结构化输出 ====================
+
+  /**
+   * 生成 JSON 结构化响应
+   * @param prompt 提示
+   * @param schema JSON Schema 描述
+   * @param systemPrompt 系统提示
+   */
+  async generateJSON<T = unknown>(
+    prompt: string,
+    schema: string,
+    systemPrompt?: string
+  ): Promise<T> {
+    const systemMessage = systemPrompt ||
+      `你是一个数据处理助手。请严格按照用户要求的JSON格式输出,不要添加任何额外的说明文字。
+输出格式要求:${schema}`;
+
+    const response = await this.chatCompletion({
+      messages: [
+        { role: 'system', content: systemMessage },
+        { role: 'user', content: prompt },
+      ],
+      responseFormat: { type: 'json_object' },
+      temperature: 0.3,
+    });
+
+    try {
+      return JSON.parse(response.content) as T;
+    } catch {
+      // 尝试提取 JSON
+      const jsonMatch = response.content.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
+      if (jsonMatch) {
+        return JSON.parse(jsonMatch[0]) as T;
+      }
+      throw new Error('Failed to parse JSON response');
+    }
+  }
+
+  // ==================== 业务场景方法 ====================
+
+  /**
+   * 生成视频标题
+   */
+  async generateTitle(params: {
+    description: string;
+    platform: string;
+    maxLength?: number;
+  }): Promise<string[]> {
+    const { description, platform, maxLength = 50 } = params;
+
+    const response = await this.chat(
+      `请根据以下视频内容描述,生成5个不同风格的标题:\n${description}`,
+      `你是一个专业的自媒体运营专家,擅长为${platform}平台创作吸引人的标题。标题长度不超过${maxLength}个字符。每个标题独占一行,不要添加序号。`
+    );
+
+    const titles = response
+      .split('\n')
+      .filter(line => line.trim())
+      .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
+      .filter(title => title.length > 0 && title.length <= maxLength);
+
+    return titles.slice(0, 5);
+  }
+
   /**
    * 生成标签推荐
    */
@@ -78,42 +697,21 @@ export class AIService {
     platform: string;
     maxTags?: number;
   }): Promise<string[]> {
-    if (!this.client) {
-      throw new Error('AI service not configured');
-    }
-    
     const { title, description, platform, maxTags = 5 } = params;
-    
-    try {
-      const response = await this.client.chat.completions.create({
-        model: 'gpt-3.5-turbo',
-        messages: [
-          {
-            role: 'system',
-            content: `你是一个专业的自媒体运营专家,擅长为${platform}平台选择热门标签。每个标签不要带#号,用逗号分隔。`,
-          },
-          {
-            role: 'user',
-            content: `请根据以下视频信息推荐${maxTags}个相关标签:\n标题:${title}\n${description ? `描述:${description}` : ''}`,
-          },
-        ],
-        temperature: 0.7,
-        max_tokens: 200,
-      });
-      
-      const content = response.choices[0]?.message?.content || '';
-      const tags = content
-        .split(/[,,\n]/)
-        .map(tag => tag.trim().replace(/^#/, ''))
-        .filter(tag => tag.length > 0);
-      
-      return tags.slice(0, maxTags);
-    } catch (error) {
-      logger.error('AI generateTags error:', error);
-      throw error;
-    }
+
+    const response = await this.chat(
+      `请根据以下视频信息推荐${maxTags}个相关标签:\n标题:${title}\n${description ? `描述:${description}` : ''}`,
+      `你是一个专业的自媒体运营专家,擅长为${platform}平台选择热门标签。每个标签不要带#号,用逗号分隔,只输出标签不要其他内容。`
+    );
+
+    const tags = response
+      .split(/[,,\n]/)
+      .map(tag => tag.trim().replace(/^#/, ''))
+      .filter(tag => tag.length > 0);
+
+    return tags.slice(0, maxTags);
   }
-  
+
   /**
    * 优化内容描述
    */
@@ -122,37 +720,16 @@ export class AIService {
     platform: string;
     maxLength?: number;
   }): Promise<string> {
-    if (!this.client) {
-      throw new Error('AI service not configured');
-    }
-    
     const { original, platform, maxLength = 500 } = params;
-    
-    try {
-      const response = await this.client.chat.completions.create({
-        model: 'gpt-3.5-turbo',
-        messages: [
-          {
-            role: 'system',
-            content: `你是一个专业的自媒体运营专家,擅长为${platform}平台优化视频描述。优化后的描述要吸引人、有互动性,长度不超过${maxLength}个字符。`,
-          },
-          {
-            role: 'user',
-            content: `请优化以下视频描述:\n${original}`,
-          },
-        ],
-        temperature: 0.7,
-        max_tokens: 600,
-      });
-      
-      const content = response.choices[0]?.message?.content || original;
-      return content.slice(0, maxLength);
-    } catch (error) {
-      logger.error('AI optimizeDescription error:', error);
-      throw error;
-    }
+
+    const response = await this.chat(
+      `请优化以下视频描述:\n${original}`,
+      `你是一个专业的自媒体运营专家,擅长为${platform}平台优化视频描述。优化后的描述要吸引人、有互动性,长度不超过${maxLength}个字符。直接输出优化后的描述,不要添加其他说明。`
+    );
+
+    return response.slice(0, maxLength);
   }
-  
+
   /**
    * 生成评论回复
    */
@@ -162,49 +739,28 @@ export class AIService {
     context?: string;
     tone?: 'friendly' | 'professional' | 'humorous';
   }): Promise<string[]> {
-    if (!this.client) {
-      throw new Error('AI service not configured');
-    }
-    
     const { comment, authorName, context, tone = 'friendly' } = params;
-    
+
     const toneDesc = {
       friendly: '友好亲切',
       professional: '专业正式',
       humorous: '幽默风趣',
     };
-    
-    try {
-      const response = await this.client.chat.completions.create({
-        model: 'gpt-3.5-turbo',
-        messages: [
-          {
-            role: 'system',
-            content: `你是一个自媒体账号运营人员,需要用${toneDesc[tone]}的语气回复粉丝评论。回复要简洁、有互动性,表达对粉丝的感谢。`,
-          },
-          {
-            role: 'user',
-            content: `粉丝"${authorName}"的评论:${comment}\n${context ? `视频内容:${context}` : ''}\n请生成3个不同的回复选项。`,
-          },
-        ],
-        temperature: 0.8,
-        max_tokens: 300,
-      });
-      
-      const content = response.choices[0]?.message?.content || '';
-      const replies = content
-        .split('\n')
-        .filter(line => line.trim())
-        .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
-        .filter(reply => reply.length > 0);
-      
-      return replies.slice(0, 3);
-    } catch (error) {
-      logger.error('AI generateReply error:', error);
-      throw error;
-    }
+
+    const response = await this.chat(
+      `粉丝"${authorName}"的评论:${comment}\n${context ? `视频内容:${context}` : ''}\n请生成3个不同的回复选项,每个回复独占一行。`,
+      `你是一个自媒体账号运营人员,需要用${toneDesc[tone]}的语气回复粉丝评论。回复要简洁、有互动性,表达对粉丝的感谢。`
+    );
+
+    const replies = response
+      .split('\n')
+      .filter(line => line.trim())
+      .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
+      .filter(reply => reply.length > 0);
+
+    return replies.slice(0, 3);
   }
-  
+
   /**
    * 推荐最佳发布时间
    */
@@ -213,51 +769,635 @@ export class AIService {
     contentType: string;
     targetAudience?: string;
   }): Promise<{ time: string; reason: string }[]> {
-    if (!this.client) {
-      throw new Error('AI service not configured');
-    }
-    
     const { platform, contentType, targetAudience } = params;
-    
+
     try {
-      const response = await this.client.chat.completions.create({
-        model: 'gpt-3.5-turbo',
-        messages: [
-          {
-            role: 'system',
-            content: '你是一个数据分析专家,熟悉各自媒体平台的用户活跃规律。',
-          },
-          {
-            role: 'user',
-            content: `请为${platform}平台的${contentType}类型内容推荐3个最佳发布时间。${targetAudience ? `目标受众:${targetAudience}` : ''}\n请以JSON数组格式返回,每项包含time(HH:mm格式)和reason字段。`,
-          },
-        ],
-        temperature: 0.5,
-        max_tokens: 300,
-      });
-      
-      const content = response.choices[0]?.message?.content || '[]';
-      
-      try {
-        // 尝试提取 JSON
-        const jsonMatch = content.match(/\[[\s\S]*\]/);
-        if (jsonMatch) {
-          return JSON.parse(jsonMatch[0]);
-        }
-      } catch {
-        // 解析失败,返回默认推荐
-      }
-      
+      const result = await this.generateJSON<{ time: string; reason: string }[]>(
+        `请为${platform}平台的${contentType}类型内容推荐3个最佳发布时间。${targetAudience ? `目标受众:${targetAudience}` : ''}`,
+        '返回JSON数组,每项包含time(HH:mm格式)和reason字段',
+        '你是一个数据分析专家,熟悉各自媒体平台的用户活跃规律。'
+      );
+
+      return Array.isArray(result) ? result : [];
+    } catch {
       return [
         { time: '12:00', reason: '午休时间,用户活跃度高' },
         { time: '18:00', reason: '下班时间,通勤路上刷手机' },
         { time: '21:00', reason: '晚间黄金时段,用户放松娱乐' },
       ];
+    }
+  }
+
+  /**
+   * 内容审核
+   */
+  async moderateContent(content: string): Promise<{
+    safe: boolean;
+    categories: string[];
+    suggestion: string;
+  }> {
+    try {
+      return await this.generateJSON(
+        `请审核以下内容是否合规:\n${content}`,
+        '返回JSON对象,包含safe(布尔值)、categories(问题类别数组,如空则为[])、suggestion(修改建议)字段',
+        '你是一个内容审核专家,需要检查内容是否包含:违法违规、色情低俗、暴力血腥、政治敏感、虚假信息等问题。'
+      );
+    } catch {
+      return { safe: true, categories: [], suggestion: '' };
+    }
+  }
+
+  /**
+   * 内容摘要生成
+   */
+  async summarize(content: string, maxLength: number = 200): Promise<string> {
+    return this.quickChat(
+      `请将以下内容总结为不超过${maxLength}字的摘要:\n${content}`,
+      '你是一个专业的文字编辑,擅长提炼核心内容。直接输出摘要,不要添加任何前缀。'
+    );
+  }
+
+  /**
+   * 文本翻译
+   */
+  async translate(
+    text: string,
+    targetLang: string = '英文',
+    sourceLang?: string
+  ): Promise<string> {
+    const prompt = sourceLang
+      ? `请将以下${sourceLang}文本翻译成${targetLang}:\n${text}`
+      : `请将以下文本翻译成${targetLang}:\n${text}`;
+
+    return this.chat(prompt, '你是一个专业翻译,请提供准确、自然的翻译。直接输出翻译结果。');
+  }
+
+  /**
+   * 关键词提取
+   */
+  async extractKeywords(text: string, count: number = 5): Promise<string[]> {
+    const response = await this.chat(
+      `请从以下文本中提取${count}个关键词,用逗号分隔:\n${text}`,
+      '你是一个文本分析专家。只输出关键词,不要其他内容。'
+    );
+
+    return response.split(/[,,]/).map(k => k.trim()).filter(k => k).slice(0, count);
+  }
+
+  // ==================== 登录页面分析 ====================
+
+  /**
+   * 分析登录页面状态
+   * @param imageBase64 页面截图的 Base64 编码
+   * @param platform 平台名称
+   * @returns 登录状态分析结果
+   */
+  async analyzeLoginStatus(imageBase64: string, platform: string): Promise<LoginStatusAnalysis> {
+    const prompt = `请分析这张${platform}平台的网页截图,判断以下内容:
+
+1. 用户是否已经登录成功?(判断依据:是否能看到用户头像、用户名、个人中心入口、创作者后台等已登录状态的元素)
+2. 页面上是否有验证码或其他二次验证?(如:图形验证码、滑块验证、短信验证码输入框、扫码验证、人脸识别提示等)
+3. 如果有验证,是什么类型的验证?
+4. 简要描述当前页面的状态
+
+请严格按照以下JSON格式返回:
+{
+  "isLoggedIn": true或false,
+  "hasVerification": true或false,
+  "verificationType": "captcha"或"sms"或"qrcode"或"face"或"slider"或"other"或null,
+  "verificationDescription": "验证的具体描述,如果没有验证则为null",
+  "pageDescription": "当前页面状态的简要描述",
+  "suggestedAction": "建议用户进行的操作,如果不需要则为null"
+}`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 500,
+      });
+
+      // 尝试解析 JSON
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          isLoggedIn: Boolean(result.isLoggedIn),
+          hasVerification: Boolean(result.hasVerification),
+          verificationType: result.verificationType || undefined,
+          verificationDescription: result.verificationDescription || undefined,
+          pageDescription: result.pageDescription || '无法解析页面状态',
+          suggestedAction: result.suggestedAction || undefined,
+        };
+      }
+
+      // 如果无法解析 JSON,返回默认值
+      return {
+        isLoggedIn: false,
+        hasVerification: false,
+        pageDescription: response,
+      };
     } catch (error) {
-      logger.error('AI recommendPublishTime error:', error);
-      throw error;
+      logger.error('analyzeLoginStatus error:', error);
+      return {
+        isLoggedIn: false,
+        hasVerification: false,
+        pageDescription: '分析失败',
+      };
     }
   }
+
+  /**
+   * 从页面截图中提取账号信息
+   * @param imageBase64 页面截图的 Base64 编码
+   * @param platform 平台名称
+   * @returns 账号信息提取结果
+   */
+  async extractAccountInfo(imageBase64: string, platform: string): Promise<AccountInfoExtraction> {
+    // 根据平台提供更具体的提示
+    const platformHints: Record<string, string> = {
+      baijiahao: `
+百家号平台常见的账号信息位置:
+- 页面左上角或侧边栏可能显示作者名称和头像
+- 侧边栏"首页"或"个人中心"菜单可以进入账号信息页面
+- 头部右上角可能有用户头像和下拉菜单
+- 账号设置页面会显示完整的账号信息`,
+      douyin: `
+抖音平台常见的账号信息位置:
+- 页面顶部或右上角显示用户头像和昵称
+- 点击头像可以进入个人主页
+- 侧边栏可能有"我的"或"个人中心"入口`,
+      xiaohongshu: `
+小红书平台常见的账号信息位置:
+- 页面左侧或顶部显示创作者信息
+- 侧边栏有"个人中心"或"账号管理"入口`,
+      kuaishou: `
+快手平台常见的账号信息位置:
+- 页面顶部显示用户信息
+- 侧边栏有创作者中心入口`,
+      weixin_video: `
+视频号平台常见的账号信息位置:
+- 页面左上角显示账号名称
+- 侧边栏有账号设置入口`,
+    };
+
+    const platformHint = platformHints[platform] || '';
+
+    const prompt = `请仔细分析这张${platform}平台的网页截图,尝试提取以下账号信息:
+
+1. 账号名称/昵称(这是最重要的信息,请仔细查找页面上的用户名、作者名、昵称等)
+2. 账号ID(如果可见)
+3. 头像描述(如果可见)
+4. 粉丝数量(如果可见)
+5. 作品数量(如果可见)
+6. 其他相关信息
+${platformHint}
+
+重要提示:
+- 账号名称可能显示在页面顶部、侧边栏、头像旁边等位置
+- 如果看到任何类似用户名或昵称的文字,请提取出来
+- 即使只找到账号名称,也请返回 found: true
+
+如果当前页面确实没有显示任何账号信息,请告诉我应该如何操作才能看到账号信息。
+
+请严格按照以下JSON格式返回:
+{
+  "found": true或false(是否找到账号信息,只要找到账号名称就算找到),
+  "accountName": "账号名称/昵称,如果找不到则为null",
+  "accountId": "账号ID,如果找不到则为null",
+  "avatarDescription": "头像描述,如果看不到则为null",
+  "fansCount": "粉丝数量(数字),如果看不到则为null",
+  "worksCount": "作品数量(数字),如果看不到则为null",
+  "otherInfo": "其他相关信息,如果没有则为null",
+  "navigationGuide": "如果没找到账号信息,请描述具体的操作步骤(如:点击左侧菜单的'个人中心'),如果已找到则为null"
+}`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 600,
+      });
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          found: Boolean(result.found),
+          accountName: result.accountName || undefined,
+          accountId: result.accountId || undefined,
+          avatarDescription: result.avatarDescription || undefined,
+          fansCount: result.fansCount || undefined,
+          worksCount: result.worksCount || undefined,
+          otherInfo: result.otherInfo || undefined,
+          navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
+        };
+      }
+
+      return {
+        found: false,
+        navigationGuide: response,
+      };
+    } catch (error) {
+      logger.error('extractAccountInfo error:', error);
+      return {
+        found: false,
+        navigationGuide: '分析失败,请手动查看页面',
+      };
+    }
+  }
+
+  /**
+   * 获取页面操作指导
+   * @param imageBase64 页面截图的 Base64 编码
+   * @param platform 平台名称
+   * @param goal 操作目标(如:"获取账号信息"、"完成登录")
+   * @returns 页面操作指导
+   */
+  async getPageOperationGuide(
+    imageBase64: string,
+    platform: string,
+    goal: string
+  ): Promise<PageOperationGuide> {
+    const prompt = `请分析这张${platform}平台的网页截图,我的目标是:${goal}
+
+请告诉我下一步应该进行什么操作。如果需要点击某个元素,请尽可能提供:
+1. 操作类型(点击、输入、滚动、等待、跳转)
+2. 目标元素的描述(文字内容、位置描述)
+3. 如果是点击操作,估计目标在截图中的大致位置(假设截图尺寸为 1920x1080,给出x,y坐标)
+4. 如果是输入操作,需要输入什么内容
+
+请严格按照以下JSON格式返回:
+{
+  "hasAction": true或false(是否需要执行操作),
+  "actionType": "click"或"input"或"scroll"或"wait"或"navigate"或null,
+  "targetDescription": "目标元素的文字描述",
+  "targetSelector": "可能的CSS选择器,如果能推断的话",
+  "targetPosition": {"x": 数字, "y": 数字} 或 null,
+  "inputText": "需要输入的文字,如果不需要输入则为null",
+  "explanation": "操作说明和原因"
+}`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 500,
+      });
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          hasAction: Boolean(result.hasAction),
+          actionType: result.actionType || undefined,
+          targetDescription: result.targetDescription || undefined,
+          targetSelector: result.targetSelector || undefined,
+          targetPosition: result.targetPosition || undefined,
+          inputText: result.inputText || undefined,
+          explanation: result.explanation || '无法解析操作指导',
+        };
+      }
+
+      return {
+        hasAction: false,
+        explanation: response,
+      };
+    } catch (error) {
+      logger.error('getPageOperationGuide error:', error);
+      return {
+        hasAction: false,
+        explanation: '分析失败',
+      };
+    }
+  }
+
+  /**
+   * 通过 HTML 分析页面并返回操作指导
+   * @param html 页面 HTML 内容
+   * @param platform 平台名称
+   * @param goal 操作目标
+   * @returns 页面操作指导(包含精确的 CSS 选择器)
+   */
+  async analyzeHtmlForOperation(
+    html: string,
+    platform: string,
+    goal: string
+  ): Promise<PageOperationGuide> {
+    this.ensureAvailable();
+
+    // 简化 HTML,移除不必要的内容,保留关键元素
+    const simplifiedHtml = this.simplifyHtml(html);
+
+    const prompt = `你是一个网页自动化助手。我正在${platform}平台上操作,目标是:${goal}
+
+以下是当前页面的HTML结构(已简化):
+\`\`\`html
+${simplifiedHtml}
+\`\`\`
+
+请分析这个页面,告诉我下一步应该进行什么操作来达成目标。
+
+要求:
+1. 识别页面当前状态(是否已登录、是否有验证码、是否有弹窗等)
+2. 找出需要操作的目标元素(按钮、链接、输入框等)
+3. 提供精确的 CSS 选择器来定位该元素
+4. 选择器要尽可能唯一和稳定(优先使用 id、data-* 属性、唯一 class)
+
+请严格按照以下JSON格式返回:
+{
+  "hasAction": true或false,
+  "actionType": "click" | "input" | "scroll" | "wait" | null,
+  "targetSelector": "精确的CSS选择器,如 #login-btn 或 button[data-action='login'] 或 .login-button",
+  "targetDescription": "目标元素的描述",
+  "inputText": "如果是输入操作,需要输入的内容,否则为null",
+  "explanation": "操作说明和当前页面状态分析"
 }
 
-export const aiService = new AIService();
+注意:
+- CSS选择器必须能够唯一定位到目标元素
+- 如果有多个相似元素,使用更具体的选择器(如 :first-child, :nth-child(n))
+- 优先使用 id 选择器,其次是 data-* 属性,再次是唯一的 class
+- 如果页面已完成目标(如已登录成功),返回 hasAction: false`;
+
+    try {
+      const response = await this.chat(prompt, undefined, this.models.chat);
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          hasAction: Boolean(result.hasAction),
+          actionType: result.actionType || undefined,
+          targetDescription: result.targetDescription || undefined,
+          targetSelector: result.targetSelector || undefined,
+          inputText: result.inputText || undefined,
+          explanation: result.explanation || '无法解析操作指导',
+        };
+      }
+
+      return {
+        hasAction: false,
+        explanation: response,
+      };
+    } catch (error) {
+      logger.error('analyzeHtmlForOperation error:', error);
+      return {
+        hasAction: false,
+        explanation: '分析失败',
+      };
+    }
+  }
+
+  /**
+   * 简化 HTML,移除不必要的内容
+   */
+  private simplifyHtml(html: string): string {
+    let simplified = html;
+
+    // 移除 script 标签及内容
+    simplified = simplified.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
+
+    // 移除 style 标签及内容
+    simplified = simplified.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
+
+    // 移除 HTML 注释
+    simplified = simplified.replace(/<!--[\s\S]*?-->/g, '');
+
+    // 移除 svg 内容(保留标签)
+    simplified = simplified.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '<svg></svg>');
+
+    // 移除 noscript
+    simplified = simplified.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '');
+
+    // 移除 data:image 等 base64 内容
+    simplified = simplified.replace(/data:[^"'\s]+/g, 'data:...');
+
+    // 移除过长的属性值(如内联样式)
+    simplified = simplified.replace(/style="[^"]{100,}"/gi, 'style="..."');
+
+    // 压缩连续空白
+    simplified = simplified.replace(/\s+/g, ' ');
+
+    // 限制总长度(避免 token 超限)
+    const maxLength = 30000;
+    if (simplified.length > maxLength) {
+      // 尝试保留 body 部分
+      const bodyMatch = simplified.match(/<body[^>]*>([\s\S]*)<\/body>/i);
+      if (bodyMatch) {
+        simplified = bodyMatch[1];
+      }
+      // 如果还是太长,截断
+      if (simplified.length > maxLength) {
+        simplified = simplified.substring(0, maxLength) + '\n... (HTML 已截断)';
+      }
+    }
+
+    return simplified.trim();
+  }
+
+  // ==================== 发布辅助 ====================
+
+  /**
+   * 分析发布页面状态
+   * @param imageBase64 页面截图的 Base64 编码
+   * @param platform 平台名称
+   * @returns 发布状态分析结果
+   */
+  async analyzePublishStatus(imageBase64: string, platform: string): Promise<PublishStatusAnalysis> {
+    const prompt = `请分析这张${platform}平台视频发布页面的截图,判断当前的发布状态。
+
+请仔细观察页面,判断:
+1. 是否正在上传/处理视频(显示进度条、loading 等)
+2. 是否发布成功(显示成功提示、跳转到作品列表等)
+3. 是否发布失败(显示错误提示)
+4. 是否需要输入验证码(图片验证码、滑块验证、短信验证码等)
+5. 是否需要进行其他操作才能继续发布(如点击发布按钮、确认信息等)
+
+请严格按照以下JSON格式返回:
+{
+  "status": "uploading" 或 "processing" 或 "success" 或 "failed" 或 "need_captcha" 或 "need_action",
+  "captchaType": "image" 或 "sms" 或 "slider" 或 "other" 或 null(仅当 status 为 need_captcha 时填写),
+  "captchaDescription": "验证码的具体描述(如:请输入图片中的4位数字)",
+  "errorMessage": "错误信息(仅当 status 为 failed 时填写)",
+  "nextAction": {
+    "actionType": "click" 或 "input" 或 "wait",
+    "targetDescription": "需要操作的目标描述",
+    "targetSelector": "目标元素的CSS选择器(如果能推断的话)"
+  } 或 null,
+  "pageDescription": "当前页面状态的详细描述",
+  "confidence": 0-100 之间的数字,表示你对这个判断的信心程度
+}
+
+注意:
+- 如果看到"发布成功"、"上传完成"等字样,status 应为 "success"
+- 如果看到验证码输入框、滑块验证等,status 应为 "need_captcha"
+- 如果页面显示发布按钮但还未点击,status 应为 "need_action"
+- 如果页面正在加载或显示进度,status 应为 "uploading" 或 "processing"`;
+
+    try {
+      const response = await this.analyzeImage({
+        imageBase64,
+        prompt,
+        maxTokens: 600,
+      });
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          status: result.status || 'need_action',
+          captchaType: result.captchaType || undefined,
+          captchaDescription: result.captchaDescription || undefined,
+          errorMessage: result.errorMessage || undefined,
+          nextAction: result.nextAction || undefined,
+          pageDescription: result.pageDescription || '无法解析页面状态',
+          confidence: result.confidence || 50,
+        };
+      }
+
+      return {
+        status: 'need_action',
+        pageDescription: response,
+        confidence: 30,
+      };
+    } catch (error) {
+      logger.error('analyzePublishStatus error:', error);
+      return {
+        status: 'need_action',
+        pageDescription: '分析失败',
+        confidence: 0,
+      };
+    }
+  }
+
+  /**
+   * 分析发布页面 HTML 并获取操作指导
+   * @param html 页面 HTML 内容
+   * @param platform 平台名称
+   * @param currentStatus 当前发布状态
+   * @returns 操作指导
+   */
+  async analyzePublishPageHtml(
+    html: string,
+    platform: string,
+    currentStatus: string
+  ): Promise<PageOperationGuide> {
+    const simplifiedHtml = this.simplifyHtml(html);
+
+    const prompt = `你是一个自动化发布助手。我正在${platform}平台上发布视频,当前状态是:${currentStatus}
+
+以下是当前页面的HTML结构(已简化):
+\`\`\`html
+${simplifiedHtml}
+\`\`\`
+
+请分析这个页面,告诉我下一步应该进行什么操作来完成发布。
+
+要求:
+1. 如果需要点击"发布"按钮,找到正确的发布按钮
+2. 如果需要输入验证码,找到验证码输入框
+3. 如果需要确认/关闭弹窗,找到相应按钮
+4. 提供精确的 CSS 选择器来定位目标元素
+
+请严格按照以下JSON格式返回:
+{
+  "hasAction": true或false,
+  "actionType": "click" | "input" | "wait" | null,
+  "targetSelector": "精确的CSS选择器",
+  "targetDescription": "目标元素的描述",
+  "inputText": "如果是输入操作,需要输入的内容,否则为null",
+  "explanation": "操作说明"
+}`;
+
+    try {
+      const response = await this.chat(prompt, undefined, this.models.chat);
+
+      const jsonMatch = response.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const result = JSON.parse(jsonMatch[0]);
+        return {
+          hasAction: Boolean(result.hasAction),
+          actionType: result.actionType || undefined,
+          targetDescription: result.targetDescription || undefined,
+          targetSelector: result.targetSelector || undefined,
+          inputText: result.inputText || undefined,
+          explanation: result.explanation || '无法解析操作指导',
+        };
+      }
+
+      return {
+        hasAction: false,
+        explanation: response,
+      };
+    } catch (error) {
+      logger.error('analyzePublishPageHtml error:', error);
+      return {
+        hasAction: false,
+        explanation: '分析失败',
+      };
+    }
+  }
+
+  // ==================== 工具方法 ====================
+
+  /**
+   * 计算余弦相似度
+   */
+  private cosineSimilarity(vec1: number[], vec2: number[]): number {
+    if (vec1.length !== vec2.length) {
+      throw new Error('Vectors must have the same length');
+    }
+
+    let dotProduct = 0;
+    let norm1 = 0;
+    let norm2 = 0;
+
+    for (let i = 0; i < vec1.length; i++) {
+      dotProduct += vec1[i] * vec2[i];
+      norm1 += vec1[i] * vec1[i];
+      norm2 += vec2[i] * vec2[i];
+    }
+
+    return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
+  }
+
+  /**
+   * 错误处理
+   */
+  private handleError(error: unknown): Error {
+    if (error instanceof OpenAI.APIError) {
+      const status = error.status;
+      const message = error.message;
+
+      switch (status) {
+        case 400:
+          return new Error(`请求参数错误: ${message}`);
+        case 401:
+          return new Error('API Key 无效或已过期');
+        case 403:
+          return new Error('没有权限访问该模型');
+        case 404:
+          return new Error('请求的模型不存在');
+        case 429:
+          return new Error('请求频率超限,请稍后重试');
+        case 500:
+          return new Error('服务器内部错误,请稍后重试');
+        default:
+          return new Error(`API 错误 (${status}): ${message}`);
+      }
+    }
+
+    if (error instanceof Error) {
+      return error;
+    }
+
+    return new Error('未知错误');
+  }
+}
+
+// 导出单例实例
+export const aiService = new QwenAIService();
+
+// 为了向后兼容,也导出别名
+export const AIService = QwenAIService;

+ 90 - 0
server/src/app.ts

@@ -17,6 +17,8 @@ import { logger } from './utils/logger.js';
 import { taskScheduler } from './scheduler/index.js';
 import { registerTaskExecutors } from './services/taskExecutors.js';
 import { taskQueueService } from './services/TaskQueueService.js';
+import { browserLoginService } from './services/BrowserLoginService.js';
+import { wsManager } from './websocket/index.js';
 
 const execAsync = promisify(exec);
 
@@ -227,6 +229,9 @@ async function bootstrap() {
     taskQueueService.startWorker();
   }
 
+  // 注册浏览器登录服务的事件监听(用于 AI 分析结果的 WebSocket 推送)
+  setupBrowserLoginEvents();
+
   // 启动 HTTP 服务
   httpServer.listen(config.port, () => {
     logger.info(`Server running on port ${config.port}`);
@@ -238,6 +243,91 @@ async function bootstrap() {
   });
 }
 
+/**
+ * 设置浏览器登录服务的事件监听
+ * 用于通过 WebSocket 推送 AI 分析结果给前端
+ */
+function setupBrowserLoginEvents(): void {
+  // AI 分析结果事件
+  browserLoginService.on('aiAnalysis', (data: {
+    sessionId: string;
+    userId?: number;
+    status: string;
+    analysis: {
+      isLoggedIn: boolean;
+      hasVerification: boolean;
+      verificationType?: string;
+      verificationDescription?: string;
+      pageDescription: string;
+      suggestedAction?: string;
+    };
+  }) => {
+    if (data.userId) {
+      wsManager.sendToUser(data.userId, 'login:aiAnalysis', {
+        sessionId: data.sessionId,
+        status: data.status,
+        analysis: data.analysis,
+      });
+    }
+  });
+
+  // 验证码检测事件
+  browserLoginService.on('verificationNeeded', (data: {
+    sessionId: string;
+    userId?: number;
+    verificationType?: string;
+    description?: string;
+    suggestedAction?: string;
+  }) => {
+    if (data.userId) {
+      wsManager.sendToUser(data.userId, 'login:verificationNeeded', {
+        sessionId: data.sessionId,
+        verificationType: data.verificationType,
+        description: data.description,
+        suggestedAction: data.suggestedAction,
+      });
+    }
+  });
+
+  // 导航建议事件
+  browserLoginService.on('navigationSuggestion', (data: {
+    sessionId: string;
+    userId?: number;
+    guide: unknown;
+  }) => {
+    if (data.userId) {
+      wsManager.sendToUser(data.userId, 'login:navigationSuggestion', {
+        sessionId: data.sessionId,
+        guide: data.guide,
+      });
+    }
+  });
+
+  // 登录结果事件(也通过 WebSocket 推送)
+  browserLoginService.on('loginResult', (data: {
+    sessionId: string;
+    userId?: number;
+    status: string;
+    cookies?: string;
+    accountInfo?: unknown;
+    error?: string;
+    message?: string;
+  }) => {
+    logger.info(`[BrowserLogin] Login result event: ${data.sessionId}, status: ${data.status}`);
+    if (data.userId) {
+      wsManager.sendToUser(data.userId, 'login:result', {
+        sessionId: data.sessionId,
+        status: data.status,
+        accountInfo: data.accountInfo,
+        error: data.error,
+        message: data.message,
+      });
+    }
+  });
+
+  logger.info('Browser login events registered');
+}
+
 // 优雅关闭
 process.on('SIGTERM', async () => {
   logger.info('SIGTERM received, shutting down gracefully');

+ 471 - 0
server/src/automation/platforms/baijiahao.ts

@@ -0,0 +1,471 @@
+/// <reference lib="dom" />
+import path from 'path';
+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';
+
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
+/**
+ * 百家号平台适配器
+ */
+export class BaijiahaoAdapter extends BasePlatformAdapter {
+  readonly platform: PlatformType = 'baijiahao';
+  readonly loginUrl = 'https://baijiahao.baidu.com/';
+  readonly publishUrl = 'https://baijiahao.baidu.com/builder/rc/edit?type=video';
+  
+  protected getCookieDomain(): string {
+    return '.baidu.com';
+  }
+
+  /**
+   * 检查 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('baijiahao');
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+  
+  async getQRCode(): Promise<QRCodeInfo> {
+    try {
+      await this.initBrowser();
+      
+      if (!this.page) throw new Error('Page not initialized');
+      
+      // 访问百家号登录页面
+      await this.page.goto('https://baijiahao.baidu.com/', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+      
+      // 等待二维码出现
+      await this.page.waitForSelector('img[src*="qrcode"]', { timeout: 15000 });
+      
+      // 获取二维码图片
+      const qrcodeImg = await this.page.$('img[src*="qrcode"]');
+      const qrcodeUrl = await qrcodeImg?.getAttribute('src');
+      
+      if (!qrcodeUrl) {
+        throw new Error('Failed to get QR code');
+      }
+      
+      return {
+        qrcodeUrl,
+        qrcodeKey: `baijiahao_${Date.now()}`,
+        expireTime: Date.now() + 300000,
+      };
+    } catch (error) {
+      logger.error('Baijiahao 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('/builder/rc/home') || currentUrl.includes('/builder/rc/content')) {
+        const cookies = await this.getCookies();
+        await this.closeBrowser();
+        
+        return {
+          status: 'success',
+          message: '登录成功',
+          cookies,
+        };
+      }
+      
+      // 检查是否需要手机验证
+      const phoneInput = await this.page.$('input[type="tel"]');
+      if (phoneInput) {
+        return { status: 'scanned', message: '需要手机验证' };
+      }
+      
+      return { status: 'pending', message: '等待扫码' };
+    } catch (error) {
+      logger.error('Baijiahao checkQRCodeStatus error:', error);
+      return { status: 'expired', 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('https://baijiahao.baidu.com/builder/rc/home', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+      
+      await this.page.waitForTimeout(2000);
+      
+      const currentUrl = this.page.url();
+      const isLoggedIn = currentUrl.includes('/builder/rc/') && !currentUrl.includes('login');
+      
+      await this.closeBrowser();
+      return isLoggedIn;
+    } catch (error) {
+      logger.error('Baijiahao 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');
+      
+      // 访问设置页面获取账号信息
+      await this.page.goto('https://baijiahao.baidu.com/builder/rc/home', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+      
+      await this.page.waitForTimeout(3000);
+      
+      // 尝试从页面获取账号信息
+      const accountInfo = await this.page.evaluate(() => {
+        // 尝试获取用户名
+        const nameEl = document.querySelector('.user-name, .user-info .name, [class*="author-name"]');
+        const name = nameEl?.textContent?.trim() || '';
+        
+        // 尝试获取头像
+        const avatarEl = document.querySelector('.user-avatar img, .author-avatar img, [class*="avatar"] img');
+        const avatar = avatarEl?.getAttribute('src') || '';
+        
+        return { name, avatar };
+      });
+      
+      await this.closeBrowser();
+      
+      return {
+        accountId: `baijiahao_${Date.now()}`,
+        accountName: accountInfo.name || '百家号账号',
+        avatarUrl: accountInfo.avatar || '',
+        fansCount: 0,
+        worksCount: 0,
+      };
+    } catch (error) {
+      logger.error('Baijiahao getAccountInfo error:', error);
+      await this.closeBrowser();
+      return {
+        accountId: '',
+        accountName: '百家号账号',
+        avatarUrl: '',
+        fansCount: 0,
+        worksCount: 0,
+      };
+    }
+  }
+  
+  /**
+   * 通过 Python 服务发布视频(带 AI 辅助)
+   */
+  private async publishVideoViaPython(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Baijiahao Python] Starting publish via Python service with AI assist...');
+    onProgress?.(5, '正在通过 Python 服务发布...');
+
+    try {
+      // 准备 cookie 字符串
+      let cookieStr = cookies;
+      try {
+        const cookieArray = JSON.parse(cookies);
+        if (Array.isArray(cookieArray)) {
+          cookieStr = cookieArray.map((c: { name: string; value: string }) => `${c.name}=${c.value}`).join('; ');
+        }
+      } catch {
+        // 已经是字符串格式
+      }
+
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath)
+        ? params.videoPath
+        : path.resolve(SERVER_ROOT, params.videoPath);
+
+      const absoluteCoverPath = params.coverPath
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          platform: 'baijiahao',
+          cookie: cookieStr,
+          title: params.title,
+          description: params.description || params.title,
+          video_path: absoluteVideoPath,
+          cover_path: absoluteCoverPath,
+          tags: params.tags || [],
+          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+          return_screenshot: true,
+        }),
+        signal: AbortSignal.timeout(600000), // 10分钟超时
+      });
+
+      const result = await response.json();
+      logger.info('[Baijiahao Python] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
+
+      // 使用通用的 AI 辅助处理方法
+      return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
+    } catch (error) {
+      logger.error('[Baijiahao Python] Publish failed:', error);
+      throw error;
+    }
+  }
+
+  async publishVideo(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
+    options?: { headless?: boolean }
+  ): Promise<PublishResult> {
+    // 优先尝试使用 Python 服务
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      logger.info('[Baijiahao] Python service available, using Python method');
+      try {
+        return await this.publishVideoViaPython(cookies, params, onProgress);
+      } catch (pythonError) {
+        logger.warn('[Baijiahao] Python publish failed:', pythonError);
+        // Python 服务可用但发布失败,可能是功能未实现
+        if (pythonError instanceof Error && pythonError.message.includes('暂未实现')) {
+          return {
+            success: false,
+            errorMessage: '百家号发布功能正在开发中,敬请期待',
+          };
+        }
+        onProgress?.(0, 'Python 服务发布失败,正在尝试浏览器模式...');
+      }
+    }
+
+    // 回退到 Playwright 方式
+    const useHeadless = options?.headless ?? true;
+
+    try {
+      await this.initBrowser({ headless: useHeadless });
+      await this.setCookies(cookies);
+
+      if (!this.page) throw new Error('Page not initialized');
+
+      onProgress?.(5, '正在打开发布页面...');
+
+      // 访问发布页面
+      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')) {
+        await this.closeBrowser();
+        return {
+          success: false,
+          errorMessage: '账号登录已过期,请重新登录',
+        };
+      }
+
+      onProgress?.(10, '正在上传视频...');
+
+      // 上传视频
+      const fileInput = this.page.locator('input[type="file"]').first();
+      if (await fileInput.count() > 0) {
+        await fileInput.setInputFiles(params.videoPath);
+      } else {
+        // 尝试其他上传方式
+        const uploadArea = this.page.locator('[class*="upload"], [class*="video-upload"]').first();
+        if (await uploadArea.count() > 0) {
+          const [fileChooser] = await Promise.all([
+            this.page.waitForEvent('filechooser', { timeout: 10000 }),
+            uploadArea.click(),
+          ]);
+          await fileChooser.setFiles(params.videoPath);
+        } else {
+          throw new Error('未找到上传入口');
+        }
+      }
+
+      // 等待上传完成
+      onProgress?.(30, '视频上传中...');
+      await this.page.waitForTimeout(5000);
+
+      // 等待视频处理
+      const maxWaitTime = 300000; // 5分钟
+      const startTime = Date.now();
+
+      while (Date.now() - startTime < maxWaitTime) {
+        // 检查上传进度
+        const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
+        if (progressText) {
+          const match = progressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(30 + Math.floor(progress * 0.3), `视频上传中: ${progress}%`);
+          }
+        }
+
+        // 检查是否上传完成
+        const uploadSuccess = await this.page.locator('[class*="success"], [class*="complete"]').count();
+        if (uploadSuccess > 0) {
+          break;
+        }
+
+        await this.page.waitForTimeout(2000);
+      }
+
+      onProgress?.(60, '正在填写视频信息...');
+
+      // 填写标题
+      const titleInput = this.page.locator('input[placeholder*="标题"], textarea[placeholder*="标题"]').first();
+      if (await titleInput.count() > 0) {
+        await titleInput.fill(params.title);
+      }
+
+      // 填写描述
+      if (params.description) {
+        const descInput = this.page.locator('textarea[placeholder*="简介"], textarea[placeholder*="描述"]').first();
+        if (await descInput.count() > 0) {
+          await descInput.fill(params.description);
+        }
+      }
+
+      onProgress?.(80, '正在发布...');
+
+      // 点击发布按钮
+      const publishBtn = this.page.locator('button:has-text("发布"), [class*="publish-btn"]').first();
+      if (await publishBtn.count() > 0) {
+        await publishBtn.click();
+      } else {
+        throw new Error('未找到发布按钮');
+      }
+
+      // 等待发布结果
+      onProgress?.(90, '等待发布完成...');
+      await this.page.waitForTimeout(5000);
+
+      // AI 辅助检测发布状态
+      const aiStatus = await this.aiAnalyzePublishStatus();
+      if (aiStatus) {
+        if (aiStatus.status === 'success') {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        if (aiStatus.status === 'need_captcha' && onCaptchaRequired) {
+          const imageBase64 = await this.screenshotBase64();
+          try {
+            await onCaptchaRequired({
+              taskId: `baijiahao_captcha_${Date.now()}`,
+              type: 'image',
+              imageBase64,
+            });
+          } catch {
+            logger.error('[Baijiahao] Captcha handling failed');
+          }
+        }
+
+        if (aiStatus.status === 'failed') {
+          throw new Error(aiStatus.errorMessage || '发布失败');
+        }
+      }
+
+      // 检查是否跳转到内容管理页面
+      const finalUrl = this.page.url();
+      if (finalUrl.includes('/content') || finalUrl.includes('/rc/home')) {
+        onProgress?.(100, '发布成功!');
+        await this.closeBrowser();
+        return {
+          success: true,
+          videoUrl: finalUrl,
+        };
+      }
+
+      // 超时
+      await this.closeBrowser();
+      return {
+        success: false,
+        errorMessage: '发布超时,请手动检查是否发布成功',
+      };
+
+    } catch (error) {
+      logger.error('[Baijiahao Publish] Error:', error);
+      await this.closeBrowser();
+      return {
+        success: false,
+        errorMessage: error instanceof Error ? error.message : '发布失败',
+      };
+    }
+  }
+  
+  async getComments(cookies: string, videoId: string): Promise<CommentData[]> {
+    logger.warn('[Baijiahao] getComments not implemented');
+    return [];
+  }
+  
+  async replyComment(cookies: string, commentId: string, content: string): Promise<boolean> {
+    logger.warn('[Baijiahao] replyComment not implemented');
+    return false;
+  }
+  
+  async getAnalytics(cookies: string, dateRange: DateRange): Promise<AnalyticsData> {
+    logger.warn('[Baijiahao] getAnalytics not implemented');
+    return {
+      fansCount: 0,
+      fansIncrease: 0,
+      viewsCount: 0,
+      likesCount: 0,
+      commentsCount: 0,
+      sharesCount: 0,
+    };
+  }
+}

+ 327 - 2
server/src/automation/platforms/base.ts

@@ -7,6 +7,7 @@ import type {
 } from '@media-manager/shared';
 import { BrowserManager } from '../browser.js';
 import { logger } from '../../utils/logger.js';
+import { aiService, type PublishStatusAnalysis, type PageOperationGuide } from '../../ai/index.js';
 
 export interface WorkItem {
   videoId?: string;
@@ -263,8 +264,332 @@ export abstract class BasePlatformAdapter {
     if (!this.page) throw new Error('Page not initialized');
     await this.page.screenshot({ path });
   }
-  
-  // 抽象方法 - 子类必须实现
+
+  /**
+   * 截图并返回 Base64 格式
+   */
+  protected async screenshotBase64(): Promise<string> {
+    if (!this.page) throw new Error('Page not initialized');
+    const buffer = await this.page.screenshot({ type: 'jpeg', quality: 80 });
+    return buffer.toString('base64');
+  }
+
+  /**
+   * 获取页面 HTML
+   */
+  protected async getPageHtml(): Promise<string> {
+    if (!this.page) throw new Error('Page not initialized');
+    return await this.page.content();
+  }
+
+  // ==================== AI 辅助发布方法 ====================
+
+  /**
+   * AI 分析发布页面状态
+   * @returns 发布状态分析结果
+   */
+  protected async aiAnalyzePublishStatus(): Promise<PublishStatusAnalysis | null> {
+    if (!aiService.isAvailable()) {
+      logger.debug('[AI Publish] AI service not available');
+      return null;
+    }
+
+    try {
+      const screenshot = await this.screenshotBase64();
+      const result = await aiService.analyzePublishStatus(screenshot, this.platform);
+      logger.info(`[AI Publish] Status analysis: ${result.status}, confidence: ${result.confidence}%`);
+      return result;
+    } catch (error) {
+      logger.error('[AI Publish] Failed to analyze status:', error);
+      return null;
+    }
+  }
+
+  /**
+   * AI 获取发布操作指导
+   * @param currentStatus 当前状态描述
+   * @returns 操作指导
+   */
+  protected async aiGetPublishOperationGuide(currentStatus: string): Promise<PageOperationGuide | null> {
+    if (!aiService.isAvailable()) {
+      logger.debug('[AI Publish] AI service not available');
+      return null;
+    }
+
+    try {
+      const html = await this.getPageHtml();
+      const result = await aiService.analyzePublishPageHtml(html, this.platform, currentStatus);
+      logger.info(`[AI Publish] Operation guide: hasAction=${result.hasAction}, action=${result.actionType}`);
+      return result;
+    } catch (error) {
+      logger.error('[AI Publish] Failed to get operation guide:', error);
+      return null;
+    }
+  }
+
+  /**
+   * AI 辅助执行操作
+   * @param guide 操作指导
+   * @returns 是否执行成功
+   */
+  protected async aiExecuteOperation(guide: PageOperationGuide): Promise<boolean> {
+    if (!this.page || !guide.hasAction) return false;
+
+    try {
+      switch (guide.actionType) {
+        case 'click':
+          if (guide.targetSelector) {
+            logger.info(`[AI Publish] Clicking: ${guide.targetSelector}`);
+            await this.page.click(guide.targetSelector, { timeout: 10000 });
+            return true;
+          }
+          break;
+
+        case 'input':
+          if (guide.targetSelector && guide.inputText) {
+            logger.info(`[AI Publish] Inputting to: ${guide.targetSelector}`);
+            await this.page.fill(guide.targetSelector, guide.inputText);
+            return true;
+          }
+          break;
+
+        case 'wait':
+          logger.info('[AI Publish] Waiting...');
+          await this.page.waitForTimeout(3000);
+          return true;
+
+        case 'scroll':
+          logger.info('[AI Publish] Scrolling...');
+          await this.page.evaluate(() => window.scrollBy(0, 300));
+          return true;
+      }
+    } catch (error) {
+      logger.error(`[AI Publish] Failed to execute operation:`, error);
+    }
+
+    return false;
+  }
+
+  /**
+   * AI 辅助发布监控循环
+   * 监控发布过程,检测验证码和发布结果
+   * @param options 配置选项
+   * @returns 发布结果
+   */
+  protected async aiAssistedPublishMonitor(options: {
+    maxAttempts?: number;
+    checkInterval?: number;
+    onCaptchaRequired?: (captchaInfo: {
+      taskId: string;
+      type: 'sms' | 'image';
+      captchaDescription?: string;
+      imageBase64?: string;
+    }) => Promise<string>;
+    onProgress?: (progress: number, message: string) => void;
+  }): Promise<{ success: boolean; message: string; needManualIntervention?: boolean }> {
+    const maxAttempts = options.maxAttempts || 30;
+    const checkInterval = options.checkInterval || 3000;
+
+    for (let attempt = 0; attempt < maxAttempts; attempt++) {
+      // 等待页面稳定
+      await this.page?.waitForTimeout(checkInterval);
+
+      // AI 分析当前页面状态
+      const status = await this.aiAnalyzePublishStatus();
+      if (!status) {
+        logger.warn('[AI Publish] AI analysis unavailable, continuing...');
+        continue;
+      }
+
+      logger.info(`[AI Publish] Attempt ${attempt + 1}/${maxAttempts}: status=${status.status}`);
+
+      switch (status.status) {
+        case 'success':
+          options.onProgress?.(100, '发布成功');
+          return { success: true, message: status.pageDescription };
+
+        case 'failed':
+          options.onProgress?.(0, status.errorMessage || '发布失败');
+          return { success: false, message: status.errorMessage || '发布失败' };
+
+        case 'need_captcha':
+          if (options.onCaptchaRequired) {
+            options.onProgress?.(50, `检测到验证码: ${status.captchaDescription || '请输入验证码'}`);
+
+            // 如果是图片验证码,截图发送
+            let imageBase64: string | undefined;
+            if (status.captchaType === 'image' || status.captchaType === 'slider') {
+              imageBase64 = await this.screenshotBase64();
+            }
+
+            try {
+              const captchaCode = await options.onCaptchaRequired({
+                taskId: `captcha_${Date.now()}`,
+                type: status.captchaType === 'sms' ? 'sms' : 'image',
+                captchaDescription: status.captchaDescription,
+                imageBase64,
+              });
+
+              // 用户输入了验证码,尝试 AI 指导输入
+              if (captchaCode && status.nextAction?.targetSelector) {
+                await this.page?.fill(status.nextAction.targetSelector, captchaCode);
+                await this.page?.waitForTimeout(500);
+
+                // 尝试点击确认按钮
+                const guide = await this.aiGetPublishOperationGuide('已输入验证码,需要点击确认');
+                if (guide?.hasAction && guide.actionType === 'click' && guide.targetSelector) {
+                  await this.page?.click(guide.targetSelector);
+                }
+              }
+            } catch (captchaError) {
+              logger.error('[AI Publish] Captcha handling failed:', captchaError);
+              return { success: false, message: '验证码处理失败', needManualIntervention: true };
+            }
+          } else {
+            // 没有验证码处理回调,需要人工介入
+            logger.warn('[AI Publish] Captcha required but no handler provided');
+            return { success: false, message: '需要验证码', needManualIntervention: true };
+          }
+          break;
+
+        case 'need_action':
+          if (status.nextAction) {
+            options.onProgress?.(Math.min(80, 50 + attempt * 2), status.pageDescription);
+
+            // 尝试执行 AI 建议的操作
+            const guide = await this.aiGetPublishOperationGuide(status.pageDescription);
+            if (guide?.hasAction) {
+              await this.aiExecuteOperation(guide);
+            }
+          }
+          break;
+
+        case 'uploading':
+        case 'processing':
+          options.onProgress?.(Math.min(90, 30 + attempt * 2), status.pageDescription);
+          // 继续等待
+          break;
+      }
+    }
+
+    // 超过最大尝试次数
+    logger.warn('[AI Publish] Max attempts reached');
+    return { success: false, message: '发布超时,请检查发布状态' };
+  }
+
+  /**
+   * AI 辅助处理 Python API 发布结果
+   * 当 Python 返回截图时,使用 AI 分析发布状态
+   * @param result Python API 返回的结果
+   * @param onCaptchaRequired 验证码回调
+   * @param onProgress 进度回调
+   * @returns 处理后的发布结果
+   */
+  protected async aiProcessPythonPublishResult(
+    result: {
+      success?: boolean;
+      screenshot_base64?: string;
+      page_url?: string;
+      video_id?: string;
+      video_url?: string;
+      need_captcha?: boolean;
+      captcha_type?: string;
+      status?: string;
+      error?: string;
+    },
+    onCaptchaRequired?: (captchaInfo: {
+      taskId: string;
+      type: 'sms' | 'image';
+      phone?: string;
+      imageBase64?: string;
+    }) => Promise<string>,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult & { needCaptcha?: boolean; captchaType?: string }> {
+    // 如果 Python 返回成功
+    if (result.success) {
+      onProgress?.(100, '发布成功');
+      return {
+        success: true,
+        platformVideoId: result.video_id || `${this.platform}_${Date.now()}`,
+        videoUrl: result.video_url || '',
+      };
+    }
+
+    // 如果返回了截图,使用 AI 分析
+    if (result.screenshot_base64 && aiService.isAvailable()) {
+      logger.info(`[${this.platform} Python] Got screenshot, analyzing with AI...`);
+      
+      const aiStatus = await aiService.analyzePublishStatus(result.screenshot_base64, this.platform);
+      logger.info(`[${this.platform} Python] AI analysis: status=${aiStatus.status}, confidence=${aiStatus.confidence}%`);
+
+      // AI 判断发布成功
+      if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+        onProgress?.(100, '发布成功');
+        return {
+          success: true,
+          platformVideoId: result.video_id || `${this.platform}_${Date.now()}`,
+          videoUrl: result.video_url || result.page_url || '',
+        };
+      }
+
+      // AI 检测到需要验证码
+      if (aiStatus.status === 'need_captcha') {
+        logger.info(`[${this.platform} Python] AI detected captcha: ${aiStatus.captchaDescription}`);
+        
+        if (onCaptchaRequired) {
+          onProgress?.(50, `AI 检测到验证码: ${aiStatus.captchaDescription || '请输入验证码'}`);
+          
+          try {
+            const captchaCode = await onCaptchaRequired({
+              taskId: `${this.platform}_captcha_${Date.now()}`,
+              type: aiStatus.captchaType === 'sms' ? 'sms' : 'image',
+              imageBase64: result.screenshot_base64,
+            });
+            
+            // 验证码已获取,但 Python 发布已结束,需要通过 Playwright 继续
+            logger.info(`[${this.platform} Python] Got captcha code, need to continue with Playwright`);
+          } catch {
+            logger.error(`[${this.platform} Python] Captcha handling failed`);
+          }
+        }
+
+        return {
+          success: false,
+          needCaptcha: true,
+          captchaType: aiStatus.captchaType || 'image',
+          errorMessage: aiStatus.captchaDescription || '需要验证码',
+        };
+      }
+
+      // AI 判断发布失败
+      if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+        return {
+          success: false,
+          errorMessage: aiStatus.errorMessage || 'AI 检测到发布失败',
+        };
+      }
+    }
+
+    // Python 返回需要验证码
+    if (result.need_captcha || result.status === 'need_captcha') {
+      logger.info(`[${this.platform} Python] Captcha required: type=${result.captcha_type}`);
+      onProgress?.(0, `检测到需要验证码,切换到浏览器模式...`);
+      return {
+        success: false,
+        needCaptcha: true,
+        captchaType: result.captcha_type || 'image',
+        errorMessage: result.error || '需要验证码',
+      };
+    }
+
+    // 其他失败情况
+    return {
+      success: false,
+      errorMessage: result.error || '发布失败',
+    };
+  }
+
+  // ==================== 抽象方法 - 子类必须实现 ====================
   
   /**
    * 获取扫码登录二维码

+ 290 - 23
server/src/automation/platforms/bilibili.ts

@@ -1,3 +1,5 @@
+/// <reference lib="dom" />
+import path from 'path';
 import { BasePlatformAdapter } from './base.js';
 import type {
   AccountProfile,
@@ -10,6 +12,12 @@ 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';
+
+// 服务器根目录(用于构造绝对路径)
+const SERVER_ROOT = path.resolve(process.cwd());
+
 /**
  * B站平台适配器
  */
@@ -120,37 +128,296 @@ export class BilibiliAdapter extends BasePlatformAdapter {
     }
   }
   
-  async publishVideo(cookies: string, params: PublishParams): Promise<PublishResult> {
+  /**
+   * 检查 Python 发布服务是否可用
+   */
+  private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      await this.initBrowser();
+      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('bilibili');
+      }
+      return false;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 通过 Python 服务发布视频(带 AI 辅助)
+   */
+  private async publishVideoViaPython(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void
+  ): Promise<PublishResult> {
+    logger.info('[Bilibili Python] Starting publish via Python service with AI assist...');
+    onProgress?.(5, '正在通过 Python 服务发布...');
+
+    try {
+      // 准备 cookie 字符串
+      let cookieStr = cookies;
+      try {
+        const cookieArray = JSON.parse(cookies);
+        if (Array.isArray(cookieArray)) {
+          cookieStr = cookieArray.map((c: { name: string; value: string }) => `${c.name}=${c.value}`).join('; ');
+        }
+      } catch {
+        // 已经是字符串格式
+      }
+
+      // 将相对路径转换为绝对路径
+      const absoluteVideoPath = path.isAbsolute(params.videoPath)
+        ? params.videoPath
+        : path.resolve(SERVER_ROOT, params.videoPath);
+
+      const absoluteCoverPath = params.coverPath
+        ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
+        : undefined;
+
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          platform: 'bilibili',
+          cookie: cookieStr,
+          title: params.title,
+          description: params.description || params.title,
+          video_path: absoluteVideoPath,
+          cover_path: absoluteCoverPath,
+          tags: params.tags || [],
+          post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
+          return_screenshot: true,
+        }),
+        signal: AbortSignal.timeout(600000), // 10分钟超时
+      });
+
+      const result = await response.json();
+      logger.info('[Bilibili Python] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
+
+      // 使用通用的 AI 辅助处理方法
+      return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
+    } catch (error) {
+      logger.error('[Bilibili Python] Publish failed:', error);
+      throw error;
+    }
+  }
+
+  async publishVideo(
+    cookies: string,
+    params: PublishParams,
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
+    options?: { headless?: boolean }
+  ): Promise<PublishResult> {
+    // 优先尝试使用 Python 服务
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      logger.info('[Bilibili] Python service available, using Python method');
+      try {
+        return await this.publishVideoViaPython(cookies, params, onProgress);
+      } catch (pythonError) {
+        logger.warn('[Bilibili] Python publish failed, falling back to Playwright:', pythonError);
+        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
+      }
+    } else {
+      logger.info('[Bilibili] Python service not available, using Playwright method');
+    }
+
+    // 回退到 Playwright 方式
+    const useHeadless = options?.headless ?? true;
+
+    try {
+      await this.initBrowser({ headless: useHeadless });
       await this.setCookies(cookies);
-      
+
       if (!this.page) throw new Error('Page not initialized');
-      
-      await this.page.goto(this.publishUrl);
-      await this.page.waitForLoadState('networkidle');
-      
+
+      onProgress?.(5, '正在打开上传页面...');
+
+      await this.page.goto(this.publishUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 60000,
+      });
+
+      await this.page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = this.page.url();
+      if (currentUrl.includes('passport') || currentUrl.includes('login')) {
+        await this.closeBrowser();
+        return {
+          success: false,
+          errorMessage: '账号登录已过期,请重新登录',
+        };
+      }
+
+      onProgress?.(10, '正在上传视频...');
+
       // B站上传逻辑
       const fileInput = await this.page.$('input[type="file"]');
-      if (!fileInput) throw new Error('File input not found');
-      
+      if (!fileInput) throw new Error('未找到文件上传入口');
+
       await fileInput.setInputFiles(params.videoPath);
-      await this.page.waitForSelector('.upload-success', { timeout: 300000 });
-      
+
+      // 等待上传完成
+      onProgress?.(20, '视频上传中...');
+      const maxWaitTime = 300000; // 5分钟
+      const startTime = Date.now();
+
+      while (Date.now() - startTime < maxWaitTime) {
+        // 检查上传进度
+        const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
+        if (progressText) {
+          const match = progressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(20 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
+          }
+        }
+
+        // 检查上传完成
+        const uploadSuccess = await this.page.locator('.upload-success, [class*="success"], [class*="complete"]').count();
+        if (uploadSuccess > 0) {
+          logger.info('[Bilibili Publish] Video upload completed');
+          break;
+        }
+
+        await this.page.waitForTimeout(2000);
+      }
+
+      onProgress?.(60, '正在填写视频信息...');
+
       // 填写标题
-      await this.type('.video-title input', params.title);
-      
-      // 选择分区(默认生活区)
-      await this.click('.type-btn');
-      await this.click('.type-item[data-tid="160"]');
-      
+      const titleInput = this.page.locator('.video-title input, input[placeholder*="标题"]').first();
+      if (await titleInput.count() > 0) {
+        await titleInput.fill(params.title);
+      }
+
+      // 填写简介
+      if (params.description) {
+        const descInput = this.page.locator('textarea[placeholder*="简介"], .desc-input textarea').first();
+        if (await descInput.count() > 0) {
+          await descInput.fill(params.description);
+        }
+      }
+
+      // 选择分区
+      try {
+        const typeBtn = this.page.locator('.type-btn, [class*="select-type"]').first();
+        if (await typeBtn.count() > 0) {
+          await typeBtn.click();
+          await this.page.waitForTimeout(500);
+          // 默认选择生活区
+          const lifeCategory = this.page.locator('[data-tid="160"], :has-text("生活")').first();
+          if (await lifeCategory.count() > 0) {
+            await lifeCategory.click();
+          }
+        }
+      } catch {
+        logger.warn('[Bilibili Publish] Failed to select category');
+      }
+
+      onProgress?.(80, '正在发布...');
+
       // 点击发布
-      await this.click('.submit-add');
-      await this.page.waitForSelector('.success-hint', { timeout: 60000 });
-      
-      await this.closeBrowser();
-      
-      return { success: true };
+      const submitBtn = this.page.locator('.submit-add, button:has-text("投稿"), [class*="submit"]').first();
+      if (await submitBtn.count() > 0) {
+        await submitBtn.click();
+      } else {
+        throw new Error('未找到发布按钮');
+      }
+
+      // 等待发布结果
+      onProgress?.(90, '等待发布完成...');
+      const publishMaxWait = 120000; // 2分钟
+      const publishStartTime = Date.now();
+      let aiCheckCounter = 0;
+
+      while (Date.now() - publishStartTime < publishMaxWait) {
+        await this.page.waitForTimeout(3000);
+
+        // 检查成功提示
+        const successHint = await this.page.locator('.success-hint, [class*="success"]:has-text("成功")').count();
+        if (successHint > 0) {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        // 检查错误提示
+        const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
+          throw new Error(`发布失败: ${errorHint}`);
+        }
+
+        // AI 辅助检测(每 3 次循环)
+        aiCheckCounter++;
+        if (aiCheckCounter >= 3) {
+          aiCheckCounter = 0;
+          const aiStatus = await this.aiAnalyzePublishStatus();
+          if (aiStatus) {
+            logger.info(`[Bilibili Publish] AI status: ${aiStatus.status}, confidence: ${aiStatus.confidence}%`);
+
+            if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+
+            if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+              throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+            }
+
+            if (aiStatus.status === 'need_captcha' && onCaptchaRequired) {
+              const imageBase64 = await this.screenshotBase64();
+              try {
+                const captchaCode = await onCaptchaRequired({
+                  taskId: `bilibili_captcha_${Date.now()}`,
+                  type: 'image',
+                  imageBase64,
+                });
+                if (captchaCode) {
+                  const guide = await this.aiGetPublishOperationGuide('需要输入验证码');
+                  if (guide?.hasAction && guide.targetSelector) {
+                    await this.page.fill(guide.targetSelector, captchaCode);
+                  }
+                }
+              } catch {
+                logger.error('[Bilibili Publish] Captcha handling failed');
+              }
+            }
+
+            if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
+              const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
+              if (guide?.hasAction) {
+                await this.aiExecuteOperation(guide);
+              }
+            }
+          }
+        }
+      }
+
+      // 超时,AI 最终检查
+      const finalAiStatus = await this.aiAnalyzePublishStatus();
+      if (finalAiStatus?.status === 'success') {
+        onProgress?.(100, '发布成功!');
+        await this.closeBrowser();
+        return { success: true, videoUrl: this.page.url() };
+      }
+
+      throw new Error('发布超时,请手动检查是否发布成功');
+
     } catch (error) {
       logger.error('Bilibili publishVideo error:', error);
       await this.closeBrowser();

+ 279 - 13
server/src/automation/platforms/douyin.ts

@@ -76,6 +76,18 @@ export class DouyinAdapter extends BasePlatformAdapter {
       const currentUrl = this.page.url();
 
       if (currentUrl.includes('/creator-micro/home')) {
+        // 在判断登录成功之前,先检查是否有二次校验弹框
+        // 抖音二次校验弹框特征:标题"身份验证"、选项包含"接收短信验证码"等
+        const hasSecondaryVerification = await this.checkSecondaryVerification();
+        
+        if (hasSecondaryVerification) {
+          logger.info('[Douyin] Secondary verification detected, waiting for user to complete');
+          return { 
+            status: 'scanned', 
+            message: '需要二次校验,请在手机上完成身份验证' 
+          };
+        }
+
         // 登录成功,获取 cookie
         const cookies = await this.getCookies();
         await this.closeBrowser();
@@ -87,6 +99,16 @@ export class DouyinAdapter extends BasePlatformAdapter {
         };
       }
 
+      // 检查是否有二次校验弹框(扫码后可能直接弹出,URL 还没变化)
+      const hasSecondaryVerification = await this.checkSecondaryVerification();
+      if (hasSecondaryVerification) {
+        logger.info('[Douyin] Secondary verification detected after scan');
+        return { 
+          status: 'scanned', 
+          message: '需要二次校验,请在手机上完成身份验证' 
+        };
+      }
+
       // 检查是否扫码
       const scanTip = await this.page.$('.scan-tip');
       if (scanTip) {
@@ -101,6 +123,94 @@ export class DouyinAdapter extends BasePlatformAdapter {
   }
 
   /**
+   * 检查是否存在二次校验弹框
+   * 抖音登录时可能弹出身份验证弹框,要求短信验证码或刷脸验证
+   */
+  private async checkSecondaryVerification(): Promise<boolean> {
+    if (!this.page) return false;
+
+    try {
+      // 检测多种可能的二次校验弹框
+      // 1. 身份验证弹框(标题"身份验证")
+      const verifyTitle = this.page.getByText('身份验证', { exact: false });
+      const titleCount = await verifyTitle.count().catch(() => 0);
+      if (titleCount > 0) {
+        const isVisible = await verifyTitle.first().isVisible().catch(() => false);
+        if (isVisible) {
+          logger.info('[Douyin] Found "身份验证" dialog');
+          return true;
+        }
+      }
+
+      // 2. 检查是否有"接收短信验证码"选项
+      const smsOption = this.page.getByText('接收短信验证码', { exact: false });
+      const smsCount = await smsOption.count().catch(() => 0);
+      if (smsCount > 0) {
+        const isVisible = await smsOption.first().isVisible().catch(() => false);
+        if (isVisible) {
+          logger.info('[Douyin] Found "接收短信验证码" option');
+          return true;
+        }
+      }
+
+      // 3. 检查是否有"手机刷脸验证"选项
+      const faceOption = this.page.getByText('手机刷脸验证', { exact: false });
+      const faceCount = await faceOption.count().catch(() => 0);
+      if (faceCount > 0) {
+        const isVisible = await faceOption.first().isVisible().catch(() => false);
+        if (isVisible) {
+          logger.info('[Douyin] Found "手机刷脸验证" option');
+          return true;
+        }
+      }
+
+      // 4. 检查是否有"发送短信验证"选项
+      const sendSmsOption = this.page.getByText('发送短信验证', { exact: false });
+      const sendSmsCount = await sendSmsOption.count().catch(() => 0);
+      if (sendSmsCount > 0) {
+        const isVisible = await sendSmsOption.first().isVisible().catch(() => false);
+        if (isVisible) {
+          logger.info('[Douyin] Found "发送短信验证" option');
+          return true;
+        }
+      }
+
+      // 5. 检查页面内容中是否包含二次校验关键文本
+      const pageContent = await this.page.content().catch(() => '');
+      if (pageContent.includes('为保障账号安全') && 
+          (pageContent.includes('身份验证') || pageContent.includes('完成身份验证'))) {
+        logger.info('[Douyin] Found secondary verification text in page content');
+        return true;
+      }
+
+      // 6. 检查是否有验证相关的弹框容器
+      const verifySelectors = [
+        '[class*="verify-modal"]',
+        '[class*="identity-verify"]',
+        '[class*="second-verify"]',
+        '[class*="security-verify"]',
+      ];
+
+      for (const selector of verifySelectors) {
+        const element = this.page.locator(selector).first();
+        const count = await element.count().catch(() => 0);
+        if (count > 0) {
+          const isVisible = await element.isVisible().catch(() => false);
+          if (isVisible) {
+            logger.info(`[Douyin] Found verification modal via selector: ${selector}`);
+            return true;
+          }
+        }
+      }
+
+      return false;
+    } catch (error) {
+      logger.error('[Douyin] Error checking secondary verification:', error);
+      return false;
+    }
+  }
+
+  /**
    * 检查登录状态
    */
   async checkLoginStatus(cookies: string): Promise<boolean> {
@@ -353,6 +463,7 @@ export class DouyinAdapter extends BasePlatformAdapter {
 
   /**
    * 处理验证码弹框(支持短信验证码和图形验证码)
+   * 优先使用传统方式检测,AI 作为辅助
    * @param onCaptchaRequired 验证码回调
    * @returns 'success' | 'failed' | 'not_needed'
    */
@@ -367,7 +478,7 @@ export class DouyinAdapter extends BasePlatformAdapter {
     if (!this.page) return 'not_needed';
 
     try {
-      // 1. 先检测图形验证码弹框("请完成身份验证后继续")
+      // 1. 先使用传统方式检测图形验证码弹框
       logger.info('[Douyin Publish] Checking for captcha...');
       const imageCaptchaResult = await this.handleImageCaptcha(onCaptchaRequired);
       if (imageCaptchaResult !== 'not_needed') {
@@ -379,8 +490,62 @@ export class DouyinAdapter extends BasePlatformAdapter {
       const smsCaptchaResult = await this.handleSmsCaptcha(onCaptchaRequired);
       if (smsCaptchaResult !== 'not_needed') {
         logger.info(`[Douyin Publish] SMS captcha result: ${smsCaptchaResult}`);
+        return smsCaptchaResult;
+      }
+
+      // 3. 如果传统方式没检测到,使用 AI 辅助检测
+      const aiStatus = await this.aiAnalyzePublishStatus();
+      if (aiStatus?.status === 'need_captcha') {
+        logger.info(`[Douyin Publish] AI detected captcha: type=${aiStatus.captchaType}, desc=${aiStatus.captchaDescription}`);
+
+        // AI 检测到验证码,尝试处理
+        if (onCaptchaRequired && aiStatus.captchaType) {
+          // 获取验证码截图
+          const imageBase64 = await this.screenshotBase64();
+
+          try {
+            const captchaCode = await onCaptchaRequired({
+              taskId: `ai_captcha_${Date.now()}`,
+              type: aiStatus.captchaType === 'sms' ? 'sms' : 'image',
+              captchaDescription: aiStatus.captchaDescription,
+              imageBase64,
+            });
+
+            if (captchaCode) {
+              // 使用 AI 指导输入验证码
+              const guide = await this.aiGetPublishOperationGuide('需要输入验证码');
+              if (guide?.hasAction && guide.targetSelector) {
+                await this.page.fill(guide.targetSelector, captchaCode);
+                await this.page.waitForTimeout(500);
+
+                // 查找确认按钮
+                const confirmGuide = await this.aiGetPublishOperationGuide('已输入验证码,需要点击确认按钮');
+                if (confirmGuide?.hasAction && confirmGuide.targetSelector) {
+                  await this.page.click(confirmGuide.targetSelector);
+                  await this.page.waitForTimeout(2000);
+                }
+
+                // 验证是否成功
+                const afterStatus = await this.aiAnalyzePublishStatus();
+                if (afterStatus?.status === 'need_captcha') {
+                  logger.warn('[Douyin Publish] AI: Captcha still present after input');
+                  return 'failed';
+                }
+                return 'success';
+              }
+            }
+          } catch (captchaError) {
+            logger.error('[Douyin Publish] AI captcha handling failed:', captchaError);
+          }
+        }
+
+        // 没有回调或处理失败,需要手动介入
+        if (this.isHeadless) {
+          return 'need_retry_headful';
+        }
       }
-      return smsCaptchaResult;
+
+      return 'not_needed';
 
     } catch (error) {
       logger.error('[Douyin Publish] Captcha handling error:', error);
@@ -724,15 +889,16 @@ export class DouyinAdapter extends BasePlatformAdapter {
   }
 
   /**
-   * 通过 Python 服务发布视频(参考 matrix 项目
+   * 通过 Python 服务发布视频(带 AI 辅助
    * @returns PublishResult - 如果需要验证码,返回 { success: false, needCaptcha: true, captchaType: '...' }
    */
   private async publishVideoViaPython(
     cookies: string,
     params: PublishParams,
-    onProgress?: (progress: number, message: string) => void
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>
   ): Promise<PublishResult & { needCaptcha?: boolean; captchaType?: string }> {
-    logger.info('[Douyin Python] Starting publish via Python service...');
+    logger.info('[Douyin Python] Starting publish via Python service with AI assist...');
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
@@ -745,7 +911,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
         ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
         : undefined;
 
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -771,20 +938,63 @@ export class DouyinAdapter extends BasePlatformAdapter {
         logger.info('[Douyin Python] Publish successful');
         return {
           success: true,
-          platformVideoId: `douyin_${Date.now()}`,
+          platformVideoId: result.video_id || `douyin_${Date.now()}`,
           videoUrl: result.video_url || '',
         };
       }
 
-      // 检查是否需要验证码
-      if (result.need_captcha) {
+      // 如果返回了截图,使用 AI 分析
+      if (result.screenshot_base64) {
+        logger.info('[Douyin Python] Got screenshot, analyzing with AI...');
+        const { aiService } = await import('../../ai/index.js');
+        
+        if (aiService.isAvailable()) {
+          const aiStatus = await aiService.analyzePublishStatus(result.screenshot_base64, 'douyin');
+          logger.info(`[Douyin Python] AI analysis: status=${aiStatus.status}, confidence=${aiStatus.confidence}%`);
+
+          // AI 判断发布成功
+          if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+            onProgress?.(100, '发布成功');
+            return {
+              success: true,
+              platformVideoId: `douyin_${Date.now()}`,
+              videoUrl: result.video_url || result.page_url || '',
+            };
+          }
+
+          // AI 检测到需要验证码
+          if (aiStatus.status === 'need_captcha') {
+            logger.info(`[Douyin Python] AI detected captcha: ${aiStatus.captchaDescription}`);
+            
+            // 如果有验证码回调,尝试处理
+            if (onCaptchaRequired) {
+              onProgress?.(50, `AI 检测到验证码: ${aiStatus.captchaDescription || '请输入验证码'}`);
+              // 返回需要验证码的状态,让上层处理
+              return {
+                success: false,
+                needCaptcha: true,
+                captchaType: aiStatus.captchaType || 'image',
+                errorMessage: aiStatus.captchaDescription || '需要验证码',
+              };
+            }
+          }
+
+          // AI 判断发布失败
+          if (aiStatus.status === 'failed') {
+            throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+          }
+        }
+      }
+
+      // Python 返回需要验证码
+      if (result.need_captcha || result.status === 'need_captcha') {
         logger.info(`[Douyin Python] Captcha required: type=${result.captcha_type}`);
-        onProgress?.(0, `检测到需要${result.captcha_type}验证码,切换到浏览器模式...`);
+        onProgress?.(0, `检测到需要${result.captcha_type || ''}验证码,切换到浏览器模式...`);
         return {
           success: false,
           needCaptcha: true,
-          captchaType: result.captcha_type,
-          errorMessage: result.error || `需要${result.captcha_type}验证码`,
+          captchaType: result.captcha_type || 'image',
+          errorMessage: result.error || `需要验证码`,
         };
       }
 
@@ -1222,6 +1432,9 @@ export class DouyinAdapter extends BasePlatformAdapter {
       const publishPageUrl = this.page.url();
       logger.info(`[Douyin Publish] Publish page URL: ${publishPageUrl}`);
 
+      // AI 检测计数器,避免过于频繁
+      let aiCheckCounter = 0;
+
       while (Date.now() - publishStartTime < publishMaxWait) {
         await this.page.waitForTimeout(3000);
         const currentUrl = this.page.url();
@@ -1283,14 +1496,67 @@ export class DouyinAdapter extends BasePlatformAdapter {
           throw new Error(`发布失败: ${errorToast}`);
         }
 
+        // 每隔几次循环使用 AI 辅助检测发布状态
+        aiCheckCounter++;
+        if (aiCheckCounter >= 3) {
+          aiCheckCounter = 0;
+          const aiStatus = await this.aiAnalyzePublishStatus();
+          if (aiStatus) {
+            logger.info(`[Douyin Publish] AI status: ${aiStatus.status}, confidence: ${aiStatus.confidence}%`);
+
+            if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+              logger.info('[Douyin Publish] AI detected publish success');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return {
+                success: true,
+                videoUrl: currentUrl,
+              };
+            }
+
+            if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+              logger.error(`[Douyin Publish] AI detected failure: ${aiStatus.errorMessage}`);
+              throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+            }
+
+            // AI 建议需要操作
+            if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
+              logger.info(`[Douyin Publish] AI suggests action: ${aiStatus.nextAction.targetDescription}`);
+              const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
+              if (guide?.hasAction) {
+                await this.aiExecuteOperation(guide);
+              }
+            }
+          }
+        }
+
         // 更新进度
         onProgress?.(90 + Math.min(9, Math.floor(elapsed / 20)), `等待发布完成 (${elapsed}s)...`);
       }
 
-      // 如果超时,最后检查一次当前页面状态
+      // 如果超时,使用 AI 做最后一次状态检查
       const finalUrl = this.page.url();
       logger.info(`[Douyin Publish] Timeout! Final URL: ${finalUrl}`);
 
+      // AI 最终检查
+      const finalAiStatus = await this.aiAnalyzePublishStatus();
+      if (finalAiStatus) {
+        logger.info(`[Douyin Publish] Final AI status: ${finalAiStatus.status}`);
+
+        if (finalAiStatus.status === 'success') {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: finalUrl,
+          };
+        }
+
+        if (finalAiStatus.status === 'failed') {
+          throw new Error(finalAiStatus.errorMessage || 'AI 检测到发布失败');
+        }
+      }
+
       if (finalUrl.includes('/content/manage')) {
         onProgress?.(100, '发布成功!');
         await this.closeBrowser();

+ 178 - 43
server/src/automation/platforms/kuaishou.ts

@@ -147,27 +147,28 @@ export class KuaishouAdapter extends BasePlatformAdapter {
   }
 
   /**
-   * 通过 Python 服务发布视频(参考 matrix 项目
+   * 通过 Python 服务发布视频(带 AI 辅助
    */
   private async publishVideoViaPython(
     cookies: string,
     params: PublishParams,
     onProgress?: (progress: number, message: string) => void
   ): Promise<PublishResult> {
-    logger.info('[Kuaishou Python] Starting publish via Python service...');
+    logger.info('[Kuaishou Python] Starting publish via Python service with AI assist...');
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
       // 将相对路径转换为绝对路径
-      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
-        ? params.videoPath 
+      const absoluteVideoPath = path.isAbsolute(params.videoPath)
+        ? params.videoPath
         : path.resolve(SERVER_ROOT, params.videoPath);
-      
-      const absoluteCoverPath = params.coverPath 
+
+      const absoluteCoverPath = params.coverPath
         ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
         : undefined;
 
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -182,24 +183,16 @@ export class KuaishouAdapter extends BasePlatformAdapter {
           tags: params.tags || [],
           post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
           location: params.location || '重庆市',
+          return_screenshot: true,
         }),
         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 || '发布失败');
-      }
+      logger.info('[Kuaishou Python] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
+
+      // 使用通用的 AI 辅助处理方法
+      return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
     } catch (error) {
       logger.error('[Kuaishou Python] Publish failed:', error);
       throw error;
@@ -207,9 +200,11 @@ export class KuaishouAdapter extends BasePlatformAdapter {
   }
 
   async publishVideo(
-    cookies: string, 
+    cookies: string,
     params: PublishParams,
-    onProgress?: (progress: number, message: string) => void
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
+    options?: { headless?: boolean }
   ): Promise<PublishResult> {
     // 优先尝试使用 Python 服务
     const pythonAvailable = await this.checkPythonServiceAvailable();
@@ -219,40 +214,180 @@ export class KuaishouAdapter extends BasePlatformAdapter {
         return await this.publishVideoViaPython(cookies, params, onProgress);
       } catch (pythonError) {
         logger.warn('[Kuaishou] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
+        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
       }
     } else {
       logger.info('[Kuaishou] Python service not available, using Playwright method');
     }
 
     // 回退到 Playwright 方式
+    const useHeadless = options?.headless ?? true;
+
     try {
-      await this.initBrowser();
+      await this.initBrowser({ headless: useHeadless });
       await this.setCookies(cookies);
-      
+
       if (!this.page) throw new Error('Page not initialized');
-      
-      await this.page.goto(this.publishUrl);
-      await this.page.waitForLoadState('networkidle');
-      
+
+      onProgress?.(5, '正在打开上传页面...');
+
+      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')) {
+        await this.closeBrowser();
+        return {
+          success: false,
+          errorMessage: '账号登录已过期,请重新登录',
+        };
+      }
+
+      onProgress?.(10, '正在上传视频...');
+
       const fileInput = await this.page.$('input[type="file"]');
-      if (!fileInput) throw new Error('File input not found');
-      
+      if (!fileInput) throw new Error('未找到文件上传入口');
+
       await fileInput.setInputFiles(params.videoPath);
-      await this.page.waitForSelector('.upload-success', { timeout: 300000 });
-      
-      await this.type('.title-input', params.title);
-      
+
+      // 等待上传完成
+      onProgress?.(20, '视频上传中...');
+      const maxWaitTime = 300000; // 5分钟
+      const startTime = Date.now();
+
+      while (Date.now() - startTime < maxWaitTime) {
+        const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
+        if (progressText) {
+          const match = progressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(20 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
+          }
+        }
+
+        const uploadSuccess = await this.page.locator('.upload-success, [class*="success"], [class*="complete"]').count();
+        if (uploadSuccess > 0) {
+          logger.info('[Kuaishou Publish] Video upload completed');
+          break;
+        }
+
+        await this.page.waitForTimeout(2000);
+      }
+
+      onProgress?.(60, '正在填写视频信息...');
+
+      // 填写标题
+      const titleInput = this.page.locator('.title-input, input[placeholder*="标题"]').first();
+      if (await titleInput.count() > 0) {
+        await titleInput.fill(params.title);
+      }
+
+      // 填写描述
       if (params.description) {
-        await this.type('.desc-input', params.description);
+        const descInput = this.page.locator('.desc-input, textarea[placeholder*="描述"]').first();
+        if (await descInput.count() > 0) {
+          await descInput.fill(params.description);
+        }
       }
-      
-      await this.click('.publish-btn');
-      await this.page.waitForSelector('.publish-success', { timeout: 60000 });
-      
-      await this.closeBrowser();
-      
-      return { success: true };
+
+      onProgress?.(80, '正在发布...');
+
+      // 点击发布
+      const publishBtn = this.page.locator('.publish-btn, button:has-text("发布")').first();
+      if (await publishBtn.count() > 0) {
+        await publishBtn.click();
+      } else {
+        throw new Error('未找到发布按钮');
+      }
+
+      // 等待发布结果
+      onProgress?.(90, '等待发布完成...');
+      const publishMaxWait = 120000; // 2分钟
+      const publishStartTime = Date.now();
+      let aiCheckCounter = 0;
+
+      while (Date.now() - publishStartTime < publishMaxWait) {
+        await this.page.waitForTimeout(3000);
+
+        // 检查成功提示
+        const successHint = await this.page.locator('.publish-success, [class*="success"]:has-text("成功")').count();
+        if (successHint > 0) {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        // 检查错误提示
+        const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
+          throw new Error(`发布失败: ${errorHint}`);
+        }
+
+        // AI 辅助检测(每 3 次循环)
+        aiCheckCounter++;
+        if (aiCheckCounter >= 3) {
+          aiCheckCounter = 0;
+          const aiStatus = await this.aiAnalyzePublishStatus();
+          if (aiStatus) {
+            logger.info(`[Kuaishou Publish] AI status: ${aiStatus.status}, confidence: ${aiStatus.confidence}%`);
+
+            if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+
+            if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+              throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+            }
+
+            if (aiStatus.status === 'need_captcha' && onCaptchaRequired) {
+              const imageBase64 = await this.screenshotBase64();
+              try {
+                const captchaCode = await onCaptchaRequired({
+                  taskId: `kuaishou_captcha_${Date.now()}`,
+                  type: 'image',
+                  imageBase64,
+                });
+                if (captchaCode) {
+                  const guide = await this.aiGetPublishOperationGuide('需要输入验证码');
+                  if (guide?.hasAction && guide.targetSelector) {
+                    await this.page.fill(guide.targetSelector, captchaCode);
+                  }
+                }
+              } catch {
+                logger.error('[Kuaishou Publish] Captcha handling failed');
+              }
+            }
+
+            if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
+              const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
+              if (guide?.hasAction) {
+                await this.aiExecuteOperation(guide);
+              }
+            }
+          }
+        }
+      }
+
+      // 超时,AI 最终检查
+      const finalAiStatus = await this.aiAnalyzePublishStatus();
+      if (finalAiStatus?.status === 'success') {
+        onProgress?.(100, '发布成功!');
+        await this.closeBrowser();
+        return { success: true, videoUrl: this.page.url() };
+      }
+
+      throw new Error('发布超时,请手动检查是否发布成功');
+
     } catch (error) {
       logger.error('Kuaishou publishVideo error:', error);
       await this.closeBrowser();

+ 204 - 73
server/src/automation/platforms/weixin.ts

@@ -168,27 +168,28 @@ export class WeixinAdapter extends BasePlatformAdapter {
   }
 
   /**
-   * 通过 Python 服务发布视频
+   * 通过 Python 服务发布视频(带 AI 辅助)
    */
   private async publishVideoViaPython(
     cookies: string,
     params: PublishParams,
     onProgress?: (progress: number, message: string) => void
   ): Promise<PublishResult> {
-    logger.info('[Weixin Python] Starting publish via Python service...');
+    logger.info('[Weixin Python] Starting publish via Python service with AI assist...');
     onProgress?.(5, '正在通过 Python 服务发布...');
 
     try {
       // 将相对路径转换为绝对路径
-      const absoluteVideoPath = path.isAbsolute(params.videoPath) 
-        ? params.videoPath 
+      const absoluteVideoPath = path.isAbsolute(params.videoPath)
+        ? params.videoPath
         : path.resolve(SERVER_ROOT, params.videoPath);
-      
-      const absoluteCoverPath = params.coverPath 
+
+      const absoluteCoverPath = params.coverPath
         ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
         : undefined;
 
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish`, {
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -203,24 +204,16 @@ export class WeixinAdapter extends BasePlatformAdapter {
           tags: params.tags || [],
           post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
           location: params.location || '重庆市',
+          return_screenshot: true,
         }),
         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 || '发布失败');
-      }
+      logger.info('[Weixin Python] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
+
+      // 使用通用的 AI 辅助处理方法
+      return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
     } catch (error) {
       logger.error('[Weixin Python] Publish failed:', error);
       throw error;
@@ -228,9 +221,11 @@ export class WeixinAdapter extends BasePlatformAdapter {
   }
   
   async publishVideo(
-    cookies: string, 
+    cookies: string,
     params: PublishParams,
-    onProgress?: (progress: number, message: string) => void
+    onProgress?: (progress: number, message: string) => void,
+    onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
+    options?: { headless?: boolean }
   ): Promise<PublishResult> {
     // 优先尝试使用 Python 服务
     const pythonAvailable = await this.checkPythonServiceAvailable();
@@ -240,76 +235,212 @@ export class WeixinAdapter extends BasePlatformAdapter {
         return await this.publishVideoViaPython(cookies, params, onProgress);
       } catch (pythonError) {
         logger.warn('[Weixin] Python publish failed, falling back to Playwright:', pythonError);
-        onProgress?.(0, 'Python服务发布失败,正在切换到浏览器模式...');
+        onProgress?.(0, 'Python 服务发布失败,正在切换到浏览器模式...');
       }
     } else {
       logger.info('[Weixin] Python service not available, using Playwright method');
     }
 
     // 回退到 Playwright 方式
+    const useHeadless = options?.headless ?? true;
+
     try {
-      await this.initBrowser();
+      await this.initBrowser({ headless: useHeadless });
       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, '正在选择视频文件...');
-      
+
+      onProgress?.(5, '正在打开上传页面...');
+
+      await this.page.goto(this.publishUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 60000,
+      });
+
+      await this.page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = this.page.url();
+      if (currentUrl.includes('login')) {
+        await this.closeBrowser();
+        return {
+          success: false,
+          errorMessage: '账号登录已过期,请重新登录',
+        };
+      }
+
+      onProgress?.(10, '正在上传视频...');
+
       // 上传视频
-      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');
+      let uploadTriggered = false;
+      const uploadDiv = this.page.locator('div.upload-content, [class*="upload-area"]').first();
+      if (await uploadDiv.count() > 0) {
+        try {
+          const [fileChooser] = await Promise.all([
+            this.page.waitForEvent('filechooser', { timeout: 10000 }),
+            uploadDiv.click(),
+          ]);
+          await fileChooser.setFiles(params.videoPath);
+          uploadTriggered = true;
+        } catch {
+          logger.warn('[Weixin Publish] File chooser method failed');
         }
       }
-      
-      onProgress?.(40, '等待视频上传完成...');
-      
+
+      // 备用方法:直接设置 file input
+      if (!uploadTriggered) {
+        const fileInput = await this.page.$('input[type="file"]');
+        if (fileInput) {
+          await fileInput.setInputFiles(params.videoPath);
+          uploadTriggered = true;
+        }
+      }
+
+      if (!uploadTriggered) {
+        throw new Error('未找到上传入口');
+      }
+
+      onProgress?.(20, '视频上传中...');
+
       // 等待上传完成
-      for (let i = 0; i < 120; i++) {
-        const buttonClass = await this.page.getByRole('button', { name: '发表' }).getAttribute('class');
-        if (buttonClass && !buttonClass.includes('disabled')) {
-          break;
+      const maxWaitTime = 300000; // 5分钟
+      const startTime = Date.now();
+
+      while (Date.now() - startTime < maxWaitTime) {
+        // 检查发布按钮是否可用
+        try {
+          const buttonClass = await this.page.getByRole('button', { name: '发表' }).getAttribute('class');
+          if (buttonClass && !buttonClass.includes('disabled')) {
+            logger.info('[Weixin Publish] Upload completed, publish button enabled');
+            break;
+          }
+        } catch {
+          // 继续等待
         }
+
+        // 检查上传进度
+        const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
+        if (progressText) {
+          const match = progressText.match(/(\d+)%/);
+          if (match) {
+            const progress = parseInt(match[1]);
+            onProgress?.(20 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
+          }
+        }
+
         await this.page.waitForTimeout(3000);
       }
-      
+
+      onProgress?.(60, '正在填写视频信息...');
+
+      // 填写标题和话题
+      const editorDiv = this.page.locator('div.input-editor, [contenteditable="true"]').first();
+      if (await editorDiv.count() > 0) {
+        await editorDiv.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?.(80, '正在发布...');
-      
+
       // 点击发布
-      await this.page.locator('div.form-btns button:has-text("发表")').click();
-      
-      // 等待跳转
-      try {
-        await this.page.waitForURL('**/post/list', { timeout: 30000 });
-      } catch {
-        // 检查是否已经在列表页
+      const publishBtn = this.page.locator('div.form-btns button:has-text("发表"), button:has-text("发表")').first();
+      if (await publishBtn.count() > 0) {
+        await publishBtn.click();
+      } else {
+        throw new Error('未找到发布按钮');
       }
-      
-      await this.closeBrowser();
-      
-      onProgress?.(100, '发布成功');
-      
-      return { success: true, message: '发布成功' };
+
+      // 等待发布结果
+      onProgress?.(90, '等待发布完成...');
+      const publishMaxWait = 120000; // 2分钟
+      const publishStartTime = Date.now();
+      let aiCheckCounter = 0;
+
+      while (Date.now() - publishStartTime < publishMaxWait) {
+        await this.page.waitForTimeout(3000);
+        const newUrl = this.page.url();
+
+        // 检查是否跳转到列表页
+        if (newUrl.includes('/post/list')) {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: newUrl,
+          };
+        }
+
+        // 检查错误提示
+        const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
+        if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
+          throw new Error(`发布失败: ${errorHint}`);
+        }
+
+        // AI 辅助检测(每 3 次循环)
+        aiCheckCounter++;
+        if (aiCheckCounter >= 3) {
+          aiCheckCounter = 0;
+          const aiStatus = await this.aiAnalyzePublishStatus();
+          if (aiStatus) {
+            logger.info(`[Weixin Publish] AI status: ${aiStatus.status}, confidence: ${aiStatus.confidence}%`);
+
+            if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return { success: true, videoUrl: this.page.url() };
+            }
+
+            if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+              throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+            }
+
+            if (aiStatus.status === 'need_captcha' && onCaptchaRequired) {
+              const imageBase64 = await this.screenshotBase64();
+              try {
+                const captchaCode = await onCaptchaRequired({
+                  taskId: `weixin_captcha_${Date.now()}`,
+                  type: 'image',
+                  imageBase64,
+                });
+                if (captchaCode) {
+                  const guide = await this.aiGetPublishOperationGuide('需要输入验证码');
+                  if (guide?.hasAction && guide.targetSelector) {
+                    await this.page.fill(guide.targetSelector, captchaCode);
+                  }
+                }
+              } catch {
+                logger.error('[Weixin Publish] Captcha handling failed');
+              }
+            }
+
+            if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
+              const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
+              if (guide?.hasAction) {
+                await this.aiExecuteOperation(guide);
+              }
+            }
+          }
+        }
+      }
+
+      // 超时,AI 最终检查
+      const finalAiStatus = await this.aiAnalyzePublishStatus();
+      if (finalAiStatus?.status === 'success') {
+        onProgress?.(100, '发布成功!');
+        await this.closeBrowser();
+        return { success: true, videoUrl: this.page.url() };
+      }
+
+      throw new Error('发布超时,请手动检查是否发布成功');
+
     } catch (error) {
       logger.error('Weixin publishVideo error:', error);
       await this.closeBrowser();

+ 95 - 17
server/src/automation/platforms/xiaohongshu.ts

@@ -430,30 +430,24 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
         cookie_length: requestBody.cookie?.length || 0,
       });
 
-      const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/publish`, {
+      // 使用 AI 辅助发布接口
+      const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
-        body: JSON.stringify(requestBody),
+        body: JSON.stringify({
+          ...requestBody,
+          return_screenshot: true,
+        }),
         signal: AbortSignal.timeout(300000), // 5分钟超时
       });
 
       const result = await response.json();
-      logger.info('[Xiaohongshu API] Response:', result);
-      
-      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 || '发布失败');
-      }
+      logger.info('[Xiaohongshu API] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
+
+      // 使用通用的 AI 辅助处理方法
+      return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
     } catch (error) {
       logger.error('[Xiaohongshu API] Publish failed:', error);
       throw error;
@@ -941,6 +935,7 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       // 等待发布结果
       const publishMaxWait = 120000; // 2分钟
       const publishStartTime = Date.now();
+      let aiCheckCounter = 0;
 
       while (Date.now() - publishStartTime < publishMaxWait) {
         await this.page.waitForTimeout(3000);
@@ -976,11 +971,94 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
           throw new Error(`发布失败: ${errorToast}`);
         }
 
+        // AI 辅助检测发布状态(每隔几次循环)
+        aiCheckCounter++;
+        if (aiCheckCounter >= 3) {
+          aiCheckCounter = 0;
+          const aiStatus = await this.aiAnalyzePublishStatus();
+          if (aiStatus) {
+            logger.info(`[Xiaohongshu Publish] AI status: ${aiStatus.status}, confidence: ${aiStatus.confidence}%`);
+
+            if (aiStatus.status === 'success' && aiStatus.confidence >= 70) {
+              logger.info('[Xiaohongshu Publish] AI detected publish success');
+              onProgress?.(100, '发布成功!');
+              await this.closeBrowser();
+              return {
+                success: true,
+                videoUrl: currentUrl,
+              };
+            }
+
+            if (aiStatus.status === 'failed' && aiStatus.confidence >= 70) {
+              logger.error(`[Xiaohongshu Publish] AI detected failure: ${aiStatus.errorMessage}`);
+              throw new Error(aiStatus.errorMessage || 'AI 检测到发布失败');
+            }
+
+            // AI 检测到需要验证码
+            if (aiStatus.status === 'need_captcha') {
+              logger.warn('[Xiaohongshu Publish] AI detected captcha required');
+              if (onCaptchaRequired) {
+                const imageBase64 = await this.screenshotBase64();
+                try {
+                  const captchaCode = await onCaptchaRequired({
+                    taskId: `xhs_captcha_${Date.now()}`,
+                    type: aiStatus.captchaType === 'sms' ? 'sms' : 'image',
+                    imageBase64,
+                  });
+
+                  if (captchaCode) {
+                    const guide = await this.aiGetPublishOperationGuide('需要输入验证码');
+                    if (guide?.hasAction && guide.targetSelector) {
+                      await this.page.fill(guide.targetSelector, captchaCode);
+                      await this.page.waitForTimeout(500);
+
+                      const confirmGuide = await this.aiGetPublishOperationGuide('已输入验证码,需要点击确认');
+                      if (confirmGuide?.hasAction && confirmGuide.targetSelector) {
+                        await this.page.click(confirmGuide.targetSelector);
+                      }
+                    }
+                  }
+                } catch (captchaError) {
+                  logger.error('[Xiaohongshu Publish] Captcha handling failed:', captchaError);
+                }
+              }
+            }
+
+            // AI 建议需要操作
+            if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
+              logger.info(`[Xiaohongshu Publish] AI suggests action: ${aiStatus.nextAction.targetDescription}`);
+              const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
+              if (guide?.hasAction) {
+                await this.aiExecuteOperation(guide);
+              }
+            }
+          }
+        }
+
         const elapsed = Math.floor((Date.now() - publishStartTime) / 1000);
         onProgress?.(90 + Math.min(9, Math.floor(elapsed / 15)), `等待发布完成 (${elapsed}s)...`);
       }
 
-      // 超时,截图调试
+      // 超时,使用 AI 做最后一次状态检查
+      const finalAiStatus = await this.aiAnalyzePublishStatus();
+      if (finalAiStatus) {
+        logger.info(`[Xiaohongshu Publish] Final AI status: ${finalAiStatus.status}`);
+
+        if (finalAiStatus.status === 'success') {
+          onProgress?.(100, '发布成功!');
+          await this.closeBrowser();
+          return {
+            success: true,
+            videoUrl: this.page.url(),
+          };
+        }
+
+        if (finalAiStatus.status === 'failed') {
+          throw new Error(finalAiStatus.errorMessage || 'AI 检测到发布失败');
+        }
+      }
+
+      // 截图调试
       try {
         const screenshotPath = `uploads/debug/xhs_publish_timeout_${Date.now()}.png`;
         await this.page.screenshot({ path: screenshotPath, fullPage: true });

+ 18 - 3
server/src/config/index.ts

@@ -54,9 +54,24 @@ export const config = {
     key: process.env.ENCRYPTION_KEY || 'your-encryption-key-32-chars-long!',
   },
 
-  // AI 配置
+  // AI 配置 - 阿里云百炼千问大模型
   ai: {
-    apiKey: process.env.OPENAI_API_KEY || '',
-    baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
+    apiKey: process.env.DASHSCOPE_API_KEY || process.env.OPENAI_API_KEY || '',
+    baseUrl: process.env.DASHSCOPE_BASE_URL || process.env.OPENAI_BASE_URL || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+    // 默认模型配置
+    defaultModel: process.env.AI_DEFAULT_MODEL || 'qwen-plus',
+    // 可用模型列表
+    models: {
+      // 商业版模型
+      chat: process.env.AI_CHAT_MODEL || 'qwen-plus', // 对话模型
+      fast: process.env.AI_FAST_MODEL || 'qwen-turbo', // 快速模型
+      reasoning: process.env.AI_REASONING_MODEL || 'qwq-plus', // 推理模型
+      vision: process.env.AI_VISION_MODEL || 'qwen-vl-plus', // 视觉理解模型
+      coder: process.env.AI_CODER_MODEL || 'qwen-coder-plus', // 代码模型
+      embedding: process.env.AI_EMBEDDING_MODEL || 'text-embedding-v3', // 嵌入模型
+    },
+    // 请求配置
+    timeout: parseInt(process.env.AI_TIMEOUT || '60000', 10),
+    maxRetries: parseInt(process.env.AI_MAX_RETRIES || '3', 10),
   },
 };

+ 2 - 1
server/src/routes/accounts.ts

@@ -261,7 +261,8 @@ router.post(
   ],
   asyncHandler(async (req, res) => {
     const { platform } = req.body;
-    const result = await browserLoginService.startLoginSession(platform as PlatformType);
+    const userId = req.user!.userId;
+    const result = await browserLoginService.startLoginSession(platform as PlatformType, userId);
     res.json({ success: true, data: result });
   })
 );

+ 375 - 26
server/src/routes/ai.ts

@@ -1,6 +1,6 @@
 import { Router } from 'express';
 import { body } from 'express-validator';
-import { aiService } from '../ai/index.js';
+import { aiService, type ChatMessage } from '../ai/index.js';
 import { authenticate } from '../middleware/auth.js';
 import { asyncHandler, AppError } from '../middleware/error.js';
 import { validateRequest } from '../middleware/validate.js';
@@ -10,6 +10,16 @@ const router = Router();
 
 router.use(authenticate);
 
+// 检查 AI 服务是否可用的中间件
+const checkAIAvailable = asyncHandler(async (_req, _res, next) => {
+  if (!aiService.isAvailable()) {
+    throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
+  }
+  next();
+});
+
+// ==================== 基础 API ====================
+
 // 检查 AI 服务状态
 router.get(
   '/status',
@@ -18,30 +28,381 @@ router.get(
       success: true,
       data: {
         available: aiService.isAvailable(),
+        models: aiService.isAvailable() ? aiService.getAvailableModels() : null,
       },
     });
   })
 );
 
+// 通用聊天接口
+router.post(
+  '/chat',
+  checkAIAvailable,
+  [
+    body('prompt').notEmpty().withMessage('提示内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { prompt, systemPrompt, model } = req.body;
+    const response = await aiService.chat(prompt, systemPrompt, model);
+    res.json({ success: true, data: response });
+  })
+);
+
+// 高级聊天补全接口
+router.post(
+  '/chat/completion',
+  checkAIAvailable,
+  [
+    body('messages').isArray({ min: 1 }).withMessage('消息列表不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { messages, model, temperature, maxTokens, topP, responseFormat } = req.body;
+
+    const response = await aiService.chatCompletion({
+      messages: messages as ChatMessage[],
+      model,
+      temperature,
+      maxTokens,
+      topP,
+      responseFormat,
+    });
+
+    res.json({ success: true, data: response });
+  })
+);
+
+// 流式聊天接口 (SSE)
+router.post(
+  '/chat/stream',
+  checkAIAvailable,
+  [
+    body('messages').isArray({ min: 1 }).withMessage('消息列表不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { messages, model, temperature, maxTokens } = req.body;
+
+    // 设置 SSE 响应头
+    res.setHeader('Content-Type', 'text/event-stream');
+    res.setHeader('Cache-Control', 'no-cache');
+    res.setHeader('Connection', 'keep-alive');
+    res.setHeader('X-Accel-Buffering', 'no');
+
+    try {
+      await aiService.chatCompletionStream(
+        {
+          messages: messages as ChatMessage[],
+          model,
+          temperature,
+          maxTokens,
+        },
+        (chunk, done) => {
+          if (done) {
+            res.write(`data: [DONE]\n\n`);
+            res.end();
+          } else {
+            res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
+          }
+        }
+      );
+    } catch (error) {
+      res.write(`data: ${JSON.stringify({ error: (error as Error).message })}\n\n`);
+      res.end();
+    }
+  })
+);
+
+// 快速聊天(使用快速模型)
+router.post(
+  '/chat/quick',
+  checkAIAvailable,
+  [
+    body('prompt').notEmpty().withMessage('提示内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { prompt, systemPrompt } = req.body;
+    const response = await aiService.quickChat(prompt, systemPrompt);
+    res.json({ success: true, data: response });
+  })
+);
+
+// 代码聊天(使用代码模型)
+router.post(
+  '/chat/code',
+  checkAIAvailable,
+  [
+    body('prompt').notEmpty().withMessage('提示内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { prompt, systemPrompt } = req.body;
+    const response = await aiService.codeChat(prompt, systemPrompt);
+    res.json({ success: true, data: response });
+  })
+);
+
+// 推理聊天(使用推理模型)
+router.post(
+  '/chat/reasoning',
+  checkAIAvailable,
+  [
+    body('prompt').notEmpty().withMessage('提示内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { prompt, systemPrompt } = req.body;
+    const response = await aiService.reasoningChat(prompt, systemPrompt);
+    res.json({ success: true, data: response });
+  })
+);
+
+// ==================== 视觉理解 ====================
+
+// 图像分析
+router.post(
+  '/vision/analyze',
+  checkAIAvailable,
+  [
+    body('prompt').notEmpty().withMessage('提示内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { prompt, imageUrl, imageBase64, model, maxTokens } = req.body;
+
+    if (!imageUrl && !imageBase64) {
+      throw new AppError('必须提供图片URL或Base64数据', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION);
+    }
+
+    const response = await aiService.analyzeImage({
+      prompt,
+      imageUrl,
+      imageBase64,
+      model,
+      maxTokens,
+    });
+
+    res.json({ success: true, data: response });
+  })
+);
+
+// 登录状态分析(专用于浏览器登录辅助)
+router.post(
+  '/vision/login-status',
+  checkAIAvailable,
+  [
+    body('imageBase64').notEmpty().withMessage('截图数据不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { imageBase64, platform } = req.body;
+    const result = await aiService.analyzeLoginStatus(imageBase64, platform);
+    res.json({ success: true, data: result });
+  })
+);
+
+// 账号信息提取(从截图中提取账号信息)
+router.post(
+  '/vision/extract-account',
+  checkAIAvailable,
+  [
+    body('imageBase64').notEmpty().withMessage('截图数据不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { imageBase64, platform } = req.body;
+    const result = await aiService.extractAccountInfo(imageBase64, platform);
+    res.json({ success: true, data: result });
+  })
+);
+
+// 页面操作指导(AI 指导用户进行操作 - 基于截图)
+router.post(
+  '/vision/operation-guide',
+  checkAIAvailable,
+  [
+    body('imageBase64').notEmpty().withMessage('截图数据不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    body('goal').notEmpty().withMessage('操作目标不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { imageBase64, platform, goal } = req.body;
+    const result = await aiService.getPageOperationGuide(imageBase64, platform, goal);
+    res.json({ success: true, data: result });
+  })
+);
+
+// 页面操作指导(AI 指导用户进行操作 - 基于 HTML)
+router.post(
+  '/html/operation-guide',
+  checkAIAvailable,
+  [
+    body('html').notEmpty().withMessage('HTML 内容不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    body('goal').notEmpty().withMessage('操作目标不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { html, platform, goal } = req.body;
+    const result = await aiService.analyzeHtmlForOperation(html, platform, goal);
+    res.json({ success: true, data: result });
+  })
+);
+
+// ==================== 发布辅助 ====================
+
+// 分析发布页面状态(检测验证码、发布结果等)
+router.post(
+  '/publish/analyze-status',
+  checkAIAvailable,
+  [
+    body('imageBase64').notEmpty().withMessage('截图数据不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { imageBase64, platform } = req.body;
+    const result = await aiService.analyzePublishStatus(imageBase64, platform);
+    res.json({ success: true, data: result });
+  })
+);
+
+// 分析发布页面 HTML 获取操作指导
+router.post(
+  '/publish/operation-guide',
+  checkAIAvailable,
+  [
+    body('html').notEmpty().withMessage('HTML 内容不能为空'),
+    body('platform').notEmpty().withMessage('平台名称不能为空'),
+    body('currentStatus').notEmpty().withMessage('当前状态不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { html, platform, currentStatus } = req.body;
+    const result = await aiService.analyzePublishPageHtml(html, platform, currentStatus);
+    res.json({ success: true, data: result });
+  })
+);
+
+// ==================== 文本嵌入 ====================
+
+// 生成嵌入向量
+router.post(
+  '/embedding',
+  checkAIAvailable,
+  [
+    body('input').notEmpty().withMessage('输入文本不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { input, model, dimensions } = req.body;
+    const embeddings = await aiService.createEmbedding({ input, model, dimensions });
+    res.json({ success: true, data: embeddings });
+  })
+);
+
+// 计算文本相似度
+router.post(
+  '/similarity',
+  checkAIAvailable,
+  [
+    body('text1').notEmpty().withMessage('文本1不能为空'),
+    body('text2').notEmpty().withMessage('文本2不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { text1, text2 } = req.body;
+    const similarity = await aiService.calculateSimilarity(text1, text2);
+    res.json({ success: true, data: { similarity } });
+  })
+);
+
+// ==================== 文本处理 ====================
+
+// 文本摘要
+router.post(
+  '/summarize',
+  checkAIAvailable,
+  [
+    body('content').notEmpty().withMessage('内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { content, maxLength } = req.body;
+    const summary = await aiService.summarize(content, maxLength);
+    res.json({ success: true, data: summary });
+  })
+);
+
+// 文本翻译
+router.post(
+  '/translate',
+  checkAIAvailable,
+  [
+    body('text').notEmpty().withMessage('文本不能为空'),
+    body('targetLang').notEmpty().withMessage('目标语言不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { text, targetLang, sourceLang } = req.body;
+    const translated = await aiService.translate(text, targetLang, sourceLang);
+    res.json({ success: true, data: translated });
+  })
+);
+
+// 关键词提取
+router.post(
+  '/keywords',
+  checkAIAvailable,
+  [
+    body('text').notEmpty().withMessage('文本不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { text, count } = req.body;
+    const keywords = await aiService.extractKeywords(text, count);
+    res.json({ success: true, data: keywords });
+  })
+);
+
+// 内容审核
+router.post(
+  '/moderate',
+  checkAIAvailable,
+  [
+    body('content').notEmpty().withMessage('内容不能为空'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const { content } = req.body;
+    const result = await aiService.moderateContent(content);
+    res.json({ success: true, data: result });
+  })
+);
+
+// ==================== 自媒体业务场景 ====================
+
 // 生成标题
 router.post(
   '/generate-title',
+  checkAIAvailable,
   [
     body('description').notEmpty().withMessage('描述不能为空'),
     body('platform').notEmpty().withMessage('平台不能为空'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
-    if (!aiService.isAvailable()) {
-      throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
-    }
-    
     const titles = await aiService.generateTitle({
       description: req.body.description,
       platform: req.body.platform,
       maxLength: req.body.maxLength,
     });
-    
+
     res.json({ success: true, data: titles });
   })
 );
@@ -49,23 +410,20 @@ router.post(
 // 生成标签
 router.post(
   '/generate-tags',
+  checkAIAvailable,
   [
     body('title').notEmpty().withMessage('标题不能为空'),
     body('platform').notEmpty().withMessage('平台不能为空'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
-    if (!aiService.isAvailable()) {
-      throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
-    }
-    
     const tags = await aiService.generateTags({
       title: req.body.title,
       description: req.body.description,
       platform: req.body.platform,
       maxTags: req.body.maxTags,
     });
-    
+
     res.json({ success: true, data: tags });
   })
 );
@@ -73,22 +431,19 @@ router.post(
 // 优化描述
 router.post(
   '/optimize-description',
+  checkAIAvailable,
   [
     body('original').notEmpty().withMessage('原始描述不能为空'),
     body('platform').notEmpty().withMessage('平台不能为空'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
-    if (!aiService.isAvailable()) {
-      throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
-    }
-    
     const optimized = await aiService.optimizeDescription({
       original: req.body.original,
       platform: req.body.platform,
       maxLength: req.body.maxLength,
     });
-    
+
     res.json({ success: true, data: optimized });
   })
 );
@@ -96,23 +451,20 @@ router.post(
 // 生成评论回复
 router.post(
   '/generate-reply',
+  checkAIAvailable,
   [
     body('comment').notEmpty().withMessage('评论内容不能为空'),
     body('authorName').notEmpty().withMessage('评论作者不能为空'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
-    if (!aiService.isAvailable()) {
-      throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
-    }
-    
     const replies = await aiService.generateReply({
       comment: req.body.comment,
       authorName: req.body.authorName,
       context: req.body.context,
       tone: req.body.tone,
     });
-    
+
     res.json({ success: true, data: replies });
   })
 );
@@ -120,22 +472,19 @@ router.post(
 // 推荐发布时间
 router.post(
   '/recommend-time',
+  checkAIAvailable,
   [
     body('platform').notEmpty().withMessage('平台不能为空'),
     body('contentType').notEmpty().withMessage('内容类型不能为空'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
-    if (!aiService.isAvailable()) {
-      throw new AppError('AI 服务未配置', HTTP_STATUS.SERVICE_UNAVAILABLE, ERROR_CODES.SERVICE_UNAVAILABLE);
-    }
-    
     const times = await aiService.recommendPublishTime({
       platform: req.body.platform,
       contentType: req.body.contentType,
       targetAudience: req.body.targetAudience,
     });
-    
+
     res.json({ success: true, data: times });
   })
 );

+ 595 - 0
server/src/services/AILoginAssistant.ts

@@ -0,0 +1,595 @@
+import type { Page } from 'playwright';
+import { promises as fs } from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { logger } from '../utils/logger.js';
+import { config } from '../config/index.js';
+import {
+  aiService,
+  type LoginStatusAnalysis,
+  type AccountInfoExtraction,
+  type PageOperationGuide,
+} from '../ai/index.js';
+import type { PlatformType } from '@media-manager/shared';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const DEBUG_SCREENSHOT_DIR = path.join(__dirname, '../../uploads/debug');
+
+/**
+ * AI 登录状态监控结果
+ */
+export interface AILoginMonitorResult {
+  status: 'pending' | 'logged_in' | 'verification_needed' | 'error';
+  analysis: LoginStatusAnalysis;
+  screenshotBase64?: string;
+}
+
+/**
+ * AI 账号信息获取结果
+ */
+export interface AIAccountInfoResult {
+  success: boolean;
+  accountInfo?: AccountInfoExtraction;
+  needNavigation: boolean;
+  navigationGuide?: PageOperationGuide;
+}
+
+/**
+ * AI 登录辅助服务
+ * 使用千问视觉模型分析登录页面状态
+ */
+export class AILoginAssistant {
+  private platform: PlatformType;
+  private page: Page;
+  private sessionId: string;
+  private isMonitoring: boolean = false;
+  private isAnalyzing: boolean = false; // 请求锁,防止并发请求
+  private maxAnalysisAttempts: number = 30; // 最多分析30次
+  private analysisCount: number = 0;
+  private waitAfterResponseMs: number = 3000; // API 返回后等待时间(3秒)
+  private initialDelayMs: number = 3000; // 首次分析前等待时间(3秒)
+  private savedScreenshots: string[] = []; // 保存的截图文件路径列表
+  private isDevelopment: boolean;
+
+  constructor(page: Page, platform: PlatformType, sessionId?: string) {
+    this.page = page;
+    this.platform = platform;
+    this.sessionId = sessionId || `session_${Date.now()}`;
+    this.isDevelopment = config.env === 'development';
+  }
+
+  /**
+   * 确保 debug 截图目录存在
+   */
+  private async ensureDebugDir(): Promise<void> {
+    if (!this.isDevelopment) return;
+    
+    try {
+      await fs.mkdir(DEBUG_SCREENSHOT_DIR, { recursive: true });
+    } catch (error) {
+      logger.warn('[AILoginAssistant] Failed to create debug directory:', error);
+    }
+  }
+
+  /**
+   * 保存截图到 debug 目录(仅开发模式)
+   */
+  private async saveScreenshotToDebug(base64Data: string, suffix: string = ''): Promise<string | null> {
+    if (!this.isDevelopment) return null;
+
+    try {
+      await this.ensureDebugDir();
+      
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+      const filename = `${this.platform}_${this.sessionId}_${timestamp}${suffix}.jpg`;
+      const filepath = path.join(DEBUG_SCREENSHOT_DIR, filename);
+      
+      const buffer = Buffer.from(base64Data, 'base64');
+      await fs.writeFile(filepath, buffer);
+      
+      this.savedScreenshots.push(filepath);
+      logger.info(`[AILoginAssistant] Screenshot saved: ${filename}`);
+      
+      return filepath;
+    } catch (error) {
+      logger.warn('[AILoginAssistant] Failed to save screenshot:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 清理所有保存的截图(生产环境自动调用,或手动调用)
+   */
+  async cleanupScreenshots(): Promise<void> {
+    if (this.savedScreenshots.length === 0) return;
+
+    logger.info(`[AILoginAssistant] Cleaning up ${this.savedScreenshots.length} screenshots...`);
+    
+    for (const filepath of this.savedScreenshots) {
+      try {
+        await fs.unlink(filepath);
+        logger.debug(`[AILoginAssistant] Deleted screenshot: ${filepath}`);
+      } catch (error) {
+        // 文件可能已经被删除,忽略错误
+        logger.debug(`[AILoginAssistant] Failed to delete screenshot: ${filepath}`);
+      }
+    }
+    
+    this.savedScreenshots = [];
+  }
+
+  /**
+   * 检查 AI 服务是否可用
+   */
+  isAIAvailable(): boolean {
+    return aiService.isAvailable();
+  }
+
+  /**
+   * 截取当前页面截图并转换为 Base64
+   * @param saveToDebug 是否保存到 debug 目录(仅开发模式有效)
+   * @param suffix 文件名后缀
+   */
+  async captureScreenshot(saveToDebug: boolean = true, suffix: string = ''): Promise<string> {
+    try {
+      const screenshot = await this.page.screenshot({
+        type: 'jpeg',
+        quality: 80,
+        fullPage: false,
+      });
+      const base64Data = screenshot.toString('base64');
+      
+      // 开发模式下保存截图到 debug 目录
+      if (saveToDebug && this.isDevelopment) {
+        await this.saveScreenshotToDebug(base64Data, suffix);
+      }
+      
+      return base64Data;
+    } catch (error) {
+      logger.error('[AILoginAssistant] Failed to capture screenshot:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 分析当前页面的登录状态
+   * @param analysisIndex 分析序号(用于截图文件命名)
+   */
+  async analyzeCurrentPage(analysisIndex?: number): Promise<AILoginMonitorResult> {
+    if (!this.isAIAvailable()) {
+      return {
+        status: 'error',
+        analysis: {
+          isLoggedIn: false,
+          hasVerification: false,
+          pageDescription: 'AI 服务不可用',
+        },
+      };
+    }
+
+    try {
+      // 截图文件名后缀:包含分析序号
+      const suffix = analysisIndex !== undefined ? `_analysis_${String(analysisIndex).padStart(3, '0')}` : '';
+      const screenshotBase64 = await this.captureScreenshot(true, suffix);
+      logger.info(`[AILoginAssistant] Analyzing page for platform: ${this.platform}`);
+
+      const analysis = await aiService.analyzeLoginStatus(screenshotBase64, this.platform);
+
+      logger.info(`[AILoginAssistant] Analysis result:`, {
+        isLoggedIn: analysis.isLoggedIn,
+        hasVerification: analysis.hasVerification,
+        verificationType: analysis.verificationType,
+        pageDescription: analysis.pageDescription?.substring(0, 100),
+      });
+
+      let status: AILoginMonitorResult['status'] = 'pending';
+      if (analysis.isLoggedIn) {
+        status = 'logged_in';
+      } else if (analysis.hasVerification) {
+        status = 'verification_needed';
+      }
+
+      return {
+        status,
+        analysis,
+        screenshotBase64,
+      };
+    } catch (error) {
+      logger.error('[AILoginAssistant] Analysis error:', error);
+      return {
+        status: 'error',
+        analysis: {
+          isLoggedIn: false,
+          hasVerification: false,
+          pageDescription: `分析出错: ${error instanceof Error ? error.message : '未知错误'}`,
+        },
+      };
+    }
+  }
+
+  /**
+   * 开始监控登录状态
+   * 使用异步循环确保等待 API 返回后再等待指定时间,防止并发请求
+   * @param onStatusChange 状态变化回调
+   * @param waitAfterResponseMs API 返回后等待时间(毫秒),默认3000ms
+   */
+  startMonitoring(
+    onStatusChange: (result: AILoginMonitorResult) => void,
+    waitAfterResponseMs: number = 3000
+  ): void {
+    if (this.isMonitoring) {
+      logger.warn('[AILoginAssistant] Already monitoring');
+      return;
+    }
+
+    if (!this.isAIAvailable()) {
+      logger.warn('[AILoginAssistant] AI service not available, skipping AI monitoring');
+      return;
+    }
+
+    this.isMonitoring = true;
+    this.analysisCount = 0;
+    this.waitAfterResponseMs = waitAfterResponseMs;
+    logger.info(`[AILoginAssistant] Starting login status monitoring for ${this.platform}, wait after response: ${waitAfterResponseMs}ms`);
+
+    // 使用异步循环代替 setInterval,确保顺序执行
+    this.runMonitorLoop(onStatusChange);
+  }
+
+  /**
+   * 监控循环(异步,确保顺序执行)
+   */
+  private async runMonitorLoop(onStatusChange: (result: AILoginMonitorResult) => void): Promise<void> {
+    // 首次分析前等待页面加载
+    logger.info(`[AILoginAssistant] Waiting ${this.initialDelayMs}ms before first analysis...`);
+    await this.sleep(this.initialDelayMs);
+
+    while (this.isMonitoring) {
+      // 检查分析次数上限
+      if (this.analysisCount >= this.maxAnalysisAttempts) {
+        logger.warn(`[AILoginAssistant] Max analysis attempts (${this.maxAnalysisAttempts}) reached, stopping`);
+        this.stopMonitoring();
+        return;
+      }
+
+      // 检查是否已有请求在进行中(双重保险)
+      if (this.isAnalyzing) {
+        logger.warn('[AILoginAssistant] Analysis already in progress, waiting...');
+        await this.sleep(1000);
+        continue;
+      }
+
+      try {
+        this.isAnalyzing = true;
+        this.analysisCount++;
+        
+        logger.info(`[AILoginAssistant] Starting analysis #${this.analysisCount}...`);
+        const startTime = Date.now();
+        
+        // 传递分析序号用于截图命名
+        const result = await this.analyzeCurrentPage(this.analysisCount);
+        
+        const duration = Date.now() - startTime;
+        logger.info(`[AILoginAssistant] Analysis #${this.analysisCount} completed in ${duration}ms`);
+        
+        // 调用回调
+        onStatusChange(result);
+
+        // 如果登录成功,停止监控,并清理截图(生产环境)
+        if (result.status === 'logged_in') {
+          logger.info('[AILoginAssistant] Login detected, stopping monitoring');
+          this.stopMonitoring();
+          // 生产环境下清理截图
+          if (!this.isDevelopment) {
+            await this.cleanupScreenshots();
+          }
+          return;
+        }
+
+      } catch (error) {
+        logger.error(`[AILoginAssistant] Monitor iteration #${this.analysisCount} error:`, error);
+      } finally {
+        this.isAnalyzing = false;
+      }
+
+      // 如果仍在监控,等待指定时间后再进行下一次分析
+      if (this.isMonitoring) {
+        logger.info(`[AILoginAssistant] Waiting ${this.waitAfterResponseMs}ms before next analysis...`);
+        await this.sleep(this.waitAfterResponseMs);
+      }
+    }
+  }
+
+  /**
+   * 休眠指定时间
+   */
+  private sleep(ms: number): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+
+  /**
+   * 停止监控
+   */
+  stopMonitoring(): void {
+    this.isMonitoring = false;
+    logger.info(`[AILoginAssistant] Monitoring stopped after ${this.analysisCount} analyses`);
+  }
+
+  /**
+   * 获取账号信息(带智能导航)
+   * @param maxAttempts 最大尝试次数
+   */
+  async getAccountInfoWithAI(maxAttempts: number = 5): Promise<AIAccountInfoResult> {
+    if (!this.isAIAvailable()) {
+      return {
+        success: false,
+        needNavigation: false,
+      };
+    }
+
+    for (let attempt = 0; attempt < maxAttempts; attempt++) {
+      logger.info(`[AILoginAssistant] Attempting to get account info (attempt ${attempt + 1}/${maxAttempts})`);
+
+      try {
+        // 等待页面稳定
+        await this.page.waitForTimeout(1000);
+
+        // 截图并分析
+        const screenshotBase64 = await this.captureScreenshot();
+        const accountInfo = await aiService.extractAccountInfo(screenshotBase64, this.platform);
+
+        logger.info(`[AILoginAssistant] Account info extraction result:`, {
+          found: accountInfo.found,
+          accountName: accountInfo.accountName,
+          hasNavigationSuggestion: !!accountInfo.navigationSuggestion,
+        });
+
+        if (accountInfo.found && accountInfo.accountName) {
+          return {
+            success: true,
+            accountInfo,
+            needNavigation: false,
+          };
+        }
+
+        // 如果没找到账号信息,尝试获取导航指导
+        if (accountInfo.navigationSuggestion) {
+          logger.info(`[AILoginAssistant] Navigation suggestion: ${accountInfo.navigationSuggestion}`);
+
+          // 获取具体的操作指导
+          const guide = await aiService.getPageOperationGuide(
+            screenshotBase64,
+            this.platform,
+            '获取账号信息(用户名、头像、粉丝数等)'
+          );
+
+          logger.info(`[AILoginAssistant] Operation guide:`, {
+            hasAction: guide.hasAction,
+            actionType: guide.actionType,
+            targetDescription: guide.targetDescription,
+            explanation: guide.explanation,
+          });
+
+          if (guide.hasAction) {
+            // 尝试执行操作
+            const executed = await this.executeOperation(guide);
+            if (executed) {
+              // 等待页面更新后继续下一次尝试
+              await this.page.waitForTimeout(2000);
+              continue;
+            }
+          }
+
+          return {
+            success: false,
+            accountInfo,
+            needNavigation: true,
+            navigationGuide: guide,
+          };
+        }
+
+        // 没有导航建议,返回失败
+        return {
+          success: false,
+          accountInfo,
+          needNavigation: false,
+        };
+      } catch (error) {
+        logger.error(`[AILoginAssistant] Get account info error (attempt ${attempt + 1}):`, error);
+      }
+    }
+
+    return {
+      success: false,
+      needNavigation: false,
+    };
+  }
+
+  /**
+   * 执行 AI 指导的页面操作
+   * @param guide 操作指导
+   * @returns 是否成功执行
+   */
+  async executeOperation(guide: PageOperationGuide): Promise<boolean> {
+    if (!guide.hasAction) {
+      return false;
+    }
+
+    try {
+      switch (guide.actionType) {
+        case 'click':
+          return await this.executeClick(guide);
+
+        case 'input':
+          return await this.executeInput(guide);
+
+        case 'scroll':
+          return await this.executeScroll(guide);
+
+        case 'wait':
+          await this.page.waitForTimeout(2000);
+          return true;
+
+        case 'navigate':
+          // 导航操作需要用户手动处理
+          logger.info(`[AILoginAssistant] Navigation required: ${guide.explanation}`);
+          return false;
+
+        default:
+          logger.warn(`[AILoginAssistant] Unknown action type: ${guide.actionType}`);
+          return false;
+      }
+    } catch (error) {
+      logger.error('[AILoginAssistant] Execute operation error:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 执行点击操作
+   */
+  private async executeClick(guide: PageOperationGuide): Promise<boolean> {
+    // 优先使用选择器
+    if (guide.targetSelector) {
+      try {
+        const element = this.page.locator(guide.targetSelector).first();
+        if (await element.count() > 0 && await element.isVisible()) {
+          await element.click();
+          logger.info(`[AILoginAssistant] Clicked element with selector: ${guide.targetSelector}`);
+          return true;
+        }
+      } catch (e) {
+        logger.warn(`[AILoginAssistant] Failed to click by selector: ${guide.targetSelector}`, e);
+      }
+    }
+
+    // 尝试使用文本内容定位
+    if (guide.targetDescription) {
+      try {
+        // 尝试常见的可点击元素
+        const textVariants = [
+          guide.targetDescription,
+          guide.targetDescription.replace(/["""]/g, ''),
+        ];
+
+        for (const text of textVariants) {
+          // 尝试精确匹配
+          let element = this.page.getByText(text, { exact: true }).first();
+          if (await element.count() > 0 && await element.isVisible().catch(() => false)) {
+            await element.click();
+            logger.info(`[AILoginAssistant] Clicked element with text: ${text}`);
+            return true;
+          }
+
+          // 尝试模糊匹配
+          element = this.page.getByText(text, { exact: false }).first();
+          if (await element.count() > 0 && await element.isVisible().catch(() => false)) {
+            await element.click();
+            logger.info(`[AILoginAssistant] Clicked element with fuzzy text: ${text}`);
+            return true;
+          }
+
+          // 尝试作为按钮或链接
+          element = this.page.getByRole('button', { name: text }).first();
+          if (await element.count() > 0 && await element.isVisible().catch(() => false)) {
+            await element.click();
+            logger.info(`[AILoginAssistant] Clicked button with name: ${text}`);
+            return true;
+          }
+
+          element = this.page.getByRole('link', { name: text }).first();
+          if (await element.count() > 0 && await element.isVisible().catch(() => false)) {
+            await element.click();
+            logger.info(`[AILoginAssistant] Clicked link with name: ${text}`);
+            return true;
+          }
+        }
+      } catch (e) {
+        logger.warn(`[AILoginAssistant] Failed to click by text: ${guide.targetDescription}`, e);
+      }
+    }
+
+    // 最后尝试坐标点击
+    if (guide.targetPosition) {
+      try {
+        await this.page.mouse.click(guide.targetPosition.x, guide.targetPosition.y);
+        logger.info(`[AILoginAssistant] Clicked at position: (${guide.targetPosition.x}, ${guide.targetPosition.y})`);
+        return true;
+      } catch (e) {
+        logger.warn(`[AILoginAssistant] Failed to click at position`, e);
+      }
+    }
+
+    logger.warn('[AILoginAssistant] Could not execute click operation');
+    return false;
+  }
+
+  /**
+   * 执行输入操作
+   */
+  private async executeInput(guide: PageOperationGuide): Promise<boolean> {
+    if (!guide.inputText) {
+      return false;
+    }
+
+    // 尝试使用选择器
+    if (guide.targetSelector) {
+      try {
+        const element = this.page.locator(guide.targetSelector).first();
+        if (await element.count() > 0 && await element.isVisible()) {
+          await element.fill(guide.inputText);
+          logger.info(`[AILoginAssistant] Input text to selector: ${guide.targetSelector}`);
+          return true;
+        }
+      } catch (e) {
+        logger.warn(`[AILoginAssistant] Failed to input by selector`, e);
+      }
+    }
+
+    // 尝试查找输入框
+    try {
+      const inputs = this.page.locator('input:visible, textarea:visible');
+      const count = await inputs.count();
+      if (count > 0) {
+        await inputs.first().fill(guide.inputText);
+        logger.info(`[AILoginAssistant] Input text to first visible input`);
+        return true;
+      }
+    } catch (e) {
+      logger.warn(`[AILoginAssistant] Failed to input to visible input`, e);
+    }
+
+    return false;
+  }
+
+  /**
+   * 执行滚动操作
+   */
+  private async executeScroll(guide: PageOperationGuide): Promise<boolean> {
+    try {
+      // 默认向下滚动
+      await this.page.mouse.wheel(0, 300);
+      logger.info('[AILoginAssistant] Scrolled page');
+      return true;
+    } catch (e) {
+      logger.warn('[AILoginAssistant] Failed to scroll', e);
+      return false;
+    }
+  }
+
+  /**
+   * 销毁实例
+   * @param cleanupScreenshots 是否清理截图(默认生产环境清理)
+   */
+  async destroy(cleanupScreenshots?: boolean): Promise<void> {
+    this.stopMonitoring();
+    
+    // 根据参数或环境决定是否清理截图
+    const shouldCleanup = cleanupScreenshots !== undefined ? cleanupScreenshots : !this.isDevelopment;
+    if (shouldCleanup) {
+      await this.cleanupScreenshots();
+    } else if (this.savedScreenshots.length > 0) {
+      logger.info(`[AILoginAssistant] Development mode: ${this.savedScreenshots.length} screenshots saved to debug directory`);
+    }
+  }
+}

+ 133 - 32
server/src/services/AccountService.ts

@@ -16,6 +16,7 @@ import { WS_EVENTS } from '@media-manager/shared';
 import { CookieManager } from '../automation/cookie.js';
 import { logger } from '../utils/logger.js';
 import { headlessBrowserService } from './HeadlessBrowserService.js';
+import { aiService } from '../ai/index.js';
 
 interface GetAccountsParams {
   platform?: string;
@@ -281,6 +282,7 @@ export class AccountService {
     };
 
     let needReLogin = false;
+    let aiRefreshSuccess = false;
 
     // 尝试通过无头浏览器刷新账号信息
     if (account.cookieData) {
@@ -309,41 +311,80 @@ export class AccountService {
         }
 
         if (cookieList.length > 0 && !cookieParseError) {
-          // 第一步:通过浏览器检查 Cookie 是否有效(访问后台页面检测是否被重定向到登录页)
-          const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
+          // ========== AI 辅助刷新(优先) ==========
+          if (aiService.isAvailable()) {
+            try {
+              logger.info(`[AI Refresh] Starting AI-assisted refresh for account ${accountId} (${platform})`);
+              
+              // 使用无头浏览器截图,然后 AI 分析
+              const aiResult = await this.refreshAccountWithAI(platform, cookieList, accountId);
+              
+              if (aiResult.needReLogin) {
+                // AI 检测到需要重新登录
+                updateData.status = 'expired';
+                needReLogin = true;
+                aiRefreshSuccess = true;
+                logger.warn(`[AI Refresh] Account ${accountId} needs re-login (detected by AI)`);
+              } else if (aiResult.accountInfo) {
+                // AI 成功获取到账号信息
+                updateData.status = 'active';
+                updateData.accountName = aiResult.accountInfo.accountName;
+                if (aiResult.accountInfo.avatarUrl) {
+                  updateData.avatarUrl = aiResult.accountInfo.avatarUrl;
+                }
+                if (aiResult.accountInfo.fansCount !== undefined) {
+                  updateData.fansCount = aiResult.accountInfo.fansCount;
+                }
+                if (aiResult.accountInfo.worksCount !== undefined) {
+                  updateData.worksCount = aiResult.accountInfo.worksCount;
+                }
+                aiRefreshSuccess = true;
+                logger.info(`[AI Refresh] Successfully refreshed account ${accountId}: ${aiResult.accountInfo.accountName}`);
+              }
+            } catch (aiError) {
+              logger.warn(`[AI Refresh] AI-assisted refresh failed for account ${accountId}:`, aiError);
+              // AI 刷新失败,继续使用原有逻辑
+            }
+          }
           
-          if (!isValid) {
-            // Cookie 已过期,需要重新登录
-            updateData.status = 'expired';
-            needReLogin = true;
-            logger.warn(`Account ${accountId} (${account.accountName}) cookie expired, need re-login`);
-          } else {
-            // Cookie 有效,尝试获取账号信息
-            updateData.status = 'active';
+          // ========== 原有逻辑(AI 失败时的备用方案) ==========
+          if (!aiRefreshSuccess) {
+            // 第一步:通过浏览器检查 Cookie 是否有效
+            const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
             
-            try {
-              const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
-
-              // 检查是否获取到有效信息(排除默认名称)
-              const defaultNames = [
-                `${platform}账号`, '未知账号', '抖音账号', '小红书账号', 
-                '快手账号', '视频号账号', 'B站账号', '头条账号', '百家号账号'
-              ];
-              const isValidProfile = profile.accountName && !defaultNames.includes(profile.accountName);
-
-              if (isValidProfile) {
-                updateData.accountName = profile.accountName;
-                updateData.avatarUrl = profile.avatarUrl;
-                updateData.fansCount = profile.fansCount;
-                updateData.worksCount = profile.worksCount;
-                logger.info(`Refreshed account info for ${platform}: ${profile.accountName}, fans: ${profile.fansCount}`);
-              } else {
-                // 获取的信息无效,但 Cookie 有效,保持 active 状态
-                logger.warn(`Could not fetch valid account info for ${accountId}, but cookie is valid`);
+            if (!isValid) {
+              // Cookie 已过期,需要重新登录
+              updateData.status = 'expired';
+              needReLogin = true;
+              logger.warn(`Account ${accountId} (${account.accountName}) cookie expired, need re-login`);
+            } else {
+              // Cookie 有效,尝试获取账号信息
+              updateData.status = 'active';
+              
+              try {
+                const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
+
+                // 检查是否获取到有效信息(排除默认名称)
+                const defaultNames = [
+                  `${platform}账号`, '未知账号', '抖音账号', '小红书账号', 
+                  '快手账号', '视频号账号', 'B站账号', '头条账号', '百家号账号'
+                ];
+                const isValidProfile = profile.accountName && !defaultNames.includes(profile.accountName);
+
+                if (isValidProfile) {
+                  updateData.accountName = profile.accountName;
+                  updateData.avatarUrl = profile.avatarUrl;
+                  updateData.fansCount = profile.fansCount;
+                  updateData.worksCount = profile.worksCount;
+                  logger.info(`Refreshed account info for ${platform}: ${profile.accountName}, fans: ${profile.fansCount}`);
+                } else {
+                  // 获取的信息无效,但 Cookie 有效,保持 active 状态
+                  logger.warn(`Could not fetch valid account info for ${accountId}, but cookie is valid`);
+                }
+              } catch (infoError) {
+                // 获取账号信息失败,但 Cookie 检查已通过,保持 active 状态
+                logger.warn(`Failed to fetch account info for ${accountId}, but cookie is valid:`, infoError);
               }
-            } catch (infoError) {
-              // 获取账号信息失败,但 Cookie 检查已通过,保持 active 状态
-              logger.warn(`Failed to fetch account info for ${accountId}, but cookie is valid:`, infoError);
             }
           }
         }
@@ -366,6 +407,66 @@ export class AccountService {
   }
 
   /**
+   * 使用 AI 辅助刷新账号信息
+   */
+  private async refreshAccountWithAI(
+    platform: PlatformType,
+    cookieList: { name: string; value: string; domain: string; path: string }[],
+    accountId: number
+  ): Promise<{
+    needReLogin: boolean;
+    accountInfo?: {
+      accountName: string;
+      avatarUrl?: string;
+      fansCount?: number;
+      worksCount?: number;
+    };
+  }> {
+    // 使用无头浏览器访问平台后台并截图
+    const screenshot = await headlessBrowserService.capturePageScreenshot(platform, cookieList);
+    
+    if (!screenshot) {
+      throw new Error('Failed to capture screenshot');
+    }
+    
+    // 第一步:使用 AI 分析登录状态
+    const loginStatus = await aiService.analyzeLoginStatus(screenshot, platform);
+    
+    logger.info(`[AI Refresh] Login status for account ${accountId}:`, {
+      isLoggedIn: loginStatus.isLoggedIn,
+      hasVerification: loginStatus.hasVerification,
+    });
+    
+    // 如果 AI 检测到未登录或有验证码,说明需要重新登录
+    if (!loginStatus.isLoggedIn || loginStatus.hasVerification) {
+      return { needReLogin: true };
+    }
+    
+    // 第二步:使用 AI 提取账号信息
+    const accountInfo = await aiService.extractAccountInfo(screenshot, platform);
+    
+    logger.info(`[AI Refresh] Account info extraction for ${accountId}:`, {
+      found: accountInfo.found,
+      accountName: accountInfo.accountName,
+    });
+    
+    if (accountInfo.found && accountInfo.accountName) {
+      return {
+        needReLogin: false,
+        accountInfo: {
+          accountName: accountInfo.accountName,
+          fansCount: accountInfo.fansCount ? parseInt(String(accountInfo.fansCount)) : undefined,
+          worksCount: accountInfo.worksCount ? parseInt(String(accountInfo.worksCount)) : undefined,
+        },
+      };
+    }
+    
+    // AI 未能提取到账号信息,但登录状态正常
+    // 返回空结果,让原有逻辑处理
+    return { needReLogin: false };
+  }
+
+  /**
    * 检查账号 Cookie 是否有效
    */
   async checkAccountStatus(userId: number, accountId: number): Promise<{ isValid: boolean }> {

+ 259 - 15
server/src/services/BrowserLoginService.ts

@@ -3,6 +3,8 @@ import { chromium, type Browser, type BrowserContext, type Page } from 'playwrig
 import { EventEmitter } from 'events';
 import { logger } from '../utils/logger.js';
 import { CookieManager } from '../automation/cookie.js';
+import { AILoginAssistant, type AILoginMonitorResult } from './AILoginAssistant.js';
+import { aiService } from '../ai/index.js';
 import type { PlatformType } from '@media-manager/shared';
 
 // CookieManager 使用静态方法
@@ -86,6 +88,7 @@ interface AccountInfo {
 
 interface LoginSession {
   id: string;
+  userId?: number;
   platform: PlatformType;
   browser: Browser;
   context: BrowserContext;
@@ -95,6 +98,8 @@ interface LoginSession {
   accountInfo?: AccountInfo;
   error?: string;
   createdAt: Date;
+  aiAssistant?: AILoginAssistant;
+  lastAIAnalysis?: AILoginMonitorResult;
 }
 
 class BrowserLoginService extends EventEmitter {
@@ -102,8 +107,10 @@ class BrowserLoginService extends EventEmitter {
 
   /**
    * 开始浏览器登录会话
+   * @param platform 平台类型
+   * @param userId 可选的用户ID,用于通过 WebSocket 推送状态
    */
-  async startLoginSession(platform: PlatformType): Promise<{ sessionId: string; message: string }> {
+  async startLoginSession(platform: PlatformType, userId?: number): Promise<{ sessionId: string; message: string }> {
     const config = PLATFORM_CONFIG[platform];
     if (!config) {
       throw new Error(`不支持的平台: ${platform}`);
@@ -133,6 +140,7 @@ class BrowserLoginService extends EventEmitter {
       // 创建会话
       const session: LoginSession = {
         id: sessionId,
+        userId,
         platform,
         browser,
         context,
@@ -146,9 +154,20 @@ class BrowserLoginService extends EventEmitter {
       // 导航到登录页
       await page.goto(config.loginUrl, { waitUntil: 'domcontentloaded' });
 
-      // 开始监控登录状态
+      // 创建 AI 登录助手(如果 AI 服务可用)
+      if (aiService.isAvailable()) {
+        session.aiAssistant = new AILoginAssistant(page, platform, sessionId);
+        logger.info(`[BrowserLogin] AI assistant created for session: ${sessionId}`);
+      }
+
+      // 开始监控登录状态(同时使用传统方法和 AI 方法)
       this.monitorLoginStatus(sessionId, config);
 
+      // 如果 AI 可用,启动 AI 监控
+      if (session.aiAssistant) {
+        this.startAIMonitoring(sessionId);
+      }
+
       // 设置超时(5分钟)
       setTimeout(() => {
         this.handleTimeout(sessionId);
@@ -158,7 +177,7 @@ class BrowserLoginService extends EventEmitter {
 
       return {
         sessionId,
-        message: `已打开 ${platform} 登录页面,请在浏览器中完成登录`,
+        message: `已打开 ${platform} 登录页面,请在浏览器中完成登录${aiService.isAvailable() ? '(AI 辅助已启用)' : ''}`,
       };
     } catch (error) {
       logger.error(`Failed to start login session for ${platform}:`, error);
@@ -203,8 +222,17 @@ class BrowserLoginService extends EventEmitter {
           url.includes(indicator)
         );
 
-        // 简化判断:URL跳转到成功页面就认为登录成功
+        // URL匹配成功标识后,还需要检查是否有二次校验弹框
         if (urlMatched) {
+          // 检查是否有二次校验弹框(抖音等平台可能需要短信验证或刷脸验证)
+          const hasSecondaryVerification = await this.checkSecondaryVerification(currentSession.page, currentSession.platform);
+          
+          if (hasSecondaryVerification) {
+            logger.info(`Secondary verification detected for ${sessionId}, waiting for user to complete...`);
+            // 继续等待,不判断为登录成功
+            return;
+          }
+
           logger.info(`Login success detected for ${sessionId}, URL: ${url}`);
           clearInterval(checkInterval);
           await this.handleLoginSuccess(sessionId);
@@ -216,13 +244,151 @@ class BrowserLoginService extends EventEmitter {
         if (currentSession && currentSession.status === 'pending') {
           currentSession.status = 'failed';
           currentSession.error = '浏览器已关闭';
-          this.emit('loginResult', { sessionId, status: 'failed', error: '浏览器已关闭' });
+          this.emit('loginResult', { sessionId, userId: currentSession.userId, status: 'failed', error: '浏览器已关闭' });
         }
       }
     }, 1000); // 检查间隔改为1秒,更快响应
   }
 
   /**
+   * 启动 AI 登录状态监控
+   */
+  private startAIMonitoring(sessionId: string): void {
+    const session = this.sessions.get(sessionId);
+    if (!session || !session.aiAssistant) return;
+
+    logger.info(`[BrowserLogin] Starting AI monitoring for session: ${sessionId}`);
+
+    session.aiAssistant.startMonitoring(
+      async (result: AILoginMonitorResult) => {
+        const currentSession = this.sessions.get(sessionId);
+        if (!currentSession || currentSession.status !== 'pending') {
+          currentSession?.aiAssistant?.stopMonitoring();
+          return;
+        }
+
+        // 保存最新的 AI 分析结果
+        currentSession.lastAIAnalysis = result;
+
+        // 发送 AI 分析状态更新(包含 userId 以便 WebSocket 推送)
+        this.emit('aiAnalysis', {
+          sessionId,
+          userId: currentSession.userId,
+          ...result,
+        });
+
+        logger.info(`[BrowserLogin] AI analysis for ${sessionId}:`, {
+          status: result.status,
+          isLoggedIn: result.analysis.isLoggedIn,
+          hasVerification: result.analysis.hasVerification,
+          pageDescription: result.analysis.pageDescription?.substring(0, 80),
+        });
+
+        // 如果 AI 检测到登录成功
+        if (result.status === 'logged_in' && result.analysis.isLoggedIn) {
+          logger.info(`[BrowserLogin] AI detected login success for session: ${sessionId}`);
+          // 停止 AI 监控(传统监控会继续运行来获取 Cookie)
+          currentSession.aiAssistant?.stopMonitoring();
+        }
+
+        // 如果检测到验证码
+        if (result.status === 'verification_needed') {
+          logger.info(`[BrowserLogin] AI detected verification for session: ${sessionId}`, {
+            type: result.analysis.verificationType,
+            description: result.analysis.verificationDescription,
+          });
+          // 通知前端显示验证码提示
+          this.emit('verificationNeeded', {
+            sessionId,
+            userId: currentSession.userId,
+            verificationType: result.analysis.verificationType,
+            description: result.analysis.verificationDescription,
+            suggestedAction: result.analysis.suggestedAction,
+          });
+        }
+      },
+      3000 // API 返回后等待 3 秒再进行下一次分析
+    );
+  }
+
+  /**
+   * 检查是否存在二次校验弹框
+   * 不同平台可能有不同的二次校验方式:短信验证码、刷脸验证等
+   */
+  private async checkSecondaryVerification(page: Page, platform: PlatformType): Promise<boolean> {
+    try {
+      // 抖音平台的二次校验检测
+      if (platform === 'douyin') {
+        // 1. 检查"身份验证"弹框
+        const verifyTitle = page.getByText('身份验证', { exact: false });
+        if (await verifyTitle.count() > 0 && await verifyTitle.first().isVisible().catch(() => false)) {
+          logger.info('[BrowserLogin] Found "身份验证" dialog');
+          return true;
+        }
+
+        // 2. 检查"接收短信验证码"选项
+        const smsOption = page.getByText('接收短信验证码', { exact: false });
+        if (await smsOption.count() > 0 && await smsOption.first().isVisible().catch(() => false)) {
+          logger.info('[BrowserLogin] Found "接收短信验证码" option');
+          return true;
+        }
+
+        // 3. 检查"手机刷脸验证"选项
+        const faceOption = page.getByText('手机刷脸验证', { exact: false });
+        if (await faceOption.count() > 0 && await faceOption.first().isVisible().catch(() => false)) {
+          logger.info('[BrowserLogin] Found "手机刷脸验证" option');
+          return true;
+        }
+
+        // 4. 检查"发送短信验证"选项
+        const sendSmsOption = page.getByText('发送短信验证', { exact: false });
+        if (await sendSmsOption.count() > 0 && await sendSmsOption.first().isVisible().catch(() => false)) {
+          logger.info('[BrowserLogin] Found "发送短信验证" option');
+          return true;
+        }
+
+        // 5. 检查"为保障账号安全"提示文本
+        const securityHint = page.getByText('为保障账号安全', { exact: false });
+        if (await securityHint.count() > 0 && await securityHint.first().isVisible().catch(() => false)) {
+          logger.info('[BrowserLogin] Found security verification hint');
+          return true;
+        }
+
+        // 6. 检查页面内容
+        const pageContent = await page.content().catch(() => '');
+        if ((pageContent.includes('身份验证') || pageContent.includes('为保障账号安全')) && 
+            (pageContent.includes('接收短信验证码') || pageContent.includes('手机刷脸验证') || pageContent.includes('发送短信验证'))) {
+          logger.info('[BrowserLogin] Found secondary verification in page content');
+          return true;
+        }
+      }
+
+      // 其他平台的二次校验检测可以在这里添加
+      // 通用检测:检查常见的验证弹框
+      const commonVerifySelectors = [
+        '[class*="verify-modal"]',
+        '[class*="identity-verify"]', 
+        '[class*="second-verify"]',
+        '[class*="security-verify"]',
+        '[class*="sms-verify"]',
+      ];
+
+      for (const selector of commonVerifySelectors) {
+        const element = page.locator(selector).first();
+        if (await element.count() > 0 && await element.isVisible().catch(() => false)) {
+          logger.info(`[BrowserLogin] Found verification modal: ${selector}`);
+          return true;
+        }
+      }
+
+      return false;
+    } catch (error) {
+      logger.error('[BrowserLogin] Error checking secondary verification:', error);
+      return false;
+    }
+  }
+
+  /**
    * 处理登录成功
    */
   private async handleLoginSuccess(sessionId: string): Promise<void> {
@@ -230,6 +396,9 @@ class BrowserLoginService extends EventEmitter {
     if (!session) return;
 
     try {
+      // 停止 AI 监控
+      session.aiAssistant?.stopMonitoring();
+
       // 等待页面完全稳定
       logger.info(`Waiting for page to stabilize for session: ${sessionId}`);
 
@@ -248,21 +417,58 @@ class BrowserLoginService extends EventEmitter {
       const cookies = await session.context.cookies();
       const cookieString = JSON.stringify(cookies);
 
-      // 立即关闭可见浏览器窗口
-      logger.info(`Closing visible browser for session: ${sessionId}`);
-      await this.closeSession(sessionId);
-
       // 更新状态为"正在获取账号信息"
       session.status = 'fetching';
       this.emit('loginResult', {
         sessionId,
+        userId: session.userId,
         status: 'fetching',
-        message: '登录成功,正在后台获取账号信息...',
+        message: '登录成功,正在获取账号信息...',
       });
 
-      // 使用无头浏览器后台获取账号信息
-      logger.info(`Fetching account info with headless browser for session: ${sessionId}`);
-      const accountInfo = await this.fetchAccountInfoHeadless(session.platform, cookies);
+      // 首先尝试使用 AI 在当前页面获取账号信息
+      let accountInfo: AccountInfo | null = null;
+      
+      if (session.aiAssistant && aiService.isAvailable()) {
+        logger.info(`[BrowserLogin] Trying to get account info with AI for session: ${sessionId}`);
+        const aiResult = await session.aiAssistant.getAccountInfoWithAI(3);
+        
+        if (aiResult.success && aiResult.accountInfo) {
+          logger.info(`[BrowserLogin] AI successfully extracted account info:`, aiResult.accountInfo);
+          
+          // 将 AI 提取的信息转换为 AccountInfo 格式
+          accountInfo = {
+            accountId: aiResult.accountInfo.accountId || '',
+            accountName: aiResult.accountInfo.accountName || '',
+            avatarUrl: '', // AI 无法直接提取 URL
+            fansCount: this.parseNumberString(aiResult.accountInfo.fansCount),
+            worksCount: this.parseNumberString(aiResult.accountInfo.worksCount),
+          };
+        } else if (aiResult.needNavigation && aiResult.navigationGuide) {
+          logger.info(`[BrowserLogin] AI suggests navigation: ${aiResult.navigationGuide.explanation}`);
+          // 发送导航建议给前端
+          this.emit('navigationSuggestion', {
+            sessionId,
+            guide: aiResult.navigationGuide,
+          });
+        }
+      }
+
+      // 如果 AI 没有获取到足够信息,关闭浏览器并使用无头浏览器获取
+      if (!accountInfo || !accountInfo.accountName) {
+        logger.info(`[BrowserLogin] AI info incomplete, closing browser and using headless for session: ${sessionId}`);
+        
+        // 关闭可见浏览器窗口
+        await this.closeSession(sessionId);
+
+        // 使用无头浏览器后台获取账号信息
+        logger.info(`Fetching account info with headless browser for session: ${sessionId}`);
+        accountInfo = await this.fetchAccountInfoHeadless(session.platform, cookies);
+      } else {
+        // AI 获取成功,关闭浏览器
+        logger.info(`[BrowserLogin] Closing visible browser for session: ${sessionId}`);
+        await this.closeSession(sessionId);
+      }
 
       // 加密存储
       const encryptedCookies = CookieManager.encrypt(cookieString);
@@ -276,6 +482,7 @@ class BrowserLoginService extends EventEmitter {
       // 发送事件
       this.emit('loginResult', {
         sessionId,
+        userId: session.userId,
         status: 'success',
         cookies: encryptedCookies,
         accountInfo,
@@ -285,8 +492,29 @@ class BrowserLoginService extends EventEmitter {
       logger.error(`Error handling login success for ${sessionId}:`, error);
       session.status = 'failed';
       session.error = String(error);
-      this.emit('loginResult', { sessionId, status: 'failed', error: String(error) });
+      this.emit('loginResult', { sessionId, userId: session.userId, status: 'failed', error: String(error) });
+    }
+  }
+
+  /**
+   * 解析数字字符串(支持中文单位)
+   */
+  private parseNumberString(str?: string): number {
+    if (!str) return 0;
+    
+    // 移除非数字字符(保留小数点)
+    const cleaned = str.replace(/[^0-9.万亿w]/gi, '');
+    const num = parseFloat(cleaned) || 0;
+    
+    // 处理中文单位
+    if (str.includes('万') || str.toLowerCase().includes('w')) {
+      return Math.floor(num * 10000);
+    }
+    if (str.includes('亿')) {
+      return Math.floor(num * 100000000);
     }
+    
+    return Math.floor(num);
   }
 
   /**
@@ -312,7 +540,7 @@ class BrowserLoginService extends EventEmitter {
     session.error = '登录超时';
 
     logger.warn(`Login session timeout: ${sessionId}`);
-    this.emit('loginResult', { sessionId, status: 'timeout', error: '登录超时' });
+    this.emit('loginResult', { sessionId, userId: session.userId, status: 'timeout', error: '登录超时' });
 
     await this.closeSession(sessionId);
   }
@@ -325,6 +553,7 @@ class BrowserLoginService extends EventEmitter {
     cookies?: string;
     accountInfo?: AccountInfo;
     error?: string;
+    userId?: number;
   } | null {
     const session = this.sessions.get(sessionId);
     if (!session) return null;
@@ -334,10 +563,19 @@ class BrowserLoginService extends EventEmitter {
       cookies: session.cookies,
       accountInfo: session.accountInfo,
       error: session.error,
+      userId: session.userId,
     };
   }
 
   /**
+   * 获取会话的用户ID
+   */
+  getSessionUserId(sessionId: string): number | undefined {
+    const session = this.sessions.get(sessionId);
+    return session?.userId;
+  }
+
+  /**
    * 取消登录会话
    */
   async cancelSession(sessionId: string): Promise<void> {
@@ -358,6 +596,12 @@ class BrowserLoginService extends EventEmitter {
     if (!session) return;
 
     try {
+      // 停止并销毁 AI 助手(异步清理截图)
+      if (session.aiAssistant) {
+        await session.aiAssistant.destroy();
+        session.aiAssistant = undefined;
+      }
+
       if (session.page) {
         await session.page.close().catch(() => { });
       }

+ 61 - 0
server/src/services/HeadlessBrowserService.ts

@@ -370,6 +370,67 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 访问平台后台页面并截图(用于 AI 分析)
+   * @param platform 平台类型
+   * @param cookies Cookie 数据
+   * @returns Base64 编码的截图,失败返回 null
+   */
+  async capturePageScreenshot(platform: PlatformType, cookies: CookieData[]): Promise<string | null> {
+    const browser = await chromium.launch({ headless: true });
+    
+    try {
+      const context = await browser.newContext({
+        viewport: { width: 1920, height: 1080 },
+        locale: 'zh-CN',
+        timezoneId: 'Asia/Shanghai',
+        userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+      });
+      
+      await context.addCookies(cookies);
+      const page = await context.newPage();
+      
+      const config = this.getPlatformConfig(platform);
+      
+      logger.info(`[Screenshot] Navigating to ${platform} home page: ${config.homeUrl}`);
+      
+      // 访问平台主页
+      await page.goto(config.homeUrl, {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+      
+      // 等待页面加载
+      await page.waitForTimeout(3000);
+      
+      const url = page.url();
+      logger.info(`[Screenshot] Current URL: ${url}`);
+      
+      // 截图
+      const screenshotBuffer = await page.screenshot({
+        type: 'jpeg',
+        quality: 80,
+        fullPage: false,
+      });
+      
+      const base64Screenshot = screenshotBuffer.toString('base64');
+      
+      await page.close();
+      await context.close();
+      await browser.close();
+      
+      logger.info(`[Screenshot] Captured screenshot for ${platform}, size: ${Math.round(base64Screenshot.length / 1024)}KB`);
+      
+      return base64Screenshot;
+    } catch (error) {
+      logger.error(`[Screenshot] Failed to capture screenshot for ${platform}:`, error);
+      try {
+        await browser.close();
+      } catch { /* ignore */ }
+      return null;
+    }
+  }
+
+  /**
    * 检查 Python 服务是否可用
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {

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

@@ -11,6 +11,10 @@ import type {
 import { wsManager } from '../websocket/index.js';
 import { DouyinAdapter } from '../automation/platforms/douyin.js';
 import { XiaohongshuAdapter } from '../automation/platforms/xiaohongshu.js';
+import { WeixinAdapter } from '../automation/platforms/weixin.js';
+import { KuaishouAdapter } from '../automation/platforms/kuaishou.js';
+import { BilibiliAdapter } from '../automation/platforms/bilibili.js';
+import { BaijiahaoAdapter } from '../automation/platforms/baijiahao.js';
 import { BasePlatformAdapter } from '../automation/platforms/base.js';
 import { logger } from '../utils/logger.js';
 import path from 'path';
@@ -36,6 +40,10 @@ export class PublishService {
     // 初始化平台适配器
     this.adapters.set('douyin', new DouyinAdapter());
     this.adapters.set('xiaohongshu', new XiaohongshuAdapter());
+    this.adapters.set('weixin_video', new WeixinAdapter());
+    this.adapters.set('kuaishou', new KuaishouAdapter());
+    this.adapters.set('bilibili', new BilibiliAdapter());
+    this.adapters.set('baijiahao', new BaijiahaoAdapter());
   }
   
   /**

+ 5 - 0
shared/src/constants/api.ts

@@ -112,6 +112,11 @@ export const WS_EVENTS = {
   CAPTCHA_REQUIRED: 'captcha:required',
   CAPTCHA_SUBMIT: 'captcha:submit',
   CAPTCHA_RESULT: 'captcha:result',
+  // 登录(AI 辅助)
+  LOGIN_AI_ANALYSIS: 'login:aiAnalysis',
+  LOGIN_VERIFICATION_NEEDED: 'login:verificationNeeded',
+  LOGIN_NAVIGATION_SUGGESTION: 'login:navigationSuggestion',
+  LOGIN_RESULT: 'login:result',
   // 评论
   COMMENT_NEW: 'comment:new',
   COMMENT_REPLIED: 'comment:replied',

+ 1 - 1
shared/src/constants/platforms.ts

@@ -63,7 +63,7 @@ export const PLATFORMS: Record<PlatformType, PlatformInfo> = {
     nameEn: 'WeChat Channels',
     icon: 'weixin',
     color: '#07C160',
-    loginUrl: 'https://channels.weixin.qq.com/',
+    loginUrl: 'https://channels.weixin.qq.com/platform/login',
     creatorUrl: 'https://channels.weixin.qq.com/platform',
     maxTitleLength: 30,
     maxDescriptionLength: 1000,

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません