Explorar el Código

fix 小红书,百家号

Ethanfly hace 5 horas
padre
commit
42fefc5002

+ 12 - 0
client/dist-electron/main.js

@@ -198,4 +198,16 @@ ipcMain.handle("clear-webview-cookies", async (_event, partition) => {
     return false;
   }
 });
+ipcMain.handle("set-webview-cookies", async (_event, partition, cookies) => {
+  try {
+    const ses = session.fromPartition(partition);
+    for (const cookie of cookies) {
+      await ses.cookies.set(cookie);
+    }
+    return true;
+  } catch (error) {
+    console.error("设置 cookies 失败:", error);
+    return false;
+  }
+});
 //# sourceMappingURL=main.js.map

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
client/dist-electron/main.js.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2 - 1
client/dist-electron/preload.js


+ 15 - 0
client/electron/main.ts

@@ -263,3 +263,18 @@ ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: strin
     return false;
   }
 });
+
+// 设置 webview 的 cookies
+ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string, cookies: Electron.CookiesSetDetails[]) => {
+  try {
+    const ses = session.fromPartition(partition);
+    // 逐个设置 cookie
+    for (const cookie of cookies) {
+      await ses.cookies.set(cookie);
+    }
+    return true;
+  } catch (error) {
+    console.error('设置 cookies 失败:', error);
+    return false;
+  }
+});

+ 3 - 0
client/electron/preload.ts

@@ -31,6 +31,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
     ipcRenderer.invoke('get-webview-cookies', partition, url),
   clearWebviewCookies: (partition: string) =>
     ipcRenderer.invoke('clear-webview-cookies', partition),
+  setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) =>
+    ipcRenderer.invoke('set-webview-cookies', partition, cookies),
 });
 
 // 类型声明
@@ -50,6 +52,7 @@ declare global {
       showNotification: (title: string, body: string) => void;
       getWebviewCookies: (partition: string, url: string) => Promise<Electron.Cookie[]>;
       clearWebviewCookies: (partition: string) => Promise<boolean>;
+      setWebviewCookies: (partition: string, cookies: Electron.CookiesSetDetails[]) => Promise<boolean>;
     };
   }
 }

+ 5 - 0
client/src/api/accounts.ts

@@ -124,4 +124,9 @@ export const accountsApi = {
   }> {
     return request.post('/api/accounts/verify-cookie', { platform, cookieData });
   },
+
+  // 获取账号 Cookie(用于打开后台)
+  getAccountCookie(id: number): Promise<{ cookieData: string }> {
+    return request.get(`/api/accounts/${id}/cookie`);
+  },
 };

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

@@ -62,6 +62,7 @@ declare module 'vue' {
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    Setting: typeof import('@element-plus/icons-vue')['Setting']
     TaskProgressDialog: typeof import('./components/TaskProgressDialog.vue')['default']
   }
   export interface ComponentCustomProperties {

+ 330 - 12
client/src/components/BrowserTab.vue

@@ -14,7 +14,7 @@
           <span class="url-text">{{ currentUrl }}</span>
         </div>
       </div>
-      <div class="toolbar-right">
+      <div v-if="!isAdminMode" class="toolbar-right">
         <el-tag :type="statusType" size="small">
           {{ statusText }}
         </el-tag>
@@ -40,8 +40,9 @@
     <!-- 内嵌浏览器 -->
     <div class="browser-content">
       <webview
+        v-if="webviewReady"
         ref="webviewRef"
-        :src="initialUrl"
+        :src="webviewSrc"
         :partition="webviewPartition"
         class="embedded-browser"
         allowpopups
@@ -53,8 +54,8 @@
         @dom-ready="handleDomReady"
       />
       
-      <!-- 登录成功遮罩 -->
-      <div v-if="loginStatus === 'success'" class="success-overlay">
+      <!-- 登录成功遮罩(管理模式下不显示) -->
+      <div v-if="loginStatus === 'success' && !isAdminMode" class="success-overlay">
         <div class="success-content">
           <el-icon class="success-icon"><CircleCheck /></el-icon>
           <h3>登录成功!</h3>
@@ -78,7 +79,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
 import { ArrowLeft, ArrowRight, Refresh, Loading, Lock, CircleCheck } from '@element-plus/icons-vue';
 import { ElMessage } from 'element-plus';
 import { accountsApi } from '@/api/accounts';
@@ -106,6 +107,8 @@ const canGoForward = ref(false);
 const saving = ref(false);
 const checking = ref(false);
 const loginStatus = ref<'pending' | 'checking' | 'success' | 'failed'>('pending');
+const webviewSrc = ref('about:blank'); // 初始为空白页,等 Cookie 设置完再导航
+const webviewReady = ref(false); // 控制 webview 是否渲染
 const accountInfo = ref<{
   accountId: string;
   accountName: string;
@@ -123,7 +126,15 @@ let hasShownSuccessMessage = false; // 防止重复显示成功消息
 // 计算属性
 const platform = computed(() => props.tab.browserData?.platform as PlatformType);
 
+// 是否是管理后台模式(从后台按钮打开,不需要登录检测和保存账号)
+const isAdminMode = computed(() => !!props.tab.browserData?.isAdminMode);
+
 const initialUrl = computed(() => {
+  // 优先使用 browserData 中指定的 URL
+  if (props.tab.browserData?.url) {
+    return props.tab.browserData.url;
+  }
+  // 否则使用平台默认登录 URL
   const platformInfo = PLATFORMS[platform.value];
   return platformInfo?.loginUrl || 'about:blank';
 });
@@ -176,9 +187,24 @@ function handleNavigate(event: Electron.DidNavigateEvent) {
   updateNavigation();
   
   // 页面跳转后立即检测一次登录状态(加快响应)
-  if (loginStatus.value !== 'success') {
+  // 管理模式下跳过检测
+  if (!isAdminMode.value && loginStatus.value !== 'success') {
     setTimeout(() => checkLoginSilently(), 500);
   }
+  
+  // 百家号特殊处理:如果已经登录成功但账号信息是默认值,尝试重新获取
+  if (platform.value === 'baijiahao' && 
+      loginStatus.value === 'success' && 
+      accountInfo.value?.accountName === '百家号账号' &&
+      event.url.includes('baijiahao.baidu.com/builder')) {
+    console.log('[BrowserTab] 百家号跳转到后台页面,尝试重新获取账号信息');
+    setTimeout(async () => {
+      const info = await fetchBaijiahaoAccountInfo();
+      if (info && info.accountName !== '百家号账号') {
+        accountInfo.value = info;
+      }
+    }, 1500);
+  }
 }
 
 function handleNavigateInPage(event: Electron.DidNavigateInPageEvent) {
@@ -198,8 +224,10 @@ function handleDomReady() {
   updateNavigation();
   currentUrl.value = webviewRef.value?.getURL() || '';
   
-  // 开始自动检测登录状态
-  startAutoCheck();
+  // 管理模式下不自动检测登录状态
+  if (!isAdminMode.value) {
+    startAutoCheck();
+  }
 }
 
 function updateNavigation() {
@@ -283,7 +311,10 @@ function isLoggedInByUrl(): boolean {
       /mp\.toutiao\.com\/profile/,
     ],
     baijiahao: [
-      /baijiahao\.baidu\.com\/builder\/rc/,
+      /baijiahao\.baidu\.com\/builder\/rc/,       // 创作中心首页
+      /baijiahao\.baidu\.com\/builder\/app/,      // 应用页面
+      /baijiahao\.baidu\.com\/builder\/content/,  // 内容管理
+      /baijiahao\.baidu\.com\/builder\/theme/,    // 主题页面
     ],
   };
   
@@ -304,22 +335,158 @@ async function checkLoginSilently() {
       // 简单判断是否有登录相关的 cookie
       const hasLoginCookie = checkHasLoginCookie(cookies);
       
+      // 调试日志
+      if (platform.value === 'baijiahao') {
+        console.log('[BrowserTab] 百家号检测 - URL:', currentUrl.value);
+        console.log('[BrowserTab] 百家号检测 - hasLoginCookie:', hasLoginCookie);
+        console.log('[BrowserTab] 百家号检测 - cookies 数量:', cookies.length);
+        const bduss = cookies.find(c => c.name === 'BDUSS');
+        console.log('[BrowserTab] 百家号检测 - BDUSS:', bduss ? '存在' : '不存在');
+      }
+      
       if (hasLoginCookie && loginStatus.value !== 'success') {
         // 检查 URL 是否已经在登录后的页面
         const urlIndicatesLoggedIn = isLoggedInByUrl();
         
+        if (platform.value === 'baijiahao') {
+          console.log('[BrowserTab] 百家号检测 - urlIndicatesLoggedIn:', urlIndicatesLoggedIn);
+        }
+        
         if (urlIndicatesLoggedIn) {
           // URL 表明已登录,快速响应:先显示成功,再异步获取详情
+          console.log('[BrowserTab] URL 表明已登录,调用 quickLoginSuccess');
           await quickLoginSuccess(cookies);
         } else {
           // 还在登录页面,通过服务器验证
+          console.log('[BrowserTab] 通过服务器验证登录');
           await verifyLoginWithServer(cookies, true);
         }
       }
     }
   } catch (error) {
     // 静默失败,不处理
+    console.error('[BrowserTab] checkLoginSilently 异常:', error);
+  }
+}
+
+// 通过 webview 执行脚本获取百家号账号信息
+async function fetchBaijiahaoAccountInfo(retryCount = 0): Promise<{
+  accountId: string;
+  accountName: string;
+  avatarUrl: string;
+  fansCount: number;
+  worksCount: number;
+} | null> {
+  console.log('[BrowserTab] fetchBaijiahaoAccountInfo 开始, retryCount:', retryCount);
+  
+  if (!webviewRef.value) {
+    console.log('[BrowserTab] webviewRef 不存在');
+    return null;
+  }
+  
+  // 等待页面加载完成
+  console.log('[BrowserTab] 等待1秒让页面加载...');
+  await new Promise(resolve => setTimeout(resolve, 1000));
+  
+  try {
+    // 先获取当前页面的 URL
+    const currentPageUrl = await webviewRef.value.executeJavaScript('window.location.href');
+    console.log('[BrowserTab] 当前页面 URL:', currentPageUrl);
+    
+    // 检查是否在百家号域名下
+    if (!currentPageUrl.includes('baijiahao.baidu.com')) {
+      console.log('[BrowserTab] 当前页面不在百家号域名下,跳过获取');
+      return null;
+    }
+    
+    console.log('[BrowserTab] 正在获取百家号账号信息...');
+    const script = `
+      (async () => {
+        try {
+          console.log('[Baijiahao] 当前 URL:', window.location.href);
+          console.log('[Baijiahao] 当前 Cookie:', document.cookie.substring(0, 100));
+          console.log('[Baijiahao] Fetching settingInfo...');
+          
+          const response = await fetch('https://baijiahao.baidu.com/user-ui/cms/settingInfo', {
+            method: 'GET',
+            credentials: 'include',
+            headers: {
+              'Accept': 'application/json, text/plain, */*',
+              'Referer': 'https://baijiahao.baidu.com/'
+            }
+          });
+          
+          console.log('[Baijiahao] Response status:', response.status);
+          
+          if (!response.ok) {
+            return { success: false, error: 'HTTP ' + response.status };
+          }
+          
+          const text = await response.text();
+          console.log('[Baijiahao] Response text (first 300):', text.substring(0, 300));
+          
+          let data;
+          try {
+            data = JSON.parse(text);
+          } catch (e) {
+            return { success: false, error: 'JSON parse error: ' + e.message };
+          }
+          
+          console.log('[Baijiahao] settingInfo response:', JSON.stringify(data).substring(0, 300));
+          
+          if (data.errno === 0 && data.data) {
+            const accountId = data.data.new_uc_id ? String(data.data.new_uc_id) : 'baijiahao_' + Date.now();
+            const accountName = data.data.name || '百家号账号';
+            const avatarUrl = data.data.avatar || '';
+            console.log('[Baijiahao] 获取成功: id=' + accountId + ', name=' + accountName);
+            return {
+              success: true,
+              accountId: accountId,
+              accountName: accountName,
+              avatarUrl: avatarUrl,
+            };
+          }
+          
+          return { success: false, errno: data.errno, errmsg: data.errmsg || 'Unknown error' };
+        } catch (e) {
+          console.error('[Baijiahao] Error:', e);
+          return { success: false, error: e.message || String(e) };
+        }
+      })()
+    `;
+    
+    console.log('[BrowserTab] 执行 JavaScript 脚本...');
+    const result = await webviewRef.value.executeJavaScript(script);
+    console.log('[BrowserTab] 百家号账号信息结果:', JSON.stringify(result));
+    
+    if (result?.success) {
+      console.log('[BrowserTab] 获取百家号账号信息成功:', result.accountName);
+      return {
+        accountId: result.accountId,
+        accountName: result.accountName,
+        avatarUrl: result.avatarUrl,
+        fansCount: 0,
+        worksCount: 0,
+      };
+    }
+    
+    console.log('[BrowserTab] 获取百家号账号信息失败:', result?.error || result?.errmsg);
+    
+    // 如果失败且重试次数少于3次,等待后重试
+    if (retryCount < 3) {
+      console.log(`[BrowserTab] 获取百家号账号信息失败,${retryCount + 1}/3 次重试...`);
+      await new Promise(resolve => setTimeout(resolve, 2000));
+      return fetchBaijiahaoAccountInfo(retryCount + 1);
+    }
+  } catch (error) {
+    console.error('[BrowserTab] 获取百家号账号信息异常:', error);
+    // 失败时重试
+    if (retryCount < 3) {
+      await new Promise(resolve => setTimeout(resolve, 2000));
+      return fetchBaijiahaoAccountInfo(retryCount + 1);
+    }
   }
+  return null;
 }
 
 // 快速登录成功(URL 已表明登录成功时使用)
@@ -343,6 +510,17 @@ async function quickLoginSuccess(cookies: Electron.Cookie[]) {
   
   // 异步获取账号详细信息
   try {
+    // 百家号使用特殊方式获取账号信息(通过 webview 执行脚本)
+    if (platform.value === 'baijiahao') {
+      const baijiahaoInfo = await fetchBaijiahaoAccountInfo();
+      if (baijiahaoInfo) {
+        accountInfo.value = baijiahaoInfo;
+        isVerifying = false;
+        return;
+      }
+    }
+    
+    // 其他平台通过服务器 API 验证
     const result = await accountsApi.verifyLoginCookie(platform.value, cookieString);
     if (result.success && result.accountInfo) {
       accountInfo.value = result.accountInfo;
@@ -444,7 +622,26 @@ async function verifyLoginWithServer(cookies: Electron.Cookie[], silent = false)
   cookieData.value = cookieString;
   
   try {
-    // 调用服务器 API 验证并获取账号信息
+    // 百家号使用特殊方式获取账号信息(通过 webview 执行脚本)
+    if (platform.value === 'baijiahao') {
+      const baijiahaoInfo = await fetchBaijiahaoAccountInfo();
+      if (baijiahaoInfo) {
+        if (loginStatus.value !== 'success') {
+          loginStatus.value = 'success';
+          accountInfo.value = baijiahaoInfo;
+          stopAutoCheck();
+          
+          if (!hasShownSuccessMessage) {
+            hasShownSuccessMessage = true;
+            ElMessage.success('检测到登录成功!');
+          }
+        }
+        isVerifying = false;
+        return;
+      }
+    }
+    
+    // 其他平台调用服务器 API 验证并获取账号信息
     const result = await accountsApi.verifyLoginCookie(platform.value, cookieString);
     
     if (result.success && result.accountInfo) {
@@ -533,10 +730,131 @@ async function handleSaveAccount() {
 }
 
 // 生命周期
-onMounted(() => {
-  currentUrl.value = initialUrl.value;
+onMounted(async () => {
+  // 调试:输出 browserData 信息
+  console.log('[BrowserTab] onMounted, browserData:', JSON.stringify(props.tab.browserData, null, 2));
+  console.log('[BrowserTab] isAdminMode:', isAdminMode.value);
+  console.log('[BrowserTab] initialUrl:', initialUrl.value);
+  
+  const targetUrl = initialUrl.value;
+  currentUrl.value = targetUrl;
+  
+  // 如果有预设的 Cookie,先设置到 session 中,然后再加载页面
+  if (props.tab.browserData?.cookieData) {
+    console.log('[BrowserTab] 检测到预设 Cookie,先设置再加载页面');
+    await setPresetCookies(props.tab.browserData.cookieData);
+    // 等待一小段时间确保 Cookie 设置完成
+    await new Promise(resolve => setTimeout(resolve, 100));
+  }
+  
+  // 设置目标 URL 并渲染 webview
+  webviewSrc.value = targetUrl;
+  webviewReady.value = true;
+  console.log('[BrowserTab] webview 准备就绪,加载页面:', targetUrl);
 });
 
+// 设置预设的 Cookies
+async function setPresetCookies(cookieDataStr: string) {
+  if (!window.electronAPI?.setWebviewCookies) {
+    console.warn('electronAPI.setWebviewCookies 不可用');
+    return;
+  }
+  
+  try {
+    // 尝试解析 JSON 格式的 cookie
+    let cookieList: Array<{ name: string; value: string; domain?: string; path?: string }>;
+    
+    try {
+      cookieList = JSON.parse(cookieDataStr);
+    } catch {
+      // 如果不是 JSON,尝试解析 "name=value; name2=value2" 格式
+      cookieList = parseCookieString(cookieDataStr);
+    }
+    
+    if (cookieList.length === 0) {
+      console.warn('没有有效的 Cookie 数据');
+      return;
+    }
+    
+    // 获取目标 URL(优先使用 browserData 中的 url)
+    const targetUrl = props.tab.browserData?.url || PLATFORMS[platform.value]?.loginUrl || '';
+    
+    // 获取平台的根域名
+    const platformDomainMap: Record<string, string> = {
+      douyin: '.douyin.com',
+      xiaohongshu: '.xiaohongshu.com',
+      kuaishou: '.kuaishou.com',
+      weixin_video: '.qq.com',
+      bilibili: '.bilibili.com',
+    };
+    const rootDomain = platformDomainMap[platform.value] || '';
+    
+    console.log(`[BrowserTab] 设置 Cookie, platform=${platform.value}, targetUrl=${targetUrl}, domain=${rootDomain}`);
+    
+    // 转换为 Electron 需要的格式,保留原始 Cookie 的 domain
+    const cookiesForElectron = cookieList.map(cookie => {
+      // 优先使用 Cookie 自带的 domain,否则使用平台根域名
+      let cookieDomain = cookie.domain;
+      if (!cookieDomain || cookieDomain === '') {
+        cookieDomain = rootDomain;
+      }
+      // 确保域名以点开头(用于子域名匹配)
+      if (cookieDomain && !cookieDomain.startsWith('.') && !cookieDomain.includes('localhost')) {
+        cookieDomain = '.' + cookieDomain;
+      }
+      
+      return {
+        url: targetUrl,
+        name: cookie.name,
+        value: cookie.value,
+        domain: cookieDomain,
+        path: cookie.path || '/',
+      };
+    });
+    
+    console.log(`[BrowserTab] 准备设置 ${cookiesForElectron.length} 个 Cookies`);
+    
+    // 设置 cookies
+    const success = await window.electronAPI.setWebviewCookies(
+      webviewPartition.value,
+      cookiesForElectron
+    );
+    
+    if (success) {
+      console.log(`[BrowserTab] 预设 ${cookieList.length} 个 Cookies 成功`);
+      // 标记已有登录 cookie
+      loginStatus.value = 'checking';
+    } else {
+      console.warn('[BrowserTab] 设置 Cookies 失败');
+    }
+  } catch (error) {
+    console.error('[BrowserTab] 设置预设 Cookies 失败:', error);
+  }
+}
+
+// 解析 "name=value; name2=value2" 格式的 cookie 字符串
+function parseCookieString(cookieString: string): Array<{ name: string; value: string }> {
+  const cookies: Array<{ name: string; value: string }> = [];
+  
+  const pairs = cookieString.split(';');
+  for (const pair of pairs) {
+    const trimmed = pair.trim();
+    if (!trimmed) continue;
+    
+    const eqIndex = trimmed.indexOf('=');
+    if (eqIndex === -1) continue;
+    
+    const name = trimmed.substring(0, eqIndex).trim();
+    const value = trimmed.substring(eqIndex + 1).trim();
+    
+    if (name && value) {
+      cookies.push({ name, value });
+    }
+  }
+  
+  return cookies;
+}
+
 onUnmounted(() => {
   stopAutoCheck();
   

+ 6 - 1
client/src/stores/tabs.ts

@@ -18,8 +18,10 @@ export interface Tab {
     platform: string;
     sessionId?: string;
     url?: string;
+    cookieData?: string; // 预设的 Cookie 数据(JSON 格式)
     status?: 'loading' | 'ready' | 'login_pending' | 'login_success' | 'login_failed';
     groupId?: number;
+    isAdminMode?: boolean; // 是否是管理后台模式(从后台按钮打开,不需要登录检测和保存账号)
   };
   // 是否可关闭
   closable?: boolean;
@@ -118,7 +120,7 @@ export const useTabsStore = defineStore('tabs', () => {
   }
   
   // 打开浏览器标签页
-  function openBrowserTab(platform: string, title?: string, groupId?: number): Tab {
+  function openBrowserTab(platform: string, title?: string, groupId?: number, url?: string, cookieData?: string, isAdminMode?: boolean): Tab {
     const tab = addTab({
       title: title || `${platform} 浏览器`,
       type: 'browser',
@@ -126,6 +128,9 @@ export const useTabsStore = defineStore('tabs', () => {
         platform,
         status: 'loading',
         groupId,
+        url, // 可选的初始 URL
+        cookieData, // 可选的预设 Cookie
+        isAdminMode, // 是否是管理后台模式
       },
       icon: 'Monitor',
     });

+ 7 - 4
client/src/stores/taskQueue.ts

@@ -192,7 +192,8 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
 
       case 'created': {
         const task = data.payload?.task as Task;
-        if (task && !tasks.value.find(t => t.id === task.id)) {
+        // 静默任务不添加到任务列表中
+        if (task && !task.silent && !tasks.value.find(t => t.id === task.id)) {
           tasks.value.unshift(task);
         }
         break;
@@ -244,7 +245,8 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
       case 'list': {
         const taskList = data.payload?.tasks as Task[];
         if (taskList) {
-          tasks.value = taskList;
+          // 过滤掉静默任务
+          tasks.value = taskList.filter(t => !t.silent);
         }
         break;
       }
@@ -336,8 +338,9 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
         data?: { tasks: Task[] }; 
         tasks?: Task[] 
       };
-      // 兼容两种返回格式
-      tasks.value = result?.data?.tasks || result?.tasks || [];
+      // 兼容两种返回格式,过滤掉静默任务
+      const allTasks = result?.data?.tasks || result?.tasks || [];
+      tasks.value = allTasks.filter(t => !t.silent);
       console.log('[TaskQueue] Fetched tasks:', tasks.value.length);
     } catch (e) {
       console.error('Failed to fetch tasks:', e);

+ 393 - 72
client/src/views/Accounts/index.vue

@@ -3,6 +3,10 @@
     <div class="page-header">
       <h2>账号管理</h2>
       <div class="header-actions">
+        <el-button @click="showGroupManage = true">
+          <el-icon><Setting /></el-icon>
+          管理分组
+        </el-button>
         <el-button @click="refreshAllAccounts" :disabled="!accounts.length">
           <el-icon><Refresh /></el-icon>
           刷新所有
@@ -90,20 +94,34 @@
           </template>
         </el-table-column>
         
-        <el-table-column label="分组" width="120">
+        <el-table-column label="分组" width="150">
           <template #default="{ row }">
-            {{ groups.find(g => g.id === row.groupId)?.name || '-' }}
+            <el-select 
+              :model-value="row.groupId" 
+              placeholder="无分组" 
+              clearable 
+              size="small"
+              style="width: 100%"
+              @change="(val: number | undefined) => handleChangeGroup(row.id, val)"
+            >
+              <el-option
+                v-for="group in groups"
+                :key="group.id"
+                :label="group.name"
+                :value="group.id"
+              />
+            </el-select>
           </template>
         </el-table-column>
         
         <el-table-column label="操作" width="180" fixed="right">
           <template #default="{ row }">
+            <el-button type="primary" link size="small" @click="openPlatformAdmin(row)">
+              后台
+            </el-button>
             <el-button type="primary" link size="small" @click="refreshAccount(row.id)">
               刷新
             </el-button>
-            <el-button type="primary" link size="small" @click="editAccount(row)">
-              编辑
-            </el-button>
             <el-button type="danger" link size="small" @click="deleteAccount(row.id)">
               删除
             </el-button>
@@ -165,54 +183,27 @@
     <el-dialog 
       v-model="showPlatformSelect" 
       title="选择登录平台" 
-      width="450px"
+      width="480px"
       :close-on-click-modal="true"
     >
-      <el-form :model="browserLoginForm" label-width="80px">
-        <el-form-item label="平台">
-          <el-select v-model="browserLoginForm.platform" placeholder="选择要登录的平台" style="width: 100%">
-            <el-option
-              v-for="platform in platforms"
-              :key="platform.type"
-              :label="platform.name"
-              :value="platform.type"
-              :disabled="!platform.supported"
-            >
-              <span class="platform-option">
-                <span>{{ platform.name }}</span>
-                <el-tag v-if="!platform.supported" size="small" type="info">适配中</el-tag>
-              </span>
-            </el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="分组">
-          <el-select v-model="browserLoginForm.groupId" placeholder="选择分组" clearable style="width: 100%">
-            <el-option
-              v-for="group in groups"
-              :key="group.id"
-              :label="group.name"
-              :value="group.id"
-            />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      
-      <el-alert type="info" :closable="false" style="margin-top: 10px;">
-        <template #title>
-          点击"开始登录"后会在右侧标签页中打开浏览器登录界面,登录成功后系统会自动获取Cookie。
-        </template>
-      </el-alert>
-      
-      <template #footer>
-        <el-button @click="showPlatformSelect = false">取消</el-button>
-        <el-button 
-          type="primary" 
-          @click="startBrowserLogin" 
-          :disabled="!browserLoginForm.platform"
+      <div class="platform-grid">
+        <div 
+          v-for="platform in supportedPlatforms" 
+          :key="platform.type"
+          class="platform-card"
+          :style="{ '--platform-color': platform.color }"
+          @click="selectPlatformAndLogin(platform.type)"
         >
-          开始登录
-        </el-button>
-      </template>
+          <div class="platform-icon" :class="platform.type">
+            {{ platform.name[0] }}
+          </div>
+          <span class="platform-name">{{ platform.name }}</span>
+        </div>
+      </div>
+      
+      <div class="platform-hint">
+        点击平台图标后会在右侧标签页中打开登录界面,登录成功后系统会自动获取Cookie
+      </div>
     </el-dialog>
     
     <!-- 刷新账号对话框 -->
@@ -309,12 +300,87 @@
         </template>
       </template>
     </el-dialog>
+    
+    <!-- 分组管理对话框 -->
+    <el-dialog 
+      v-model="showGroupManage" 
+      title="管理分组" 
+      width="500px"
+      :close-on-click-modal="false"
+    >
+      <div class="group-manage">
+        <!-- 新增分组 -->
+        <div class="group-add">
+          <el-input 
+            v-model="newGroupName" 
+            placeholder="输入新分组名称" 
+            style="width: 300px"
+            @keyup.enter="handleAddGroup"
+          />
+          <el-button type="primary" @click="handleAddGroup" :loading="groupSaving">
+            <el-icon><Plus /></el-icon>
+            新增
+          </el-button>
+        </div>
+        
+        <!-- 分组列表 -->
+        <div class="group-list">
+          <div v-if="groups.length === 0" class="group-empty">
+            <el-empty description="暂无分组" :image-size="80" />
+          </div>
+          <div 
+            v-else 
+            v-for="group in groups" 
+            :key="group.id" 
+            class="group-item"
+          >
+            <template v-if="editingGroupId === group.id">
+              <el-input 
+                v-model="editingGroupName" 
+                size="small"
+                style="flex: 1"
+                @keyup.enter="handleSaveGroup(group.id)"
+                @keyup.escape="cancelEditGroup"
+              />
+              <el-button type="primary" size="small" @click="handleSaveGroup(group.id)" :loading="groupSaving">
+                保存
+              </el-button>
+              <el-button size="small" @click="cancelEditGroup">
+                取消
+              </el-button>
+            </template>
+            <template v-else>
+              <span class="group-name">{{ group.name }}</span>
+              <span class="group-count">{{ getGroupAccountCount(group.id) }} 个账号</span>
+              <div class="group-actions">
+                <el-button type="primary" link size="small" @click="startEditGroup(group)">
+                  编辑
+                </el-button>
+                <el-button 
+                  type="danger" 
+                  link 
+                  size="small" 
+                  @click="handleDeleteGroup(group)"
+                  :disabled="getGroupAccountCount(group.id) > 0"
+                >
+                  删除
+                </el-button>
+              </div>
+            </template>
+          </div>
+        </div>
+      </div>
+      
+      <template #footer>
+        <el-button @click="showGroupManage = false">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
-import { Plus, Refresh, Monitor, Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue';
+import { Plus, Refresh, Monitor, Loading, CircleCheck, CircleClose, Setting } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { accountsApi } from '@/api/accounts';
 import { PLATFORMS, PLATFORM_TYPES } from '@media-manager/shared';
@@ -361,6 +427,13 @@ const submitting = ref(false);
 const showAddDialog = ref(false);
 const showPlatformSelect = ref(false);
 const showRefreshDialog = ref(false);
+const showGroupManage = ref(false);
+
+// 分组管理相关
+const newGroupName = ref('');
+const groupSaving = ref(false);
+const editingGroupId = ref<number | null>(null);
+const editingGroupName = ref('');
 
 // 刷新账号的状态
 const refreshState = reactive({
@@ -384,6 +457,9 @@ const browserLoginForm = reactive({
 
 const platforms = PLATFORM_TYPES.map(type => PLATFORMS[type]);
 
+// 只显示已支持的平台
+const supportedPlatforms = platforms.filter(p => p.supported);
+
 const filter = reactive({
   platform: '',
   groupId: undefined as number | undefined,
@@ -555,9 +631,55 @@ function handleReLoginFromRefresh() {
   showPlatformSelect.value = true;
 }
 
-function editAccount(account: PlatformAccount) {
-  // TODO: 实现编辑功能
-  ElMessage.info('编辑功能开发中');
+// 修改账号分组
+async function handleChangeGroup(accountId: number, groupId: number | undefined) {
+  try {
+    await accountsApi.updateAccount(accountId, { groupId: groupId || null });
+    // 更新本地数据
+    const account = accounts.value.find(a => a.id === accountId);
+    if (account) {
+      account.groupId = groupId || null;
+    }
+    ElMessage.success('分组已更新');
+  } catch {
+    // 错误已处理,重新加载数据以恢复
+    loadAccounts();
+  }
+}
+
+// 打开平台后台管理页面
+async function openPlatformAdmin(account: PlatformAccount) {
+  const platformInfo = PLATFORMS[account.platform as PlatformType];
+  if (!platformInfo) {
+    ElMessage.warning('未知平台');
+    return;
+  }
+  
+  try {
+    // 获取账号的 Cookie 数据
+    const { cookieData } = await accountsApi.getAccountCookie(account.id);
+    
+    // 在标签页中打开平台后台,带上 Cookie,设置为管理后台模式
+    tabsStore.openBrowserTab(
+      account.platform,
+      `${platformInfo.name} - ${account.accountName}`,
+      undefined,
+      platformInfo.creatorUrl,
+      cookieData,
+      true // isAdminMode: 管理后台模式,不校验登录和保存账号
+    );
+  } catch (error) {
+    console.error('获取账号 Cookie 失败:', error);
+    // 即使获取 Cookie 失败,也打开后台(用户需要重新登录),仍然是管理后台模式
+    tabsStore.openBrowserTab(
+      account.platform,
+      `${platformInfo.name} - ${account.accountName}`,
+      undefined,
+      platformInfo.creatorUrl,
+      undefined,
+      true // isAdminMode: 管理后台模式
+    );
+  }
 }
 
 async function deleteAccount(id: number) {
@@ -573,32 +695,105 @@ async function deleteAccount(id: number) {
   }
 }
 
-// 浏览器登录功能 - 改为在标签页中打开
-function startBrowserLogin() {
-  if (!browserLoginForm.platform) {
-    ElMessage.warning('请选择平台');
+// 分组管理方法
+function getGroupAccountCount(groupId: number): number {
+  return accounts.value.filter(a => a.groupId === groupId).length;
+}
+
+async function handleAddGroup() {
+  if (!newGroupName.value.trim()) {
+    ElMessage.warning('请输入分组名称');
     return;
   }
   
-  const platformName = getPlatformName(browserLoginForm.platform as PlatformType);
+  groupSaving.value = true;
+  try {
+    await accountsApi.createGroup({ name: newGroupName.value.trim() });
+    ElMessage.success('分组创建成功');
+    newGroupName.value = '';
+    loadAccounts(); // 重新加载以获取最新分组列表
+  } catch {
+    // 错误已处理
+  } finally {
+    groupSaving.value = false;
+  }
+}
+
+function startEditGroup(group: AccountGroup) {
+  editingGroupId.value = group.id;
+  editingGroupName.value = group.name;
+}
+
+function cancelEditGroup() {
+  editingGroupId.value = null;
+  editingGroupName.value = '';
+}
+
+async function handleSaveGroup(groupId: number) {
+  if (!editingGroupName.value.trim()) {
+    ElMessage.warning('分组名称不能为空');
+    return;
+  }
+  
+  groupSaving.value = true;
+  try {
+    await accountsApi.updateGroup(groupId, { name: editingGroupName.value.trim() });
+    ElMessage.success('分组更新成功');
+    cancelEditGroup();
+    loadAccounts(); // 重新加载以获取最新分组列表
+  } catch {
+    // 错误已处理
+  } finally {
+    groupSaving.value = false;
+  }
+}
+
+async function handleDeleteGroup(group: AccountGroup) {
+  const count = getGroupAccountCount(group.id);
+  if (count > 0) {
+    ElMessage.warning(`该分组下还有 ${count} 个账号,请先移除账号`);
+    return;
+  }
+  
+  try {
+    await ElMessageBox.confirm(`确定要删除分组"${group.name}"吗?`, '提示', {
+      type: 'warning',
+    });
+    await accountsApi.deleteGroup(group.id);
+    ElMessage.success('分组删除成功');
+    loadAccounts(); // 重新加载以获取最新分组列表
+  } catch {
+    // 取消或错误
+  }
+}
+
+// 选择平台并开始登录(九宫格点击)
+function selectPlatformAndLogin(platformType: PlatformType) {
+  const platformName = getPlatformName(platformType);
   
   // 在标签页中打开浏览器
   tabsStore.openBrowserTab(
-    browserLoginForm.platform,
-    `${platformName} 登录`,
-    browserLoginForm.groupId
+    platformType,
+    `${platformName} 登录`
   );
   
   // 关闭平台选择对话框
   showPlatformSelect.value = false;
   
-  // 重置表单
-  browserLoginForm.platform = '';
-  browserLoginForm.groupId = undefined;
-  
   ElMessage.success('已打开浏览器登录标签页');
 }
 
+// 浏览器登录功能 - 改为在标签页中打开(保留用于重新登录场景)
+function startBrowserLogin() {
+  if (!browserLoginForm.platform) {
+    ElMessage.warning('请选择平台');
+    return;
+  }
+  
+  selectPlatformAndLogin(browserLoginForm.platform as PlatformType);
+  browserLoginForm.platform = '';
+}
+
 // 检查过期账号并提示重新登录
 async function checkExpiredAccounts() {
   const expiredAccounts = accounts.value.filter(a => a.status === 'expired');
@@ -927,15 +1122,141 @@ onUnmounted(() => {
   80%, 100% { content: '...'; }
 }
 
-.platform-option {
+// 平台选择九宫格
+.platform-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+  padding: 8px 0;
+}
+
+.platform-card {
   display: flex;
+  flex-direction: column;
   align-items: center;
-  justify-content: space-between;
-  width: 100%;
+  gap: 10px;
+  padding: 20px 16px;
+  border-radius: 12px;
+  background: $bg-light;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  border: 2px solid transparent;
+  
+  &:hover {
+    background: #fff;
+    border-color: var(--platform-color, $primary-color);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    
+    .platform-icon {
+      transform: scale(1.1);
+    }
+  }
   
-  .el-tag {
-    margin-left: 8px;
-    font-size: 11px;
+  &:active {
+    transform: translateY(0);
+  }
+}
+
+.platform-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  font-weight: 600;
+  color: #fff;
+  transition: transform 0.2s ease;
+  
+  // 各平台颜色
+  &.douyin {
+    background: linear-gradient(135deg, #000000, #333333);
+  }
+  &.xiaohongshu {
+    background: linear-gradient(135deg, #FE2C55, #FF5C7C);
+  }
+  &.kuaishou {
+    background: linear-gradient(135deg, #FF5000, #FF7733);
+  }
+  &.weixin_video {
+    background: linear-gradient(135deg, #07C160, #2DC76D);
+  }
+  &.bilibili {
+    background: linear-gradient(135deg, #00A1D6, #33B5E5);
+  }
+  &.toutiao {
+    background: linear-gradient(135deg, #F85959, #FF7B7B);
+  }
+  &.baijiahao {
+    background: linear-gradient(135deg, #2932E1, #5B6AE8);
+  }
+}
+
+.platform-name {
+  font-size: 14px;
+  font-weight: 500;
+  color: $text-primary;
+}
+
+.platform-hint {
+  margin-top: 16px;
+  padding: 12px 16px;
+  background: $bg-light;
+  border-radius: 8px;
+  font-size: 13px;
+  color: $text-secondary;
+  text-align: center;
+}
+
+// 分组管理样式
+.group-manage {
+  .group-add {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 20px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid $border-light;
+  }
+  
+  .group-list {
+    max-height: 400px;
+    overflow-y: auto;
+  }
+  
+  .group-empty {
+    padding: 20px 0;
+  }
+  
+  .group-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px 16px;
+    background: $bg-light;
+    border-radius: 8px;
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .group-name {
+      flex: 1;
+      font-weight: 500;
+      color: $text-primary;
+    }
+    
+    .group-count {
+      font-size: 13px;
+      color: $text-secondary;
+    }
+    
+    .group-actions {
+      display: flex;
+      gap: 4px;
+    }
   }
 }
 </style>

+ 58 - 0
server/python/app.py

@@ -552,6 +552,64 @@ def get_all_comments():
         return jsonify({"success": False, "error": str(e)}), 500
 
 
+# ==================== 登录状态检查接口 ====================
+
+@app.route("/check_login", methods=["POST"])
+def check_login():
+    """
+    检查 Cookie 登录状态(通过浏览器访问后台页面检测)
+    
+    请求体:
+    {
+        "platform": "douyin",           # douyin | xiaohongshu | kuaishou | weixin
+        "cookie": "cookie字符串或JSON"
+    }
+    
+    响应:
+    {
+        "success": true,
+        "valid": true,                   # Cookie 是否有效
+        "need_login": false,             # 是否需要重新登录
+        "message": "登录状态有效"
+    }
+    """
+    try:
+        data = request.json
+        
+        platform = data.get("platform", "").lower()
+        cookie_str = data.get("cookie", "")
+        
+        print(f"[CheckLogin] 收到请求: platform={platform}")
+        
+        if not platform:
+            return jsonify({"success": False, "error": "缺少 platform 参数"}), 400
+        if platform not in PLATFORM_MAP:
+            return jsonify({
+                "success": False,
+                "error": f"不支持的平台: {platform},支持: {list(PLATFORM_MAP.keys())}"
+            }), 400
+        if not cookie_str:
+            return jsonify({"success": False, "error": "缺少 cookie 参数"}), 400
+        
+        # 获取对应平台的发布器
+        PublisherClass = get_publisher(platform)
+        publisher = PublisherClass(headless=HEADLESS_MODE)
+        
+        # 执行登录检查
+        result = asyncio.run(publisher.check_login_status(cookie_str))
+        
+        return jsonify(result)
+        
+    except Exception as e:
+        traceback.print_exc()
+        return jsonify({
+            "success": False, 
+            "valid": False, 
+            "need_login": True,
+            "error": str(e)
+        }), 500
+
+
 # ==================== 健康检查 ====================
 
 @app.route("/health", methods=["GET"])

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


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

@@ -364,4 +364,90 @@ class BasePublisher(ABC):
                 error=str(e)
             )
         finally:
+            await self.close_browser()
+    
+    async def check_login_status(self, cookies: str) -> dict:
+        """
+        检查 Cookie 登录状态(通过浏览器访问后台页面检测)
+        
+        Args:
+            cookies: cookie 字符串或 JSON
+            
+        Returns:
+            dict: {
+                "success": True,
+                "valid": True/False,
+                "need_login": True/False,
+                "message": "状态描述"
+            }
+        """
+        try:
+            await self.init_browser()
+            cookie_list = self.parse_cookies(cookies)
+            await self.set_cookies(cookie_list)
+            
+            if not self.page:
+                raise Exception("Page not initialized")
+            
+            # 访问平台后台首页
+            home_url = self.login_url
+            print(f"[{self.platform_name}] 访问后台页面: {home_url}")
+            await self.page.goto(home_url, wait_until='domcontentloaded', timeout=30000)
+            await asyncio.sleep(3)
+            
+            # 检查当前 URL 是否被重定向到登录页
+            current_url = self.page.url
+            print(f"[{self.platform_name}] 当前 URL: {current_url}")
+            
+            # 登录页特征
+            login_indicators = ['login', 'passport', 'signin', 'auth']
+            is_login_page = any(indicator in current_url.lower() for indicator in login_indicators)
+            
+            # 检查页面是否有登录弹窗
+            need_login = is_login_page
+            
+            if not need_login:
+                # 检查页面内容是否有登录提示
+                login_selectors = [
+                    'text="请先登录"',
+                    'text="登录后继续"',
+                    'text="请登录"',
+                    '[class*="login-modal"]',
+                    '[class*="login-dialog"]',
+                    '[class*="login-popup"]',
+                ]
+                for selector in login_selectors:
+                    try:
+                        if await self.page.locator(selector).count() > 0:
+                            need_login = True
+                            print(f"[{self.platform_name}] 检测到登录弹窗: {selector}")
+                            break
+                    except:
+                        pass
+            
+            if need_login:
+                return {
+                    "success": True,
+                    "valid": False,
+                    "need_login": True,
+                    "message": "Cookie 已过期,需要重新登录"
+                }
+            else:
+                return {
+                    "success": True,
+                    "valid": True,
+                    "need_login": False,
+                    "message": "登录状态有效"
+                }
+                
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            return {
+                "success": False,
+                "valid": False,
+                "need_login": True,
+                "error": str(e)
+            }
+        finally:
             await self.close_browser()

+ 77 - 39
server/src/app.ts

@@ -4,7 +4,7 @@ import helmet from 'helmet';
 import morgan from 'morgan';
 import compression from 'compression';
 import { createServer } from 'http';
-import { createConnection } from 'net';
+import { createConnection, createServer as createNetServer } from 'net';
 import { exec } from 'child_process';
 import { promisify } from 'util';
 import { config } from './config/index.js';
@@ -60,16 +60,26 @@ app.use(errorHandler);
 // WebSocket
 setupWebSocket(httpServer);
 
-// 检查端口是否被占用
+// 检查端口是否被占用(改进版:使用 net.Server 来测试)
 async function checkPortInUse(port: number): Promise<boolean> {
   return new Promise((resolve) => {
-    const client = createConnection({ port }, () => {
-      client.end();
-      resolve(true); // 端口被占用
+    const testServer = createNetServer();
+    
+    testServer.once('error', (err: NodeJS.ErrnoException) => {
+      if (err.code === 'EADDRINUSE') {
+        resolve(true); // 端口被占用
+      } else {
+        resolve(false);
+      }
     });
-    client.on('error', () => {
-      resolve(false); // 端口未被占用
+    
+    testServer.once('listening', () => {
+      testServer.close(() => {
+        resolve(false); // 端口未被占用
+      });
     });
+    
+    testServer.listen(port);
   });
 }
 
@@ -79,20 +89,28 @@ async function getProcessOnPort(port: number): Promise<number | null> {
   
   try {
     if (isWindows) {
-      const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
+      // Windows: 使用 netstat 命令,改进解析逻辑
+      const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
       const lines = stdout.trim().split('\n');
-      if (lines.length > 0) {
-        const parts = lines[0].trim().split(/\s+/);
-        const pid = parseInt(parts[parts.length - 1], 10);
-        if (!isNaN(pid)) return pid;
+      for (const line of lines) {
+        // 匹配 LISTENING 状态的连接
+        if (line.includes('LISTENING')) {
+          const parts = line.trim().split(/\s+/);
+          const pid = parseInt(parts[parts.length - 1], 10);
+          if (!isNaN(pid) && pid > 0) {
+            logger.info(`[Port Check] Found PID ${pid} listening on port ${port}`);
+            return pid;
+          }
+        }
       }
     } else {
       const { stdout } = await execAsync(`lsof -i :${port} -t`);
       const pid = parseInt(stdout.trim().split('\n')[0], 10);
       if (!isNaN(pid)) return pid;
     }
-  } catch {
-    // 命令执行失败,端口可能没有被占用
+  } catch (error) {
+    // 命令执行失败,可能端口没有被占用
+    logger.debug(`[Port Check] Command failed (port might not be in use):`, error);
   }
   return null;
 }
@@ -100,6 +118,13 @@ async function getProcessOnPort(port: number): Promise<number | null> {
 // 终止进程(跨平台)
 async function killProcess(pid: number): Promise<boolean> {
   const isWindows = process.platform === 'win32';
+  const currentPid = process.pid;
+  
+  // 不要杀死自己的进程
+  if (pid === currentPid) {
+    logger.warn(`[Port Check] Cannot kill own process (PID: ${pid})`);
+    return false;
+  }
   
   try {
     if (isWindows) {
@@ -108,7 +133,7 @@ async function killProcess(pid: number): Promise<boolean> {
       await execAsync(`kill -9 ${pid}`);
     }
     // 等待进程完全终止
-    await new Promise(resolve => setTimeout(resolve, 1000));
+    await new Promise(resolve => setTimeout(resolve, 2000));
     return true;
   } catch (error) {
     logger.error(`Failed to kill process ${pid}:`, error);
@@ -116,35 +141,48 @@ async function killProcess(pid: number): Promise<boolean> {
   }
 }
 
-// 检查并释放端口
-async function ensurePortAvailable(port: number): Promise<void> {
-  const inUse = await checkPortInUse(port);
-  
-  if (!inUse) {
-    return;
-  }
-  
-  logger.warn(`Port ${port} is already in use, attempting to release...`);
-  
-  const pid = await getProcessOnPort(port);
-  
-  if (pid) {
-    logger.info(`Found process ${pid} using port ${port}, terminating...`);
-    const killed = await killProcess(pid);
+// 检查并释放端口(增加重试机制)
+async function ensurePortAvailable(port: number, maxRetries: number = 3): Promise<void> {
+  for (let attempt = 1; attempt <= maxRetries; attempt++) {
+    const inUse = await checkPortInUse(port);
+    
+    if (!inUse) {
+      logger.info(`[Port Check] Port ${port} is available`);
+      return;
+    }
     
-    if (killed) {
-      logger.info(`Successfully terminated process ${pid}`);
-      // 再次检查端口
-      const stillInUse = await checkPortInUse(port);
-      if (stillInUse) {
-        throw new Error(`Port ${port} is still in use after killing process`);
+    logger.warn(`[Port Check] Attempt ${attempt}/${maxRetries}: Port ${port} is already in use`);
+    
+    const pid = await getProcessOnPort(port);
+    
+    if (pid) {
+      logger.info(`[Port Check] Found process ${pid} using port ${port}, attempting to terminate...`);
+      const killed = await killProcess(pid);
+      
+      if (killed) {
+        logger.info(`[Port Check] Successfully terminated process ${pid}`);
+        // 等待更长时间让端口释放
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        
+        // 再次检查端口
+        const stillInUse = await checkPortInUse(port);
+        if (!stillInUse) {
+          logger.info(`[Port Check] Port ${port} is now available`);
+          return;
+        }
       }
     } else {
-      throw new Error(`Failed to kill process ${pid} using port ${port}`);
+      logger.warn(`[Port Check] Port ${port} is in use but could not identify the process`);
+    }
+    
+    // 如果还有重试机会,等待一会儿
+    if (attempt < maxRetries) {
+      logger.info(`[Port Check] Waiting before retry...`);
+      await new Promise(resolve => setTimeout(resolve, 3000));
     }
-  } else {
-    throw new Error(`Port ${port} is in use but could not identify the process`);
   }
+  
+  throw new Error(`Port ${port} is still in use after ${maxRetries} attempts. Please manually kill the process or use a different port.`);
 }
 
 // 启动服务

+ 21 - 0
server/src/routes/accounts.ts

@@ -186,6 +186,27 @@ router.get(
   })
 );
 
+// 获取账号 Cookie(用于客户端打开后台)
+router.get(
+  '/:id/cookie',
+  [
+    param('id').isInt().withMessage('账号ID无效'),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const cookieData = await accountService.getAccountCookie(
+      req.user!.userId,
+      Number(req.params.id)
+    );
+    res.json({ 
+      success: true, 
+      data: {
+        cookieData,
+      }
+    });
+  })
+);
+
 // 批量刷新所有账号状态
 router.post(
   '/refresh-all',

+ 66 - 85
server/src/scheduler/index.ts

@@ -5,35 +5,44 @@ import { wsManager } from '../websocket/index.js';
 import { WS_EVENTS } from '@media-manager/shared';
 import { getAdapter, isPlatformSupported } from '../automation/platforms/index.js';
 import { LessThanOrEqual, In } from 'typeorm';
+import { taskQueueService } from '../services/TaskQueueService.js';
 
 /**
  * 定时任务调度器
  */
 export class TaskScheduler {
   private jobs: Map<string, schedule.Job> = new Map();
+  private isRefreshingAccounts = false; // 账号刷新锁,防止任务重叠执行
   
   /**
    * 启动调度器
    */
   start(): void {
-    logger.info('Starting task scheduler...');
+    logger.info('[Scheduler] ========================================');
+    logger.info('[Scheduler] Starting task scheduler...');
     
     // 每分钟检查定时发布任务
     this.scheduleJob('check-publish-tasks', '* * * * *', this.checkPublishTasks.bind(this));
     
-    // 每10分钟刷新账号状态
-    this.scheduleJob('refresh-accounts', '*/10 * * * *', this.refreshAccounts.bind(this));
+    // 每15分钟刷新账号状态
+    this.scheduleJob('refresh-accounts', '*/15 * * * *', this.refreshAccounts.bind(this));
     
     // 每天凌晨2点采集数据
     this.scheduleJob('collect-analytics', '0 2 * * *', this.collectAnalytics.bind(this));
     
+    logger.info('[Scheduler] Scheduled jobs:');
+    logger.info('[Scheduler]   - check-publish-tasks: every minute (* * * * *)');
+    logger.info('[Scheduler]   - refresh-accounts: every 15 minutes (*/15 * * * *)');
+    logger.info('[Scheduler]   - collect-analytics: daily at 2:00 AM (0 2 * * *)');
+    logger.info('[Scheduler] ========================================');
+    
     // 服务器启动时立即刷新一次账号状态
     setTimeout(() => {
-      logger.info('Initial account refresh on startup...');
-      this.refreshAccounts().catch(err => logger.error('Initial refresh failed:', err));
-    }, 5000); // 延迟5秒,等待其他服务初始化完成
+      logger.info('[Scheduler] Running initial account refresh on startup...');
+      this.refreshAccounts().catch(err => logger.error('[Scheduler] Initial refresh failed:', err));
+    }, 10000); // 延迟10秒,等待其他服务初始化完成
     
-    logger.info('Task scheduler started');
+    logger.info('[Scheduler] Task scheduler started successfully');
   }
   
   /**
@@ -159,97 +168,69 @@ export class TaskScheduler {
   
   /**
    * 刷新账号状态和信息
-   * 并行执行多个账号的刷新,提高效率
+   * 将每个账号的刷新任务加入到任务队列中执行
+   * 通过任务队列控制并发,避免浏览器资源竞争
    */
   private async refreshAccounts(): Promise<void> {
-    const accountRepository = AppDataSource.getRepository(PlatformAccount);
-    
-    const accounts = await accountRepository.find({
-      where: { status: 'active' },
-    });
-    
-    logger.info(`Refreshing ${accounts.length} active accounts...`);
+    // 检查是否正在执行刷新任务
+    if (this.isRefreshingAccounts) {
+      logger.info('[Scheduler] Account refresh is already running, skipping this cycle...');
+      return;
+    }
     
-    // 并行刷新所有账号(限制并发数为5)
-    const concurrencyLimit = 5;
-    const results: Promise<void>[] = [];
+    // 获取锁
+    this.isRefreshingAccounts = true;
+    logger.debug('[Scheduler] Acquired refresh lock');
     
-    for (let i = 0; i < accounts.length; i++) {
-      const account = accounts[i];
+    try {
+      const accountRepository = AppDataSource.getRepository(PlatformAccount);
+      
+      // 获取所有账号,不过滤 status,让刷新任务自动检测并更新状态
+      const accounts = await accountRepository.find();
+      
+      if (accounts.length === 0) {
+        logger.info('[Scheduler] No active accounts to refresh');
+        return;
+      }
+      
+      logger.info(`[Scheduler] Creating refresh tasks for ${accounts.length} active accounts...`);
       
-      const refreshPromise = (async () => {
-        if (!isPlatformSupported(account.platform)) return;
+      let tasksCreated = 0;
+      let skipped = 0;
+      
+      // 为每个账号创建刷新任务,加入任务队列
+      for (const account of accounts) {
+        if (!isPlatformSupported(account.platform)) {
+          logger.debug(`[Scheduler] Platform ${account.platform} not supported, skipping account ${account.id}`);
+          skipped++;
+          continue;
+        }
         
         try {
-          const adapter = getAdapter(account.platform);
-          
-          // 先检查登录状态
-          const isLoggedIn = await adapter.checkLoginStatus(account.cookieData || '');
-          
-          if (!isLoggedIn) {
-            await accountRepository.update(account.id, { status: 'expired' });
-            wsManager.sendToUser(account.userId, WS_EVENTS.ACCOUNT_UPDATED, {
-              account: { id: account.id, status: 'expired' },
-            });
-            logger.info(`Account ${account.id} (${account.accountName}) expired`);
-            return;
-          }
+          // 创建 sync_account 任务加入队列(静默执行,前台不弹框)
+          taskQueueService.createTask(account.userId, {
+            type: 'sync_account',
+            title: `自动刷新: ${account.accountName || account.platform}`,
+            description: `定时刷新账号 ${account.accountName} 的状态和信息`,
+            priority: 'low', // 自动任务使用低优先级,不影响用户主动操作
+            silent: true,    // 静默执行,前台不弹框显示
+            accountId: account.id,
+          });
           
-          // 登录有效,获取最新账号信息
-          try {
-            const accountInfo = await adapter.getAccountInfo(account.cookieData || '');
-            
-            if (accountInfo) {
-              // 更新账号信息
-              const updateData: Partial<PlatformAccount> = {
-                fansCount: accountInfo.fansCount ?? account.fansCount,
-                worksCount: accountInfo.worksCount ?? account.worksCount,
-                updatedAt: new Date(),
-              };
-              
-              // 如果有头像更新
-              if (accountInfo.avatarUrl && accountInfo.avatarUrl !== account.avatarUrl) {
-                updateData.avatarUrl = accountInfo.avatarUrl;
-              }
-              
-              await accountRepository.update(account.id, updateData);
-              
-              // 通知前端账号已更新
-              wsManager.sendToUser(account.userId, WS_EVENTS.ACCOUNT_UPDATED, {
-                account: { 
-                  id: account.id, 
-                  ...updateData,
-                  status: 'active',
-                },
-              });
-              
-              logger.info(`Account ${account.id} (${account.accountName}) refreshed: fans=${accountInfo.fansCount}, works=${accountInfo.worksCount}`);
-            }
-          } catch (infoError) {
-            // 获取账号信息失败不影响登录状态
-            logger.warn(`Failed to get account info for ${account.id}:`, infoError);
-          }
+          tasksCreated++;
+          logger.debug(`[Scheduler] Created refresh task for account ${account.id} (${account.accountName})`);
           
         } catch (error) {
-          logger.error(`Check account ${account.id} status failed:`, error);
+          logger.error(`[Scheduler] Failed to create refresh task for account ${account.id}:`, error);
         }
-      })();
-      
-      results.push(refreshPromise);
-      
-      // 控制并发数
-      if (results.length >= concurrencyLimit) {
-        await Promise.all(results);
-        results.length = 0;
       }
+      
+      logger.info(`[Scheduler] Account refresh tasks created: ${tasksCreated} tasks, ${skipped} skipped`);
+    } finally {
+      // 释放锁
+      this.isRefreshingAccounts = false;
+      logger.debug('[Scheduler] Released refresh lock');
     }
-    
-    // 等待剩余的任务完成
-    if (results.length > 0) {
-      await Promise.all(results);
-    }
-    
-    logger.info('Account refresh completed');
   }
   
   /**

+ 53 - 27
server/src/services/AccountService.ts

@@ -112,6 +112,30 @@ export class AccountService {
     return this.formatAccount(account);
   }
 
+  /**
+   * 获取账号的 Cookie 数据(用于客户端打开后台)
+   */
+  async getAccountCookie(userId: number, accountId: number): Promise<string> {
+    const account = await this.accountRepository.findOne({
+      where: { id: accountId, userId },
+    });
+    if (!account) {
+      throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
+    }
+    
+    if (!account.cookieData) {
+      throw new AppError('账号没有 Cookie 数据', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR);
+    }
+    
+    // 尝试解密 Cookie
+    try {
+      return CookieManager.decrypt(account.cookieData);
+    } catch {
+      // 如果解密失败,返回原始数据
+      return account.cookieData;
+    }
+  }
+
   async addAccount(userId: number, data: AddPlatformAccountRequest & {
     accountInfo?: {
       accountId?: string;
@@ -271,6 +295,7 @@ export class AccountService {
 
         // 解析 Cookie - 支持两种格式
         let cookieList: { name: string; value: string; domain: string; path: string }[];
+        let cookieParseError = false;
         try {
           // 先尝试 JSON 格式
           cookieList = JSON.parse(decryptedCookies);
@@ -279,57 +304,56 @@ export class AccountService {
           cookieList = this.parseCookieString(decryptedCookies, platform);
           if (cookieList.length === 0) {
             logger.error(`Invalid cookie format for account ${accountId}`);
-            updateData.status = 'expired';
-            needReLogin = true;
+            cookieParseError = true;
           }
         }
 
-        if (cookieList.length > 0) {
-          // 使用无头浏览器检查 Cookie 是否有效
+        if (cookieList.length > 0 && !cookieParseError) {
+          // 第一步:通过浏览器检查 Cookie 是否有效(访问后台页面检测是否被重定向到登录页)
           const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
-
+          
           if (!isValid) {
-            // Cookie 已过期
+            // Cookie 已过期,需要重新登录
             updateData.status = 'expired';
             needReLogin = true;
-            logger.warn(`Account ${accountId} cookie expired, need re-login`);
+            logger.warn(`Account ${accountId} (${account.accountName}) cookie expired, need re-login`);
           } else {
-            // Cookie 有效,使用无头浏览器获取账号信息
+            // Cookie 有效,尝试获取账号信息
             updateData.status = 'active';
-
+            
             try {
               const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
 
-              // 检查是否获取到有效信息
-              const isValidProfile = profile.accountName &&
-                profile.accountName !== `${platform}账号` &&
-                profile.accountName !== '未知账号' &&
-                profile.accountName !== '抖音账号';
+              // 检查是否获取到有效信息(排除默认名称)
+              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}`);
+                logger.info(`Refreshed account info for ${platform}: ${profile.accountName}, fans: ${profile.fansCount}`);
               } else {
-                logger.warn(`Could not fetch account info for ${accountId}, but cookie is valid`);
+                // 获取的信息无效,但 Cookie 有效,保持 active 状态
+                logger.warn(`Could not fetch valid account info for ${accountId}, but cookie is valid`);
               }
             } catch (infoError) {
-              logger.warn(`Failed to fetch account info for ${accountId}:`, infoError);
-              // Cookie 有效但获取信息失败,保持 active 状态
+              // 获取账号信息失败,但 Cookie 检查已通过,保持 active 状态
+              logger.warn(`Failed to fetch account info for ${accountId}, but cookie is valid:`, infoError);
             }
           }
         }
+        // Cookie 解析失败时,不改变状态
       } catch (error) {
         logger.error(`Failed to refresh account ${accountId}:`, error);
-        // 不抛出错误,只更新时间戳
+        // 不抛出错误,不改变状态,只更新时间戳
       }
-    } else {
-      // 没有 Cookie 数据
-      updateData.status = 'expired';
-      needReLogin = true;
     }
+    // 没有 Cookie 数据时,不改变状态
 
     await this.accountRepository.update(accountId, updateData);
 
@@ -481,10 +505,12 @@ export class AccountService {
       try {
         const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
         
-        // 检查是否获取到有效信息
-        const isValidProfile = profile.accountName && 
-          profile.accountName !== `${platform}账号` &&
-          profile.accountName !== '未知账号';
+        // 检查是否获取到有效信息(排除默认名称)
+        const defaultNames = [
+          `${platform}账号`, '未知账号', '抖音账号', '小红书账号', 
+          '快手账号', '视频号账号', 'B站账号', '头条账号', '百家号账号'
+        ];
+        const isValidProfile = profile.accountName && !defaultNames.includes(profile.accountName);
 
         if (isValidProfile) {
           return {

+ 156 - 3
server/src/services/HeadlessBrowserService.ts

@@ -32,6 +32,15 @@ const PLATFORM_API_CONFIG: Record<string, {
       return resp?.status_code === 0 || resp?.BaseResp?.StatusCode === 0;
     },
   },
+  baijiahao: {
+    // 使用设置信息接口检查 Cookie 有效性
+    checkUrl: 'https://baijiahao.baidu.com/user-ui/cms/settingInfo',
+    isValidResponse: (data: unknown) => {
+      const resp = data as { errno?: number; data?: { name?: string } };
+      // errno 为 0 且有 name 表示 Cookie 有效
+      return resp?.errno === 0 && !!resp?.data?.name;
+    },
+  },
 };
 
 export interface AccountInfo {
@@ -90,10 +99,21 @@ export interface CookieData {
 class HeadlessBrowserService {
   /**
    * 检查 Cookie 是否有效
-   * 优先使用 API 检查,如果平台不支持 API 则使用浏览器检查
+   * 优先使用 Python 服务(通过浏览器访问后台检测),回退到 API 检查
    */
   async checkCookieValid(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
-    // 优先使用 API 检查(更快更可靠)
+    // 优先使用 Python 服务检查(通过浏览器访问后台页面,检测是否被重定向到登录页)
+    const pythonAvailable = await this.checkPythonServiceAvailable();
+    if (pythonAvailable) {
+      try {
+        const result = await this.checkLoginViaPython(platform, cookies);
+        return result;
+      } catch (error) {
+        logger.warn(`[Python API] Check login failed, falling back to API check:`, error);
+      }
+    }
+
+    // 回退到 API 检查
     const apiConfig = PLATFORM_API_CONFIG[platform];
     if (apiConfig) {
       return this.checkCookieValidByApi(platform, cookies, apiConfig);
@@ -104,6 +124,47 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 通过 Python 服务检查登录状态(浏览器访问后台页面,检测是否需要登录)
+   */
+  private async checkLoginViaPython(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
+    const pythonUrl = PYTHON_SERVICE_URL;
+
+    // 构建 Cookie 字符串
+    const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
+
+    logger.info(`[Python API] Checking login status for ${platform}`);
+
+    const response = await fetch(`${pythonUrl}/check_login`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        platform,
+        cookie: cookieString,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`Python API returned ${response.status}`);
+    }
+
+    const result = await response.json() as {
+      success: boolean;
+      valid: boolean;
+      need_login: boolean;
+      message?: string;
+      error?: string;
+    };
+
+    logger.info(`[Python API] Check login result for ${platform}: valid=${result.valid}, need_login=${result.need_login}`);
+
+    if (!result.success) {
+      throw new Error(result.error || 'Check login failed');
+    }
+
+    return result.valid && !result.need_login;
+  }
+
+  /**
    * 通过 API 检查 Cookie 是否有效
    */
   private async checkCookieValidByApi(
@@ -160,6 +221,7 @@ class HeadlessBrowserService {
       douyin: 'https://creator.douyin.com/',
       bilibili: 'https://member.bilibili.com/',
       kuaishou: 'https://cp.kuaishou.com/',
+      baijiahao: 'https://baijiahao.baidu.com/',
     };
     return referers[platform] || '';
   }
@@ -366,6 +428,11 @@ class HeadlessBrowserService {
    * 获取账号信息(优先使用 Python API,回退到无头浏览器)
    */
   async fetchAccountInfo(platform: PlatformType, cookies: CookieData[]): Promise<AccountInfo> {
+    // 百家号使用特殊的 API 方式获取账号信息
+    if (platform === 'baijiahao') {
+      return this.fetchBaijiahaoAccountInfo(cookies);
+    }
+
     // 对于支持的平台,优先尝试使用 Python API 获取作品列表
     const supportedPlatforms: PlatformType[] = ['douyin', 'xiaohongshu', 'kuaishou', 'weixin_video'];
 
@@ -1507,12 +1574,98 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 获取百家号账号信息 - 通过 API 获取
+   */
+  private async fetchBaijiahaoAccountInfo(cookies: CookieData[]): Promise<AccountInfo> {
+    logger.info('[Baijiahao] Fetching account info via API...');
+    logger.info(`[Baijiahao] Cookie count: ${cookies.length}`);
+
+    try {
+      // 构建 Cookie 字符串
+      const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
+      logger.info(`[Baijiahao] Cookie string length: ${cookieString.length}`);
+
+      // 调用百家号设置信息 API
+      const response = await fetch('https://baijiahao.baidu.com/user-ui/cms/settingInfo', {
+        method: 'GET',
+        headers: {
+          'Cookie': cookieString,
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+          'Accept': 'application/json, text/plain, */*',
+          'Referer': 'https://baijiahao.baidu.com/',
+          'Origin': 'https://baijiahao.baidu.com',
+        },
+      });
+
+      logger.info(`[Baijiahao] API response status: ${response.status}`);
+
+      if (!response.ok) {
+        logger.warn(`[Baijiahao] API returned ${response.status}`);
+        return this.getDefaultAccountInfo('baijiahao');
+      }
+
+      const text = await response.text();
+      logger.info(`[Baijiahao] API response (first 500 chars): ${text.substring(0, 500)}`);
+
+      let result: {
+        errno: number;
+        errmsg: string;
+        data?: {
+          name?: string;
+          avatar?: string;
+          new_uc_id?: number;
+          wish?: string;
+        };
+      };
+
+      try {
+        result = JSON.parse(text);
+      } catch (parseError) {
+        logger.error('[Baijiahao] Failed to parse JSON response:', parseError);
+        return this.getDefaultAccountInfo('baijiahao');
+      }
+
+      logger.info(`[Baijiahao] API result: errno=${result.errno}, errmsg=${result.errmsg}, has data=${!!result.data}`);
+
+      if (result.errno === 0 && result.data) {
+        const accountInfo: AccountInfo = {
+          accountId: result.data.new_uc_id ? String(result.data.new_uc_id) : `baijiahao_${Date.now()}`,
+          accountName: result.data.name || '百家号账号',
+          avatarUrl: result.data.avatar || '',
+          fansCount: 0,
+          worksCount: 0,
+        };
+
+        logger.info(`[Baijiahao] Successfully fetched account info: ${accountInfo.accountName}, avatar: ${accountInfo.avatarUrl?.substring(0, 50)}`);
+        return accountInfo;
+      }
+
+      logger.warn(`[Baijiahao] API returned error: ${result.errmsg}`);
+      return this.getDefaultAccountInfo('baijiahao');
+    } catch (error) {
+      logger.error('[Baijiahao] Failed to fetch account info:', error);
+      return this.getDefaultAccountInfo('baijiahao');
+    }
+  }
+
+  /**
    * 获取默认账号信息
    */
   private getDefaultAccountInfo(platform: PlatformType): AccountInfo {
+    // 平台友好名称映射
+    const platformNames: Record<string, string> = {
+      douyin: '抖音',
+      xiaohongshu: '小红书',
+      kuaishou: '快手',
+      weixin_video: '视频号',
+      bilibili: 'B站',
+      toutiao: '头条',
+      baijiahao: '百家号',
+    };
+    const name = platformNames[platform] || platform;
     return {
       accountId: `${platform}_${Date.now()}`,
-      accountName: `${platform}账号`,
+      accountName: `${name}账号`,
       avatarUrl: '',
       fansCount: 0,
       worksCount: 0,

+ 1 - 0
server/src/services/TaskQueueService.ts

@@ -56,6 +56,7 @@ class TaskQueueService {
       status: 'pending',
       progress: 0,
       priority: request.priority || 'normal',
+      silent: request.silent || false, // 静默执行标记
       createdAt: new Date().toISOString(),
       accountId: request.accountId,
       userId, // 存储 userId 用于任务执行

+ 2 - 0
shared/src/types/task.ts

@@ -34,6 +34,7 @@ export interface Task {
   totalSteps?: number;     // 总步骤数
   currentStepIndex?: number; // 当前步骤索引
   priority: TaskPriority;
+  silent?: boolean;        // 静默执行,前台不弹框显示
   createdAt: string;
   startedAt?: string;
   completedAt?: string;
@@ -118,6 +119,7 @@ export interface CreateTaskRequest {
   title?: string;
   description?: string;
   priority?: TaskPriority;
+  silent?: boolean;        // 静默执行,前台不弹框显示
   accountId?: number;
   data?: Record<string, unknown>;
 }

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio