Назад към всички

browser-session-manager

// > **TL;DR**: 本文记录如何通过 Playwright + 浏览器 Session 管理,实现即梦AI视频生成的全自动化。包含图片压缩、Cookie 注入、表单填写、视频提交等完整流程。

$ git log --oneline --stat
stars:1,933
forks:367
updated:March 4, 2026
SKILL.mdreadonly

即梦AI (Jimeng) 视频自动化生成完全指南

TL;DR: 本文记录如何通过 Playwright + 浏览器 Session 管理,实现即梦AI视频生成的全自动化。包含图片压缩、Cookie 注入、表单填写、视频提交等完整流程。


📋 背景

即梦AI (jimeng.jianying.com) 是字节跳动旗下的 AI 视频生成平台。官方提供了 Web 界面,但没有开放 API。当我们需要批量生成视频或集成到工作流时,就需要通过浏览器自动化来实现。

🎯 核心挑战

  1. 登录状态保持: 需要正确处理 Cookies 和 LocalStorage
  2. 文件上传: 需要压缩图片到合适大小(WebP 格式最佳)
  3. 动态表单: 需要等待元素加载、处理弹窗
  4. 提交检测: 需要检测页面跳转和生成状态

🛠️ 技术栈

  • Playwright: 浏览器自动化框架
  • ImageMagick: 图片压缩(convert 命令)
  • Node.js: 脚本运行环境

📁 目录结构

workspace/
├── screenshots/
│   └── YYYYMMDD_HHMMSS/     # 按时间戳组织的截图
│       ├── 01_initial.png
│       ├── 02_start_uploaded.png
│       └── ...
├── skills/
│   ├── browser-session-manager/  # Session 管理 Skill
│   └── web-form-automation/      # 表单自动化 Skill
└── session-data.json            # Cookie 数据文件

🚀 完整实现步骤

1. 准备阶段:图片压缩

即梦对上传图片有大小限制,必须先压缩:

# 转换为 WebP 格式,压缩到 30-50KB
convert start.png start.webp
convert end.png end.webp

# 验证大小
ls -lh *.webp
# -rw-r--r-- 1 node node 88K Feb 15 14:59 start.webp
# -rw-r--r-- 1 node node 54K Feb 15 14:59 end.webp

⚠️ 重要: PNG 原图可能 4MB+,直接上传会失败。WebP 格式可压缩 99% 且画质损失极小。


2. Session 数据准备

从浏览器导出 Cookie 和 LocalStorage,保存为 JSON:

{
  "exportTime": "2026-02-15T14:55:58.374Z",
  "url": "https://jimeng.jianying.com/ai-tool/home?type=video",
  "hostname": "jimeng.jianying.com",
  "cookies": [
    {
      "name": "sessionid",
      "value": "xxx",
      "domain": ".jianying.com",
      "path": "/",
      "secure": false,
      "httpOnly": true,
      "sameSite": "unspecified"
    }
  ],
  "localStorage": {
    "dreamina__generator_video_modelKey": "\"dreamina_seedance_40_pro\"",
    "DREAMINA_THEME": "light"
  },
  "sessionStorage": {}
}

获取方法: 使用浏览器扩展(如 "Cookie-Editor")导出,或从 DevTools Application 面板复制。


3. 核心自动化脚本

const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');

async function generateVideo(sessionFile, startImage, endImage, prompt, screenshotDir) {
  // 读取 Session 数据
  const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
  
  // 启动浏览器
  const browser = await chromium.launch({ 
    headless: true, 
    args: ['--no-sandbox'] 
  });
  
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // ===== 关键步骤 1: 设置 Cookies =====
  if (sessionData.cookies) {
    const cookiesByDomain = {};
    for (const cookie of sessionData.cookies) {
      // 修复 sameSite 值
      let sameSite = cookie.sameSite;
      if (sameSite === 'unspecified') sameSite = 'Lax';
      if (sameSite === 'no_restriction') sameSite = 'None';
      
      const fixedCookie = { ...cookie, sameSite };
      const domain = cookie.domain || '.jianying.com';
      if (!cookiesByDomain[domain]) cookiesByDomain[domain] = [];
      cookiesByDomain[domain].push(fixedCookie);
    }
    
    for (const [domain, cookies] of Object.entries(cookiesByDomain)) {
      try {
        await context.addCookies(cookies);
      } catch (e) {
        console.log(`Cookie 设置失败: ${e.message}`);
      }
    }
  }
  
  // ===== 关键步骤 2: 导航并注入 LocalStorage =====
  await page.goto('https://jimeng.jianying.com/ai-tool/home?type=video', { 
    waitUntil: 'domcontentloaded',
    timeout: 60000 
  });
  
  await page.waitForTimeout(3000);
  
  // 注入 LocalStorage
  await page.evaluate((data) => {
    for (const [key, value] of Object.entries(data.localStorage || {})) {
      try { localStorage.setItem(key, value); } catch (e) {}
    }
  }, sessionData);
  
  // 刷新使存储生效
  await page.reload({ waitUntil: 'domcontentloaded' });
  await page.waitForTimeout(3000);
  
  // ===== 关键步骤 3: 处理登录弹窗 =====
  try {
    const agreeBtn = await page.getByText('同意').first();
    if (await agreeBtn.isVisible({ timeout: 3000 })) {
      await agreeBtn.click();
      await page.waitForTimeout(2000);
    }
  } catch (e) {
    // 没有弹窗,继续
  }
  
  // ===== 关键步骤 4: 上传首帧图片 =====
  const startBtn = await page.getByText('首帧').first();
  await startBtn.click();
  await page.waitForTimeout(1500);
  
  const fileInput = await page.locator('input[type="file"]').first();
  await fileInput.setInputFiles(startImage);
  await page.waitForTimeout(3000);  // 等待上传完成
  
  // ===== 关键步骤 5: 上传尾帧图片 =====
  const endBtn = await page.getByText('尾帧').first();
  await endBtn.click();
  await page.waitForTimeout(1500);
  
  const fileInputs = await page.locator('input[type="file"]').all();
  await fileInputs[fileInputs.length - 1].setInputFiles(endImage);
  await page.waitForTimeout(3000);
  
  // ===== 关键步骤 6: 选择模型 =====
  const modelBtn = await page.getByText('Seedance 2.0 Fast').first();
  await modelBtn.click();
  await page.waitForTimeout(2000);
  
  // 选择 Seedance 2.0(非 Fast 版本)
  const seedance20 = await page.getByText('Seedance 2.0').nth(1);
  await seedance20.click();
  await page.waitForTimeout(1500);
  
  // ===== 关键步骤 7: 设置时长为 15s =====
  const durationBtn = await page.getByText('5s').first();
  await durationBtn.click();
  await page.waitForTimeout(1500);
  
  const fifteen = await page.getByText('15s').first();
  await fifteen.click();
  await page.waitForTimeout(1500);
  
  // ===== 关键步骤 8: 输入提示词 =====
  const textarea = await page.locator('textarea').first();
  await textarea.click();
  
  // 使用 pressSequentially 模拟真实打字,触发输入事件
  await textarea.pressSequentially(prompt, { delay: 30 });
  await page.waitForTimeout(3000);
  
  // ===== 关键步骤 9: 提交生成 =====
  const submit = await page.locator('button[class*="submit"]').first();
  await submit.click({ force: true });  // force: true 确保能点击
  await page.waitForTimeout(2000);
  
  // ===== 关键步骤 10: 检测页面跳转 =====
  await page.waitForTimeout(3000);
  const currentUrl = page.url();
  
  if (currentUrl.includes('/generate')) {
    console.log('✅ 成功跳转到生成页面');
  }
  
  // 等待 5 秒查看生成状态
  await page.waitForTimeout(5000);
  await page.screenshot({ path: path.join(screenshotDir, 'final.png') });
  
  await browser.close();
}

⚠️ 踩坑记录

1. Cookie sameSite 问题

错误: browserContext.addCookies: cookies[3].sameSite: expected one of (Strict|Lax|None)

解决: 导出时 `sameSite: