Pārlūkot izejas kodu

fix(baijiahao): stabilize auth, works sync and ai publish flow

Ethanfly 1 dienu atpakaļ
vecāks
revīzija
b1f7d0ae88

+ 18 - 2
client/electron/main.ts

@@ -515,6 +515,17 @@ ipcMain.handle('get-webview-cookies', async (_event: unknown, partition: string,
   }
 });
 
+// 获取 webview 的全部 cookies(按 partition)
+ipcMain.handle('get-webview-all-cookies', async (_event: unknown, partition: string) => {
+  try {
+    const ses = session.fromPartition(partition);
+    return await ses.cookies.get({});
+  } catch (error) {
+    console.error('获取全部 cookies 失败:', error);
+    return [];
+  }
+});
+
 // 清除 webview 的 cookies
 ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: string) => {
   try {
@@ -530,6 +541,11 @@ ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: strin
 // 设置 webview 的 cookies
 ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string, cookies: Electron.CookiesSetDetails[]) => {
   try {
+    if (!Array.isArray(cookies) || cookies.length === 0) {
+      console.warn(`[Main] set-webview-cookies: cookies 为空, partition=${partition}`);
+      return false;
+    }
+
     console.log(`[Main] 设置 webview cookies, partition=${partition}, count=${cookies.length}`);
     const ses = session.fromPartition(partition);
 
@@ -547,7 +563,7 @@ ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string,
         };
 
         // 可选字段
-        if (cookie.expirationDate) {
+        if (typeof cookie.expirationDate === 'number' && Number.isFinite(cookie.expirationDate) && cookie.expirationDate > 0) {
           cookieToSet.expirationDate = cookie.expirationDate;
         }
         if (cookie.httpOnly !== undefined) {
@@ -584,7 +600,7 @@ ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string,
       console.error('[Main] 验证 Cookie 失败:', verifyError);
     }
 
-    return true;
+    return successCount > 0;
   } catch (error) {
     console.error('[Main] 设置 cookies 失败:', error);
     return false;

+ 3 - 0
client/electron/preload.ts

@@ -33,6 +33,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
   // Webview Cookie 操作
   getWebviewCookies: (partition: string, url: string) =>
     ipcRenderer.invoke('get-webview-cookies', partition, url),
+  getWebviewAllCookies: (partition: string) =>
+    ipcRenderer.invoke('get-webview-all-cookies', partition),
   clearWebviewCookies: (partition: string) =>
     ipcRenderer.invoke('clear-webview-cookies', partition),
   setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) =>
@@ -88,6 +90,7 @@ declare global {
       selectFolder: () => Promise<string | null>;
       showNotification: (title: string, body: string) => void;
       getWebviewCookies: (partition: string, url: string) => Promise<Electron.Cookie[]>;
+      getWebviewAllCookies: (partition: string) => Promise<Electron.Cookie[]>;
       clearWebviewCookies: (partition: string) => Promise<boolean>;
       setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) => Promise<boolean>;
       captureWebviewPage: (webContentsId: number) => Promise<string | null>;

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

@@ -15,10 +15,14 @@ declare module 'vue' {
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
+    ElCascader: typeof import('element-plus/es')['ElCascader']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -46,6 +50,8 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 165 - 16
client/src/components/BrowserTab.vue

@@ -2593,6 +2593,31 @@ async function collectBaijiahaoAccountInfo() {
   };
   
   console.log('[百家号] 完整账号信息:', accountInfo.value);
+  // 关键:在完整流程结束后再抓一次 Cookie,避免过早抓取导致关键 Cookie 不全
+  console.log('[百家号] 步骤4: 刷新并保存最终 Cookie...');
+  try {
+    const currentUrl = webview.getURL?.() || '';
+    if (!currentUrl.includes('/builder/rc/home')) {
+      webview.loadURL('https://baijiahao.baidu.com/builder/rc/home');
+      await waitForPageLoad(webview, 5000);
+    }
+    await sleep(1500);
+    await getCookieData();
+
+    try {
+      const parsed = JSON.parse(cookieData.value || '[]');
+      if (Array.isArray(parsed)) {
+        const names = parsed.map((c: any) => c?.name).filter(Boolean);
+        const mustHave = ['BDUSS', 'STOKEN', 'bjhStoken', 'devStoken'];
+        const present = mustHave.filter(name => names.includes(name));
+        console.log(`[百家号] 最终 Cookie 数量: ${parsed.length}, 关键 Cookie: ${present.join(', ') || '无'}`);
+      }
+    } catch {
+      // ignore
+    }
+  } catch (error) {
+    console.warn('[百家号] 最终 Cookie 刷新失败,继续保存当前结果:', error);
+  }
   showSuccessMessage();
 }
 
@@ -3732,15 +3757,82 @@ async function getCookies(): Promise<Electron.Cookie[]> {
   
   const platformInfo = PLATFORMS[platform.value];
   if (!platformInfo) return [];
-  
-  // 获取登录域名的 cookies
-  const url = platformInfo.loginUrl;
-  return await window.electronAPI.getWebviewCookies(webviewPartition.value, url);
+
+  // 百家号需要聚合多个 URL 下的 Cookie,避免仅登录页路径导致关键 Cookie 丢失
+  if (platform.value === 'baijiahao') {
+    // 优先走 partition 全量 Cookie,避免 URL/path 过滤导致 Cookie 不全
+    if (window.electronAPI.getWebviewAllCookies) {
+      try {
+        const allCookies = await window.electronAPI.getWebviewAllCookies(webviewPartition.value);
+        const baiduCookies = allCookies.filter(cookie => {
+          const domain = String(cookie.domain || '').toLowerCase();
+          return domain.includes('baidu.com');
+        });
+
+        if (baiduCookies.length > 0) {
+          const merged = new Map<string, Electron.Cookie>();
+          for (const cookie of baiduCookies) {
+            const key = `${cookie.name}|${cookie.domain}|${cookie.path}`;
+            merged.set(key, cookie);
+          }
+          console.log(`[getCookies] 百家号从 partition 全量获取到 ${merged.size} 个 baidu Cookie`);
+          return Array.from(merged.values());
+        }
+      } catch (error) {
+        console.warn('[getCookies] getWebviewAllCookies 失败,回退 URL 聚合:', error);
+      }
+    }
+
+    // 回退:按多个 URL 聚合
+    const urls = [
+      platformInfo.creatorUrl,
+      platformInfo.loginUrl,
+      'https://baijiahao.baidu.com/builder/rc/content',
+      'https://passport.baidu.com/',
+      'https://hm.baidu.com/',
+      'https://www.baidu.com/',
+    ];
+
+    const cookieLists = await Promise.all(
+      urls.map(async (url) => {
+        try {
+          return await window.electronAPI.getWebviewCookies(webviewPartition.value, url);
+        } catch (error) {
+          console.warn(`[getCookies] 获取 Cookie 失败: ${url}`, error);
+          return [] as Electron.Cookie[];
+        }
+      })
+    );
+
+    const merged = new Map<string, Electron.Cookie>();
+    for (const list of cookieLists) {
+      for (const cookie of list) {
+        const key = `${cookie.name}|${cookie.domain}|${cookie.path}`;
+        merged.set(key, cookie);
+      }
+    }
+    return Array.from(merged.values());
+  }
+
+  // 其他平台按登录 URL 获取即可
+  return await window.electronAPI.getWebviewCookies(webviewPartition.value, platformInfo.loginUrl);
 }
 
 // 格式化 cookies 为字符串
 function formatCookies(cookies: Electron.Cookie[]): string {
-  return cookies.map(c => `${c.name}=${c.value}`).join('; ');
+  // 保存完整 cookie 属性,避免后台再次注入时丢失 domain/path/sameSite 等信息
+  const normalized = cookies.map(cookie => ({
+    name: cookie.name,
+    value: cookie.value,
+    domain: cookie.domain,
+    path: cookie.path || '/',
+    expires: cookie.expirationDate,
+    httpOnly: cookie.httpOnly,
+    secure: cookie.secure,
+    sameSite: cookie.sameSite,
+  }));
+
+  return JSON.stringify(normalized);
 }
 
 // 手动检测登录(使用 URL 检测)
@@ -3887,12 +3979,35 @@ async function setPresetCookies(cookieDataStr: string) {
   }
   
   try {
+    type PresetCookie = {
+      name: string;
+      value: string;
+      domain?: string;
+      path?: string;
+      expires?: number | string;
+      expirationDate?: number | string;
+      httpOnly?: boolean;
+      secure?: boolean;
+      sameSite?: string;
+    };
+
     // 尝试解析 JSON 格式的 cookie
-    let cookieList: Array<{ name: string; value: string; domain?: string; path?: string; expires?: number }>;
+    let cookieList: PresetCookie[];
     
     try {
-      cookieList = JSON.parse(cookieDataStr);
-      console.log('[BrowserTab] 成功解析 JSON 格式的 Cookie');
+      const parsed = JSON.parse(cookieDataStr) as PresetCookie[] | { cookies?: PresetCookie[] } | PresetCookie;
+      if (Array.isArray(parsed)) {
+        cookieList = parsed;
+        console.log('[BrowserTab] 成功解析 JSON 数组格式的 Cookie');
+      } else if (parsed && Array.isArray((parsed as { cookies?: PresetCookie[] }).cookies)) {
+        cookieList = (parsed as { cookies: PresetCookie[] }).cookies;
+        console.log('[BrowserTab] 成功解析 storageState 格式的 Cookie');
+      } else if (parsed && typeof parsed === 'object' && 'name' in parsed && 'value' in parsed) {
+        cookieList = [parsed as PresetCookie];
+        console.log('[BrowserTab] 成功解析单个 Cookie 对象');
+      } else {
+        throw new Error('Unsupported cookie JSON format');
+      }
     } catch {
       // 如果不是 JSON,尝试解析 "name=value; name2=value2" 格式
       console.log('[BrowserTab] JSON 解析失败,尝试解析字符串格式');
@@ -3924,18 +4039,46 @@ async function setPresetCookies(cookieDataStr: string) {
     const cookieUrl = platform.value === 'baijiahao' 
       ? 'https://www.baidu.com' 
       : targetUrl;
+
+    // 根据 cookie 域名选择更匹配的 URL(避免 URL 与 domain 不匹配导致设置失败)
+    const resolveCookieUrl = (domain?: string): string => {
+      if (platform.value !== 'baijiahao') {
+        return cookieUrl;
+      }
+
+      const normalizedDomain = String(domain || '').replace(/^\./, '').toLowerCase();
+      if (!normalizedDomain) return cookieUrl;
+
+      if (normalizedDomain.includes('baijiahao.baidu.com')) return 'https://baijiahao.baidu.com';
+      if (normalizedDomain.includes('passport.baidu.com')) return 'https://passport.baidu.com';
+      if (normalizedDomain.includes('hm.baidu.com')) return 'https://hm.baidu.com';
+      if (normalizedDomain.endsWith('baidu.com')) return 'https://www.baidu.com';
+
+      return cookieUrl;
+    };
     
     console.log(`[BrowserTab] 设置 Cookie, platform=${platform.value}, targetUrl=${targetUrl}, cookieUrl=${cookieUrl}, domain=${rootDomain}`);
     console.log(`[BrowserTab] Cookie 列表:`, cookieList.slice(0, 3).map(c => ({ name: c.name, domain: c.domain })));
     
+    // 提取有效的过期时间(秒);<= 0 视为会话 Cookie(不应当作过期)
+    const getCookieExpires = (cookie: PresetCookie): number | undefined => {
+      const raw = cookie.expires ?? cookie.expirationDate;
+      if (raw === undefined || raw === null || raw === '') return undefined;
+      const expires = Number(raw);
+      if (!Number.isFinite(expires)) return undefined;
+      if (expires <= 0) return undefined; // Playwright/session cookie 常用 -1
+      return expires;
+    };
+
     // 过滤掉已过期的 Cookie
     const now = Date.now() / 1000;
     const validCookies = cookieList.filter(cookie => {
-      if (cookie.expires && cookie.expires < now) {
+      const expires = getCookieExpires(cookie);
+      if (expires && expires < now) {
         console.warn(`[BrowserTab] Cookie ${cookie.name} 已过期,跳过`);
         return false;
       }
-      return true;
+      return !!cookie.name && cookie.value !== undefined && cookie.value !== null;
     });
     
     if (validCookies.length === 0) {
@@ -3961,7 +4104,7 @@ async function setPresetCookies(cookieDataStr: string) {
       }
       
       const electronCookie: any = {
-        url: cookieUrl, // 使用正确的 URL
+        url: resolveCookieUrl(cookieDomain), // 使用与 domain 匹配的 URL
         name: cookie.name,
         value: cookie.value,
         domain: cookieDomain,
@@ -3969,8 +4112,9 @@ async function setPresetCookies(cookieDataStr: string) {
       };
       
       // 添加可选字段
-      if (cookie.expires) {
-        electronCookie.expirationDate = cookie.expires;
+      const expires = getCookieExpires(cookie);
+      if (expires) {
+        electronCookie.expirationDate = expires;
       }
       if ((cookie as any).httpOnly !== undefined) {
         electronCookie.httpOnly = (cookie as any).httpOnly;
@@ -3981,11 +4125,16 @@ async function setPresetCookies(cookieDataStr: string) {
       if ((cookie as any).sameSite) {
         // 转换 sameSite 值
         const sameSite = (cookie as any).sameSite;
-        if (sameSite === 'None') {
+        const normalizedSameSite = String(sameSite).toLowerCase();
+        if (normalizedSameSite === 'none' || normalizedSameSite === 'no_restriction') {
           electronCookie.sameSite = 'no_restriction';
-        } else if (sameSite === 'Lax' || sameSite === 'lax') {
+          // no_restriction 需要 secure=true,否则部分 Chromium 版本会拒绝设置
+          if (electronCookie.secure !== true) {
+            electronCookie.secure = true;
+          }
+        } else if (normalizedSameSite === 'lax') {
           electronCookie.sameSite = 'lax';
-        } else if (sameSite === 'Strict' || sameSite === 'strict') {
+        } else if (normalizedSameSite === 'strict') {
           electronCookie.sameSite = 'strict';
         }
       }

+ 36 - 6
client/src/views/Publish/index.vue

@@ -495,6 +495,23 @@ const editForm = reactive({
   publishProxyRegionPath: [] as string[],
 });
 
+function getSelectableAccountIds(): Set<number> {
+  return new Set(
+    accounts.value
+      .filter(account => account.status === 'active')
+      .map(account => Number(account.id))
+      .filter(id => Number.isFinite(id))
+  );
+}
+
+function sanitizeTargetAccounts(targetAccounts: Array<number | string | null | undefined>): number[] {
+  const selectableIds = getSelectableAccountIds();
+  const normalized = (targetAccounts || [])
+    .map(id => Number(id))
+    .filter(id => Number.isFinite(id) && selectableIds.has(id));
+  return Array.from(new Set(normalized));
+}
+
 async function loadSystemConfig() {
   try {
     await loadPublishProxyRegions();
@@ -638,16 +655,21 @@ function isScreenshotPendingError(errorMessage: string | null | undefined): bool
 
 // 是否可以使用有头浏览器重新发布(目前主要用于小红书发布失败场景)
 function canRetryWithHeadful(row: { platform: string; status: string | null | undefined }): boolean {
-  return row.platform === 'xiaohongshu' && row.status === 'failed';
+  if (row.status !== 'failed') return false;
+  return row.platform === 'xiaohongshu' || row.platform === 'baijiahao';
 }
 
 // 打开查看截图(小红书等平台暂打开创作者中心,用户可自行查看发布状态)
 function openScreenshotView(row: { platform: string; accountId: number }) {
   if (row.platform === 'xiaohongshu') {
     window.open('https://creator.xiaohongshu.com/publish/publish', '_blank', 'noopener,noreferrer');
-  } else {
-    ElMessage.info('请前往对应平台查看发布状态');
+    return;
+  }
+  if (row.platform === 'baijiahao') {
+    window.open('https://baijiahao.baidu.com/builder/rc/content', '_blank', 'noopener,noreferrer');
+    return;
   }
+  ElMessage.info('请前往对应平台查看发布状态');
 }
 
 // 是否存在待确认的发布结果
@@ -855,13 +877,18 @@ function openEditDialog() {
   editForm.title = currentTask.value.title || '';
   editForm.description = currentTask.value.description || '';
   editForm.tags = [...(currentTask.value.tags || [])];
-  editForm.targetAccounts = [...(currentTask.value.targetAccounts || [])];
+  const originalTargetAccounts = [...(currentTask.value.targetAccounts || [])];
+  editForm.targetAccounts = sanitizeTargetAccounts(originalTargetAccounts);
   editForm.scheduledAt = null;
   editForm.usePublishProxy = Boolean(currentTask.value.publishProxy?.enabled);
   editForm.publishProxyRegionPath = Array.isArray((currentTask.value.publishProxy as any)?.regionPath)
     ? normalizePublishProxyCityRegionPath((currentTask.value.publishProxy as any).regionPath)
     : [];
 
+  if (originalTargetAccounts.length > 0 && editForm.targetAccounts.length !== originalTargetAccounts.length) {
+    ElMessage.warning('原任务中的部分账号已不可用,已自动移除,请重新选择可用账号');
+  }
+
   const regionCode = String((currentTask.value.publishProxy as any)?.regionCode || '').trim();
   if (editForm.usePublishProxy && !editForm.publishProxyRegionPath.length && regionCode && publishProxyRegions.value.length > 0) {
     const inferred = findRegionPathByCode(publishProxyRegions.value, regionCode);
@@ -880,7 +907,10 @@ function handleEditFileChange(file: UploadFile) {
 }
 
 async function handleRepublish() {
-  if (!editForm.title || editForm.targetAccounts.length === 0) {
+  const targetAccounts = sanitizeTargetAccounts(editForm.targetAccounts);
+  editForm.targetAccounts = targetAccounts;
+
+  if (!editForm.title || targetAccounts.length === 0) {
     ElMessage.warning('请填写完整信息');
     return;
   }
@@ -916,7 +946,7 @@ async function handleRepublish() {
       title: editForm.title,
       description: editForm.description,
       tags: editForm.tags,
-      targetAccounts: editForm.targetAccounts,
+      targetAccounts,
       scheduledAt: editForm.scheduledAt ? editForm.scheduledAt.toISOString() : null,
       publishProxy: proxy
         ? {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1793 - 54
server/python/platforms/baijiahao.py


+ 73 - 7
server/python/platforms/base.py

@@ -216,7 +216,69 @@ class BasePublisher(ABC):
                     'path': '/'
                 })
         return cookies
-    
+
+    @staticmethod
+    def _normalize_same_site(value: Any) -> Optional[str]:
+        """将不同来源的 sameSite 值转换为 Playwright 接受的值。"""
+        if value is None:
+            return None
+        v = str(value).strip().lower()
+        if not v:
+            return None
+        mapping = {
+            "strict": "Strict",
+            "lax": "Lax",
+            "none": "None",
+            "no_restriction": "None",  # Electron
+            "unspecified": None,       # Electron: 不传该字段
+        }
+        return mapping.get(v, None)
+
+    @classmethod
+    def _sanitize_cookie_for_playwright(cls, cookie: Dict[str, Any], default_domain: str) -> Optional[Dict[str, Any]]:
+        """清洗 cookie 字段,避免 BrowserContext.add_cookies 参数校验失败。"""
+        if not isinstance(cookie, dict):
+            return None
+
+        name = str(cookie.get('name') or '').strip()
+        if not name:
+            return None
+
+        cleaned: Dict[str, Any] = {
+            'name': name,
+            'value': str(cookie.get('value') or ''),
+        }
+
+        url = str(cookie.get('url') or '').strip()
+        if url:
+            cleaned['url'] = url
+        else:
+            domain = str(cookie.get('domain') or '').strip() or default_domain
+            if not domain:
+                return None
+            cleaned['domain'] = domain
+            cleaned['path'] = str(cookie.get('path') or '/')
+
+        if 'httpOnly' in cookie:
+            cleaned['httpOnly'] = bool(cookie.get('httpOnly'))
+        if 'secure' in cookie:
+            cleaned['secure'] = bool(cookie.get('secure'))
+
+        expires_raw = cookie.get('expires', cookie.get('expirationDate'))
+        if expires_raw not in (None, '', 0):
+            try:
+                expires_val = float(expires_raw)
+                if expires_val > 0:
+                    cleaned['expires'] = expires_val
+            except Exception:
+                pass
+
+        same_site = cls._normalize_same_site(cookie.get('sameSite', cookie.get('same_site')))
+        if same_site:
+            cleaned['sameSite'] = same_site
+
+        return cleaned
+
     @staticmethod
     def cookies_to_string(cookies: list) -> str:
         """将 cookie 列表转换为字符串"""
@@ -246,13 +308,17 @@ class BasePublisher(ABC):
         """设置 cookies"""
         if not self.context:
             raise Exception("Browser context not initialized")
-        
-        # 设置默认域名
+
+        sanitized: List[Dict[str, Any]] = []
         for cookie in cookies:
-            if 'domain' not in cookie or not cookie['domain']:
-                cookie['domain'] = self.cookie_domain
-        
-        await self.context.add_cookies(cookies)
+            cleaned = self._sanitize_cookie_for_playwright(cookie, self.cookie_domain)
+            if cleaned:
+                sanitized.append(cleaned)
+
+        if not sanitized:
+            raise Exception("没有可用的 Cookie(清洗后为空)")
+
+        await self.context.add_cookies(sanitized)
     
     async def close_browser(self):
         """关闭浏览器"""

+ 6 - 2
server/src/automation/platforms/baijiahao.ts

@@ -16,6 +16,7 @@ import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigServi
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
+const PYTHON_PUBLISH_TIMEOUT_MS = 30 * 60 * 1000;
 
 /**
  * 百家号平台适配器
@@ -301,14 +302,14 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
           publish_account_id: (extra as any).publishAccountId,
           proxy: (extra as any).publishProxy || null,
           title: params.title,
-          description: params.description || params.title,
+          description: params.description ?? '',
           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分钟超时
+        signal: AbortSignal.timeout(PYTHON_PUBLISH_TIMEOUT_MS), // 百家号上传/发布链路较慢,放宽超时
       });
 
       const result = await response.json();
@@ -317,6 +318,9 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
       // 使用通用的 AI 辅助处理方法
       return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
     } catch (error) {
+      if (error instanceof Error && error.name === 'TimeoutError') {
+        throw new Error('Python 发布超时(30分钟),发布可能仍在平台侧处理中,请稍后到百家号后台确认');
+      }
       logger.error('[Baijiahao Python] Publish failed:', error);
       throw error;
     }

+ 10 - 1
server/src/services/AccountService.ts

@@ -149,9 +149,18 @@ export class AccountService {
       const parsed = JSON.parse(decryptedCookies);
       if (Array.isArray(parsed) && parsed.length > 0) {
         logger.info(`[AccountService] Cookie 格式验证通过,共 ${parsed.length} 个 Cookie`);
+        const cookieNames = parsed.map((c: any) => c?.name).filter(Boolean);
         // 记录关键 Cookie(用于调试)
-        const keyNames = parsed.slice(0, 3).map((c: any) => c.name).join(', ');
+        const keyNames = cookieNames.slice(0, 5).join(', ');
         logger.info(`[AccountService] 关键 Cookie: ${keyNames}`);
+
+        if (account.platform === 'baijiahao') {
+          const required = ['BDUSS', 'STOKEN', 'bjhStoken', 'devStoken', 'BAIDUID'];
+          const present = required.filter(name => cookieNames.includes(name));
+          logger.info(
+            `[AccountService] 百家号关键 Cookie 命中: ${present.length}/${required.length} -> ${present.join(', ') || '无'}`
+          );
+        }
       }
     } catch {
       logger.warn(`[AccountService] Cookie 不是 JSON 格式,可能是字符串格式`);

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

@@ -720,7 +720,10 @@ class HeadlessBrowserService {
     const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     // 抖音 work_list 接口 count 最大 20,需与创作者中心一致
-    const pageSize = platform === 'xiaohongshu' ? 20 : platform === 'douyin' ? 20 : 50;
+    const pageSize =
+      platform === 'xiaohongshu' || platform === 'douyin' || platform === 'baijiahao'
+        ? 20
+        : 50;
     let maxPages = 30;
     const allWorks: WorkItem[] = [];
     const seenIds = new Set<string>();

+ 66 - 14
server/src/services/PublishService.ts

@@ -31,6 +31,8 @@ interface GetTasksParams {
   status?: string;
 }
 
+const PYTHON_HEADFUL_RETRY_TIMEOUT_MS = 30 * 60 * 1000;
+
 export class PublishService {
   private taskRepository = AppDataSource.getRepository(PublishTask);
   private resultRepository = AppDataSource.getRepository(PublishResult);
@@ -61,6 +63,34 @@ export class PublishService {
     return adapter;
   }
 
+  private resolvePublishText(task: PublishTask, platform: PlatformType): { title: string; description: string } {
+    const taskTitle = String(task.title || '').trim();
+    const taskDescription = task.description == null ? '' : String(task.description).trim();
+
+    let title = taskTitle;
+    let description = taskDescription;
+
+    const platformConfig = Array.isArray(task.platformConfigs)
+      ? task.platformConfigs.find((cfg: any) => String(cfg?.platform || '') === String(platform))
+      : undefined;
+
+    if (platformConfig) {
+      const cfgTitle = typeof platformConfig.title === 'string' ? platformConfig.title.trim() : '';
+      const hasCfgDescription = Object.prototype.hasOwnProperty.call(platformConfig, 'description');
+      const cfgDescription = typeof platformConfig.description === 'string' ? platformConfig.description.trim() : '';
+
+      if (cfgTitle) {
+        title = cfgTitle;
+      }
+
+      if (hasCfgDescription) {
+        description = cfgDescription;
+      }
+    }
+
+    return { title, description };
+  }
+
   async getTasks(userId: number, params: GetTasksParams): Promise<PaginatedData<PublishTaskType>> {
     const { page, pageSize, status } = params;
     const skip = (page - 1) * pageSize;
@@ -187,6 +217,7 @@ export class PublishService {
     let successCount = 0;
     let failCount = 0;
     const totalAccounts = results.length;
+    const successAccountIds = new Set<number>();
 
     let publishProxyExtra: Awaited<ReturnType<PublishService['buildPublishProxyExtra']>> = null;
     try {
@@ -342,12 +373,26 @@ export class PublishService {
         };
 
         // 执行发布
+        const { title: resolvedTitle, description: resolvedDescription } = this.resolvePublishText(
+          task,
+          account.platform as PlatformType
+        );
+
+        if (!resolvedTitle) {
+          await this.resultRepository.update(result.id, {
+            status: 'failed',
+            errorMessage: '发布标题为空,请在发布管理中填写标题后重试',
+          });
+          failCount++;
+          continue;
+        }
+
         const publishResult = await adapter.publishVideo(
           decryptedCookies,
           {
             videoPath,
-            title: task.title || '',
-            description: task.description || undefined,
+            title: resolvedTitle,
+            description: resolvedDescription,
             coverPath: task.coverPath || undefined,
             tags: task.tags || undefined,
             extra: {
@@ -379,6 +424,7 @@ export class PublishService {
             publishedAt: new Date(),
           });
           successCount++;
+          successAccountIds.add(account.id);
 
           wsManager.sendToUser(userId, WS_EVENTS.PUBLISH_PROGRESS, {
             taskId,
@@ -439,14 +485,6 @@ export class PublishService {
 
     // 发布成功后,自动创建同步作品任务
     if (successCount > 0) {
-      // 收集成功发布的账号ID
-      const successAccountIds = new Set<number>();
-      for (const result of results) {
-        if (result.status === 'success') {
-          successAccountIds.add(result.accountId);
-        }
-      }
-
       // 为每个成功的账号创建同步任务
       for (const accountId of successAccountIds) {
         const account = await this.accountRepository.findOne({ where: { id: accountId } });
@@ -592,6 +630,15 @@ export class PublishService {
         ? videoPath
         : path.resolve(process.cwd(), videoPath);
 
+      const { title: resolvedTitle, description: resolvedDescription } = this.resolvePublishText(
+        task,
+        account.platform as PlatformType
+      );
+
+      if (!resolvedTitle) {
+        throw new Error('发布标题为空,请在发布管理中填写标题后重试');
+      }
+
       const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
@@ -602,14 +649,14 @@ export class PublishService {
           publish_task_id: taskId,
           publish_account_id: accountId,
           proxy: publishProxyExtra,
-          title: task.title,
-          description: task.description || task.title,
+          title: resolvedTitle,
+          description: resolvedDescription,
           video_path: absoluteVideoPath,
           cover_path: task.coverPath ? path.resolve(process.cwd(), task.coverPath) : undefined,
           tags: task.tags || [],
           headless: false,  // 关键:使用有头浏览器模式
         }),
-        signal: AbortSignal.timeout(600000), // 10分钟超时
+        signal: AbortSignal.timeout(PYTHON_HEADFUL_RETRY_TIMEOUT_MS),
       });
 
       const result = await response.json();
@@ -656,7 +703,12 @@ export class PublishService {
         return { success: false, message: '发布失败', error: errorMsg };
       }
     } catch (error) {
-      const errorMsg = error instanceof Error ? error.message : '发布失败';
+      const errorMsg =
+        error instanceof Error && error.name === 'TimeoutError'
+          ? '有头发布超时(30分钟),发布可能仍在平台侧处理中,请稍后到平台后台确认'
+          : error instanceof Error
+            ? error.message
+            : '发布失败';
       await this.resultRepository.update(publishResult.id, {
         status: 'failed',
         errorMessage: errorMsg,

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels