Skill: openclawzhangchong/doubao-opencli (version 1.0.0)

--- name: doubao-opencli description: >- 豆包（Doubao）AI 助手 OpenCLI 技能包。通过 opencli browser 桥接复用 Edge 浏览器登录态， 提供豆包对话、AI 播客生成（文字/网页链接/PDF 三种输入方式，自动裁剪广告）、 PPT 生成、图像生成（文生图）、会议摘要、批量提问、对话备份等功能。 使用场景：(1) 需要通过 CLI 或脚本自动化操作豆包时；(2) 需要批量生成播客/图片/PPT时； (3) 需要备份豆包对话记录时。前置条件：Edge 已登录 https://www.doubao.com/chat，opencli 已安装。 --- # 豆包 OpenCLI 技能包 通过 `opencli` 浏览器桥接复用 Edge 中已登录的豆包会话，提供 CLI 自动化操作。 ## 功能一览 | 功能 | 脚本 | 说明 | |------|------|------| | ✅ 提问 | `doubao_toolkit.ps1 ask` | 向豆包提问 | | ✅ 对话 | `doubao_toolkit.ps1 chat` | 交互式对话 | | ✅ 图像生成 | `doubao_image_gen.ps1` | 文生图 + 自动下载 | | ✅ AI 播客 | `doubao_podcast_gen.ps1` | 播客生成 + 自动下载 | | 🆕 PPT 生成 | `doubao_ppt_gen.ps1` | PPT 生成 + 自动下载 | | ⚠️ 会议 | `doubao_toolkit.ps1 meetings` | 依赖 history，新版UI可能不可用 | | ⚠️ 备份 | `doubao_toolkit.ps1 backup` | 依赖 history，新版UI可能不可用 | | ✅ 批量 | `doubao_toolkit.ps1 batch` | 批量提问 | ## 前置条件 - **Edge** 已登录 [https://www.doubao.com/chat](https://www.doubao.com/chat) - **opencli** 已安装（`npm install -g @jackwener/opencli`） - **opencli 浏览器桥接扩展** 已在 Edge 中安装 > ⚠️ 用户默认浏览器是 **Microsoft Edge**（非 Chrome），记录在 `TOOLS.md`。 ## 脚本说明 ### 1. `doubao_toolkit.ps1` — 工具箱 多功能 CLI 工具箱，包装 `opencli doubao` 命令： ```powershell # 检查连接状态 .\scripts\doubao_toolkit.ps1 status # 向豆包提问 .\scripts\doubao_toolkit.ps1 ask "你的问题" # 交互式对话 .\scripts\doubao_toolkit.ps1 chat # 图像生成（自动下载到 output/doubao_images/） .\scripts\doubao_toolkit.ps1 image "一只橘猫，水彩风格" # 会议相关 .\scripts\doubao_toolkit.ps1 meetings # 查看会议列表 .\scripts\doubao_toolkit.ps1 summary <id> # 获取会议摘要 # 备份 / 批量 .\scripts\doubao_toolkit.ps1 backup # 备份所有对话 .\scripts\doubao_toolkit.ps1 batch <file.txt> # 批量提问 ``` **注意**：`history` 命令在豆包新版 UI 下可能不可用，会影响 `meetings`、`summary`、`backup` 等依赖 `history` 的功能。 ### 2. `doubao_image_gen.ps1` — 图像生成 独立的图像生成脚本，通过 opencli browser 自动化豆包 AI 创作页面的文生图流程： ```powershell .\scripts\doubao_image_gen.ps1 "提示词" # 仅生成，显示链接 .\scripts\doubao_image_gen.ps1 "提示词" -download # 生成并下载到本地 ``` **工作流程**： 1. 打开 `https://www.doubao.com/chat/create-image` 2. 自动定位输入框（动态 ref 号） 3. 填入提示词，点击 `#flow-end-msg-send` 发送按钮 4. 等待 ~15-20s 生成完成 5. 从页面提取 4 张图片的 URL 6. 用 `Invoke-WebRequest` 下载到 `output/doubao_images/` **输出文件**： - `output/doubao_images/doubao_YYYYMMDD_HHmmss_N.jpg` — 下载的图片 - `output/doubao_images/gen_log_YYYYMMDD_HHmmss.md` — 生成记录（含图片链接） ### 3. `doubao_podcast_gen.ps1` — AI 播客生成 独立的 AI 播客生成脚本，通过 opencli browser 自动化豆包 AI 播客的生成和下载，**自动裁剪开头广告**： ```powershell # 📝 文字模式（主题/文章/大纲/脚本） .\scripts\doubao_podcast_gen.ps1 -Text "介绍人工智能的发展历史" .\scripts\doubao_podcast_gen.ps1 -Text "IaaS PaaS SaaS 三种云计算服务模式的区别和应用场景" # 🔗 网页模式（输入链接，豆包自动抓取） .\scripts\doubao_podcast_gen.ps1 -Url "https://example.com/article" # 📄 文件模式（上传 PDF，仅支持 .pdf） .\scripts\doubao_podcast_gen.ps1 -File "C:\docs\我的文章.pdf" # 自定义参数 .\scripts\doubao_podcast_gen.ps1 -Text "你的主题" -WaitPlay 30 -TrimSeconds 0 ``` **输入方式**（三选一）： - `-Text`：文字输入，支持主题、完整文章、大纲、脚本 - `-Url`：网页链接输入，豆包自动抓取网页内容生成播客 - `-File`：PDF 文件上传（仅 .pdf），豆包根据文档内容生成播客 **工作流程**： 1. 打开 `https://www.doubao.com/chat` 2. 自动定位并点击 **AI 播客** 按钮（动态 ref） 3. 输入内容（文字/网页链接/PDF文件，三选一） 4. 点击发送按钮 `#flow-end-msg-send` 5. 等待播客生成完成（~10-60秒，文件模式可能更久） 6. 自动点击播放按钮 7. 持续播放等待下载按钮可用（默认等待 40 秒） 8. 点击下载按钮 [593]/[591]（向下箭头 SVG） 9. 文件自动保存到 Edge 默认下载目录 10. 搬运到 `output/doubao_podcasts/` 11. **自动裁剪开头 4 秒广告**（使用 ffmpeg） **重要发现**： - 下载按钮的 class 包含 `downloadBtn-Iyn2FU`，通过 `span[role=img]` 的父 `div[tabindex]` 定位 - ⚠️ **关键**：opencli browser click 命令无法触发下载按钮，必须使用 JS 原生事件（dispatchEvent 触发 mousedown + mouseup + click + pointerdown） - 点击下载后 **自动保存** 到系统 Downloads 目录，不弹出保存对话框 - 下载文件格式为 **.wav**，文件名以播客标题命名 - 下载按钮需要播放 20-40 秒后才变为可用（不可用时点击无反应） - 输出文件使用时间戳命名（`AI新闻播客_20260511_173500.wav`），避免覆盖旧文件 - 下载检测改用文件快照对比（名称+大小+修改时间），不再依赖文件名比对 - 示例输出：`output/doubao_podcasts/云计算服务模式的差异与场景.wav`（28MB，9分51秒） **自动裁剪广告**： - 脚本默认使用 ffmpeg 裁剪音频开头 **4 秒**（豆包播客开头通常有品牌广告） - 通过 `-TrimSeconds` 参数自定义裁剪秒数，设为 `0` 则跳过裁剪 - 裁剪后原文件被替换，最终输出的就是去广告版本 - 需要系统已安装 ffmpeg（否则跳过裁剪，保留原文件） **输出文件**： - `output/doubao_podcasts/<播客标题>.wav` — 下载的播客音频文件（已自动裁剪开头广告） ### 4. `doubao_ppt_gen.ps1` — PPT 生成（实测验证 ✅） AI PPT 生成脚本，通过 opencli browser 自动化豆包 PPT 生成与下载流程。 ```powershell # 按主题生成（推荐） .\scripts\doubao_ppt_gen.ps1 -Topic "人工智能发展史" # 按大纲生成 .\scripts\doubao_ppt_gen.ps1 -Outline "1. 引言`n2. 技术原理`n3. 应用场景`n4. 未来展望" # 按草稿文档生成 .\scripts\doubao_ppt_gen.ps1 -DraftFile "C:\docs\my_draft.md" # 通过工具箱 .\scripts\doubao_toolkit.ps1 ppt "人工智能发展史" ``` **实测数据**（2026-05-11）： - 生成 20 页 PPT 耗时：**15 分 47 秒** - 生成 23 页 PPT 耗时：**13 分 01 秒** - 下载文件大小：**~3.4 MB**（PPTX 格式） - 下载方式：工具栏下载按钮 → 选择 PPTX → **自动保存到 Downloads** **PPT 工作流程**（实测验证）： 1. 打开 `https://www.doubao.com/chat` 2. **不要点击 PPT 快捷按钮** — 直接在普通聊天模式的 textarea 中输入提示词 3. 点击发送按钮 `#flow-end-msg-send`（**只在普通聊天模式存在**，PPT 快捷模式无发送按钮） 4. **5 阶段进度监控**（每 15 秒检查一次）： - 阶段 1：思考状态 + 素材收集（最长阶段，AI 联网搜索参考资料） - 阶段 2：思考优化 + 补充收集（1-2 分钟） - 阶段 3：生成原创图片（1-5 分钟，AI 生成封面/背景/配图） - 阶段 4：PPT 合成与渲染（5-6 分钟，左右分栏模式） - 阶段 5：已完成（检测到 "已完成PPT生成" 标志） 5. 生成完成后，**点击 PPT 卡片**（封面图或标题区域）→ 打开左右分栏编辑器 6. **点击工具栏"下载"按钮** → 弹出菜单（PPTX / PDF） 7. **选择 PPTX** → 文件自动保存到系统 Downloads 目录 8. 搬运到 `output/doubao_ppt/` 目录 **关键发现**（实测）： - ⚠️ **PPT 快捷按钮模式没有发送按钮** — 必须在普通聊天模式发送 - 发送按钮 ID：`#flow-end-msg-send`（普通聊天模式） - PPT 编辑器是一个 iframe（`/partner/ccm-slides-v3/`），但**下载按钮在主页面的 DOM 中**，不在 iframe 内 - 下载按钮特征：`data-dbx-name="button"`，文本"下载"，`aria-haspopup="menu"` - 格式选择菜单项特征：`role="menuitem"`，文本"PPTX" - **不需要确认保存对话框** — 文件自动保存到 `Downloads` 目录 - 每次页面加载后元素 ref 号会变化，脚本使用动态查找而非硬编码 **参数说明**： | 参数 | 说明 | 默认值 | |------|------|--------| | `-Topic` | PPT 主题 | — | | `-Outline` | PPT 大纲文本 | — | | `-DraftFile` | 草稿文档路径（.txt/.md） | — | | `-WaitTimeout` | 总等待超时（秒） | 1200（20 分钟） | | `-MonitorInterval` | 监控间隔（秒） | 15 | | `-OutputDir` | 输出目录 | `output/doubao_ppt/` | **播客脚本 `doubao_podcast_gen.ps1` 参数说明**： | 参数 | 说明 | 默认值 | |------|------|--------| | `-Text` | 播客文字内容（主题/文章/大纲/脚本），与 `-Url`、`-File` 三选一 | — | | `-Url` | 网页链接，豆包自动抓取生成播客，与 `-Text`、`-File` 三选一 | — | | `-File` | PDF 文件路径（仅 .pdf），与 `-Text`、`-Url` 三选一 | — | | `-OutputDir` | 输出目录 | `output/doubao_podcasts/` | | `-WaitPlay` | 播放等待秒数（下载按钮需播放后才可用） | 40 | | `-TrimSeconds` | 裁剪音频开头N秒（去广告，设为0则不裁剪） | 4 | **阶段日志示例**： ``` [11:15:30] [PHASE] [1/5] 打开豆包聊天页... [11:15:35] [PHASE] [2/5] 触发 PPT 生成... [11:15:40] [PHASE] [3/5] 监控生成进度... [11:15:55] ┌─ 进入阶段1: 思考状态 + 素材收集 [11:15:55] [11:17:30] └─ 阶段1完成，耗时 95s [11:17:30] ┌─ 进入阶段2: 思考优化 + 补充收集 [11:17:30] [11:18:45] └─ 阶段2完成，耗时 75s [11:18:45] ┌─ 进入阶段3: 生成原创图片 [11:18:45] [11:22:10] └─ 阶段3完成，耗时 205s [11:22:10] ┌─ 进入阶段4: PPT合成与渲染 [11:22:10] [11:28:30] ✅ PPT 生成完成！总耗时 765s [11:28:33] [4/5] 下载 PPT... [11:28:36] -> 点击 PPT 标题打开编辑器... [11:28:41] -> 找下载按钮... [11:28:41] -> 点击下载按钮... [11:28:43] -> 选择 PPTX 格式... [11:28:46] ✅ 下载成功: C:\Users\xxx\Downloads\xxx.pptx [11:28:46] [5/5] 搬运到输出目录... [11:28:46] ✅ 已搬运到: output/doubao_ppt/ppt_20260511_112846.pptx ``` **输出文件**： - `output/doubao_ppt/ppt_YYYYMMDD_HHmmss.pptx` — 下载的 PPT 文件 - `output/doubao_ppt/logs/ppt_log_YYYYMMDD_HHmmss.md` — 详细阶段日志 ## 已知问题 - **`history` 命令不可用**：豆包新版 UI 下 `opencli doubao history` 返回空，导致 `meetings`、`summary`、`backup` 等功能受限。 - **图像生成页面的发送按钮 ID**：`#flow-end-msg-send`，在页面重新加载后输入框 ref 号会变化，脚本使用正则动态匹配。 - **`ask` 命令的 JSON 输出**：`opencli doubao ask -f json` 的输出可能包含更新提示等非 JSON 文本，已使用 `2>$null` 过滤。 ## ⚠️ 经验教训（后续修改务必注意） > 完整经验文档见 [`knowledge/doubao-podcast-bug-fix.md`](../../knowledge/doubao-podcast-bug-fix.md) ### 1. 不要信任 ref 号 opencli browser 的 ref 号是**动态分配**的，每次页面加载都不同。永远用 class/文本/位置定位元素，而非硬编码 ref。 ### 2. 不要信任 click 返回值 `clicked: true` 不代表操作生效了。对于复杂 UI（如下载按钮），用 JS 原生事件更可靠： ```javascript el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})); el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true})); el.dispatchEvent(new MouseEvent('click', {bubbles: true})); el.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true})); ``` ### 3. 输出文件永远用时间戳命名 避免同名文件覆盖导致内容混淆。格式：`AI新闻播客_20260511_173500.wav` ### 4. 下载检测用快照对比 仅靠文件名不够，要同时检查大小和修改时间。记录下载前的快照，对比真正的变化。 ### 5. JS eval 比 ref 号更可靠 `opencli browser eval` 直接操作真实 DOM，不受 ref 号变化影响。 ### 6. 窗口必须最大化 豆包页面在窗口不够大时部分按钮（如 AI 播客）可能不显示。用 JS 最大化： ```javascript window.moveTo(0,0); window.resizeTo(window.screen.availWidth, window.screen.availHeight); ``` ## 未来扩展方向 - 支持比例/风格/模型参数的自定义 - 参考图上传功能 - AI 抠图/擦除/重绘等图片编辑模式 - 视频生成（豆包已支持） - 批量图片生成

path: scripts/doubao_image_gen.ps1
content: param(
    [string]$Prompt,
    [switch]$Download
)

$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$parentDir = Split-Path $scriptDir -Parent
$OUTPUT_DIR = $parentDir + "\output\doubao_images"
if (-not (Test-Path $OUTPUT_DIR)) { New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null }

$CREATE_IMAGE_URL = "https://www.doubao.com/chat/create-image"

function Write-Step {
    param($Msg, $Color = "Green")
    Write-Host ("  >> " + $Msg) -ForegroundColor $Color
}

if (-not $Prompt) {
    Write-Host "用法: .\doubao_image_gen.ps1 `"提示词`" [-download]"
    exit
}

Write-Step "打开 AI 创作页面..."
opencli browser open $CREATE_IMAGE_URL 2>&1 | Out-Null
Start-Sleep 3

# 找 textbox - 搜索 contenteditable=true 的行
Write-Step "找到输入框..."
$state = opencli browser state 2>&1
$textboxRef = ""
$lines = $state -split "`n"
foreach ($line in $lines) {
    if ($line -match '\[(\d+)\].*role=textbox.*contenteditable=true') {
        $textboxRef = $matches[1]
        break
    }
}
if (-not $textboxRef) {
    Write-Host "  [FAIL] 找不到输入框" -ForegroundColor Red
    exit 1
}
Write-Host ("  输入框 ref: " + $textboxRef) -ForegroundColor DarkGray

Write-Step "填入提示词..."
$fullPrompt = $Prompt
opencli browser fill $textboxRef $fullPrompt 2>&1 | Out-Null
Start-Sleep 1

Write-Step "点击生成..."
opencli browser click "#flow-end-msg-send" 2>&1 | Out-Null

Write-Step "等待生成中..."
$maxWait = 60
$waited = 0
$found = $false

while ($waited -lt $maxWait) {
    Start-Sleep 5
    $waited = $waited + 5
    $extract = opencli browser extract 2>&1
    if ($extract -match '!\[image\]\(https://[^)]+\.jpeg') {
        $found = $true
        Write-Host ("  [OK] 用时 " + $waited + "s") -ForegroundColor Green
        break
    }
    Write-Host ("  ... " + $waited + "s") -ForegroundColor DarkYellow
}

if (-not $found) {
    Write-Host "  [FAIL] 超时" -ForegroundColor Red
    exit 1
}

Write-Step "提取图片链接..."
$urls = @()
$matches = [regex]::Matches($extract, 'https://p\d+-flow-imagex-sign\.byteimg\.com[^"''\s\)]+\.jpeg[^"''\s\)]*')
foreach ($m in $matches) {
    if ($m.Value -notin $urls) { $urls = $urls + $m.Value }
}

Write-Host ("  [OK] 共 " + $urls.Count + " 张图片") -ForegroundColor Cyan

$idx = 0
foreach ($url in $urls) {
    $idx = $idx + 1
    Write-Host ("  [" + $idx + "] " + $url) -ForegroundColor Cyan
    if ($Download) {
        $file = $OUTPUT_DIR + "\doubao_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + "_" + $idx + ".jpg"
        try {
            Invoke-WebRequest -Uri $url -OutFile $file
            Write-Host ("      已保存: " + $file) -ForegroundColor Green
        } catch {
            Write-Host ("      下载失败: " + $_.Exception.Message) -ForegroundColor Yellow
        }
    }
}

# 保存链接记录
$logFile = $OUTPUT_DIR + "\gen_log_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".md"
"# 豆包图片生成记录" | Out-File $logFile -Encoding UTF8
"" | Out-File $logFile -Encoding UTF8 -Append
"提示词: " + $Prompt | Out-File $logFile -Encoding UTF8 -Append
"时间: " + (Get-Date) | Out-File $logFile -Encoding UTF8 -Append
"" | Out-File $logFile -Encoding UTF8 -Append
$idx = 0
foreach ($url in $urls) {
    $idx = $idx + 1
    "- 图片 " + $idx + ": " + $url | Out-File $logFile -Encoding UTF8 -Append
}
Write-Host ("  记录: " + $logFile) -ForegroundColor DarkGray


path: scripts/doubao_podcast_gen.ps1
content: <#
.SYNOPSIS
  豆包 AI 播客生成脚本 - 通过 opencli browser 桥接自动生成并下载播客音频

.DESCRIPTION
  使用 opencli browser 桥接复用 Edge 登录态，自动完成：
  1. 打开豆包聊天页
  2. 点击 AI 播客按钮
  3. 输入内容（支持文字/网页链接/PDF文件三种方式）
  4. 等待播客生成
  5. 播放播客（等待下载按钮可用）
  6. 点击下载按钮
  7. 从 Downloads 搬运到 output 目录
  8. 裁剪开头广告（ffmpeg）

.PARAMETER Text
  播客主题、完整文章、大纲或脚本文字（与 -Url、-File 三选一）

.PARAMETER Url
  网页链接，豆包自动抓取内容生成播客（与 -Text、-File 三选一）

.PARAMETER File
  PDF 文件路径，豆包根据文档内容生成播客（与 -Text、-Url 三选一）

.PARAMETER OutputDir
  输出目录（默认: output/doubao_podcasts/）

.PARAMETER WaitPlay
  播放等待秒数（默认: 40，下载按钮通常在播放20-40秒后可用）

.PARAMETER TrimSeconds
  切掉音频前N秒（默认: 4，豆包播客开头通常有广告，设为0则不裁剪）

.EXAMPLE
  # 主题模式
  .\doubao_podcast_gen.ps1 -Text "介绍人工智能的发展历史"

  # 网页模式
  .\doubao_podcast_gen.ps1 -Url "https://example.com/article"

  # 文件模式
  .\doubao_podcast_gen.ps1 -File "C:\docs\我的文章.pdf"

  # 自定义参数
  .\doubao_podcast_gen.ps1 -Text "你的主题" -WaitPlay 30 -TrimSeconds 0
#>

param(
    [string]$Text,

    [string]$Url,

    [string]$File,

    [string]$OutputDir = "",

    [int]$WaitPlay = 40,

    [int]$TrimSeconds = 4
)

# --- 参数校验：Text、Url、File 三选一 ---
$inputModes = @()
if ($Text) { $inputModes += "Text" }
if ($Url) { $inputModes += "Url" }
if ($File) { $inputModes += "File" }

if ($inputModes.Count -eq 0) {
    Write-Host "❌ 请指定输入方式：-Text（文字）、-Url（网页链接）、-File（PDF文件），三选一" -ForegroundColor Red
    exit 1
}
if ($inputModes.Count -gt 1) {
    Write-Host "❌ -Text、-Url、-File 不能同时使用，请三选一" -ForegroundColor Red
    exit 1
}

# 如果是文件模式，检查文件是否存在
if ($File -and -not (Test-Path $File)) {
    Write-Host "❌ 文件不存在: $File" -ForegroundColor Red
    exit 1
}

# 如果是文件模式，检查是否为 PDF
if ($File) {
    $ext = [System.IO.Path]::GetExtension($File).ToLower()
    if ($ext -ne ".pdf") {
        Write-Host "❌ 仅支持 PDF 文件上传，当前文件类型: $ext" -ForegroundColor Red
        exit 1
    }
}

# --- 路径配置 ---
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$Workspace = Resolve-Path "$ScriptDir\..\..\.."
if ($OutputDir -eq "") {
    $OutputDir = Join-Path $Workspace "output" | Join-Path -ChildPath "doubao_podcasts"
}
$DownloadsDir = Join-Path $env:USERPROFILE "Downloads"

# 确保输出目录存在
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null

Write-Host "=== 豆包 AI 播客生成器 ===" -ForegroundColor Cyan
switch ($inputModes[0]) {
    "Text" { Write-Host "输入方式: 文字" -ForegroundColor Yellow; Write-Host "内容: $Text" -ForegroundColor Gray }
    "Url"  { Write-Host "输入方式: 网页链接" -ForegroundColor Yellow; Write-Host "链接: $Url" -ForegroundColor Gray }
    "File" { Write-Host "输入方式: PDF 文件" -ForegroundColor Yellow; Write-Host "文件: $File" -ForegroundColor Gray }
}
Write-Host ""

# --- Step 1: 打开豆包聊天页并最大化窗口 ---
Write-Host "[1/8] 打开豆包聊天页并最大化窗口..." -ForegroundColor Green
$pageResult = opencli browser open "https://www.doubao.com/chat" 2>$null
if ($LASTEXITCODE -ne 0) {
    Write-Host "❌ 打开页面失败，请检查 Edge 登录状态" -ForegroundColor Red
    exit 1
}
Start-Sleep 3
# 最大化窗口，确保所有按钮可见（豆包页面在窗口不够大时会隐藏部分按钮）
opencli browser eval "window.moveTo(0,0); window.resizeTo(window.screen.availWidth, window.screen.availHeight);" 2>$null
Start-Sleep 1
Write-Host "  ✅ 窗口已最大化" -ForegroundColor Green

# --- Step 2: 点击 AI 播客按钮 ---
Write-Host "[2/8] 查找并点击 AI 播客按钮..." -ForegroundColor Green
$state = opencli browser state 2>$null
$podcastRef = $null

# 方法1: 用 CSS 选择器找 AI 播客按钮
$findResult = opencli browser find --css "button:has(div:text('AI播客')), div:text('AI播客')" 2>$null
if ($LASTEXITCODE -eq 0 -and $findResult) {
    $podcastRef = ($findResult | Select-String -Pattern "ref=(\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
}

# 方法2: 在 state 中搜索 AI 播客文本
if (-not $podcastRef) {
    $podcastLine = $state | Select-String -Pattern "AI 播客|AI播客" | Select-Object -First 1
    if ($podcastLine) {
        $podcastRef = ($podcastLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
    }
}

if ($podcastRef) {
    opencli browser click $podcastRef 2>$null
    Write-Host "  -> 点击 ref [$podcastRef]" -ForegroundColor Gray
} else {
    Write-Host "  -> 尝试点击已知位置的 AI 播客按钮 [305]..." -ForegroundColor Gray
    opencli browser click 305 2>$null
}
Start-Sleep 2

# --- Step 3: 输入内容（文字/网页/文件 三选一） ---
switch ($inputModes[0]) {
    "Text" {
        Write-Host "[3/8] 输入播客文字内容..." -ForegroundColor Green
        $state = opencli browser state 2>$null
        $inputRef = $null

        $findResult = opencli browser find --css "textarea[placeholder*='输入主题'], textarea[placeholder*='添加网页']" 2>$null
        if ($LASTEXITCODE -eq 0 -and $findResult) {
            $inputRef = ($findResult | Select-String -Pattern "ref=(\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
        }

        if (-not $inputRef) {
            $textareaLine = $state | Select-String -Pattern "textarea" | Select-Object -First 1
            if ($textareaLine) {
                $inputRef = ($textareaLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
            }
        }

        if ($inputRef) {
            opencli browser fill $inputRef --value "$Text" 2>$null
            Write-Host "  -> 填入 ref [$inputRef]" -ForegroundColor Gray
        } else {
            Write-Host "  -> 尝试直接填入 textarea..." -ForegroundColor Gray
            opencli browser fill 301 --value "$Text" 2>$null
        }
        Start-Sleep 1
    }

    "Url" {
        Write-Host "[3/8] 输入网页链接..." -ForegroundColor Green
        $state = opencli browser state 2>$null
        $inputRef = $null

        $findResult = opencli browser find --css "textarea[placeholder*='输入主题'], textarea[placeholder*='添加网页']" 2>$null
        if ($LASTEXITCODE -eq 0 -and $findResult) {
            $inputRef = ($findResult | Select-String -Pattern "ref=(\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
        }

        if (-not $inputRef) {
            $textareaLine = $state | Select-String -Pattern "textarea" | Select-Object -First 1
            if ($textareaLine) {
                $inputRef = ($textareaLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
            }
        }

        if ($inputRef) {
            opencli browser fill $inputRef --value "$Url" 2>$null
            Write-Host "  -> 填入链接 ref [$inputRef]" -ForegroundColor Gray
        } else {
            Write-Host "  -> 尝试直接填入 textarea..." -ForegroundColor Gray
            opencli browser fill 301 --value "$Url" 2>$null
        }
        Start-Sleep 1
    }

    "File" {
        Write-Host "[3/8] 上传 PDF 文件..." -ForegroundColor Green
        # 找文件上传 input
        $state = opencli browser state 2>$null
        $fileInputRef = $null

        $fileInputLine = $state | Select-String -Pattern "type=file" | Select-Object -First 1
        if ($fileInputLine) {
            $fileInputRef = ($fileInputLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
        }

        if ($fileInputRef) {
            # 使用 opencli browser upload 上传文件（如果支持）
            # 否则尝试用 fill 传入文件路径
            $uploadResult = opencli browser fill $fileInputRef --value "$File" 2>$null
            Write-Host "  -> 上传文件到 ref [$fileInputRef]" -ForegroundColor Gray
            Start-Sleep 2
        } else {
            # 备用方案：尝试点击上传区域
            Write-Host "  -> 尝试点击上传区域..." -ForegroundColor Gray
            opencli browser click 303 2>$null
            Start-Sleep 1
            # 通过 fill 到文件输入
            $uploadResult = opencli browser fill 295 --value "$File" 2>$null
            Start-Sleep 2
        }
        Write-Host "  ✅ 文件已上传" -ForegroundColor Green
    }
}

# --- Step 4: 点击发送按钮 ---
Write-Host "[4/8] 发送请求..." -ForegroundColor Green
# 尝试多种方式找到发送按钮
$sendClicked = $false

# 方式1: 通过 SVG data-opencli-ref
opencli browser click 296 2>$null
$sendClicked = $true
Start-Sleep 3

# 方式2: 如果方式1没生效，尝试点 SVG 的父容器
if (-not $sendClicked) {
    opencli browser eval @"
    const svg = document.querySelector('svg[data-opencli-ref]');
    if (svg) {
        const btn = svg.closest('div[role=button], button, div.clickable');
        if (btn) btn.click();
    }
"@ 2>$null
    Start-Sleep 2
}

# --- Step 5: 等待播客生成 ---
Write-Host "[5/8] 等待播客生成..." -ForegroundColor Green
$maxWait = 120
$waited = 0
$podcastReady = $false

while ($waited -lt $maxWait) {
    Start-Sleep 5
    $waited += 5
    $state = opencli browser state 2>$null
    
    # 检查是否出现播客卡片（有封面图或时长信息）
    if ($state -match "\d+:\d+.*\d+:\d+" -or $state -match "byteimg.*tos-cn-i") {
        $podcastReady = $true
        Write-Host "  -> 播客已生成！（等待 $waited 秒）" -ForegroundColor Green
        break
    }
    
    # 也检查页面中是否有播客播放器UI
    $hasPlayer = opencli browser eval "document.querySelector('audio') !== null || document.body.innerText.includes('正在播放') || document.body.innerText.includes('播客') && document.body.innerText.includes('秒')" 2>$null
    if ($hasPlayer -eq "true") {
        $podcastReady = $true
        Write-Host "  -> 检测到播客播放器！（等待 $waited 秒）" -ForegroundColor Green
        break
    }
    
    Write-Host "  -> 等待中... ($waited 秒)" -ForegroundColor Gray
}

if (-not $podcastReady) {
    Write-Host "⚠️ 播客可能还在生成中，继续尝试..." -ForegroundColor Yellow
}

# --- Step 6: 播放并等待下载按钮可用 ---
Write-Host "[6/8] 播放播客，等待下载按钮可用（约${WaitPlay}秒）..." -ForegroundColor Green

# 用 JS 查找播放按钮并点击
$playResult = opencli browser eval @"
(function() {
    // 找播放按钮：播放器区域中有 SVG 且文本包含 1x 的附近按钮
    const allDivs = document.querySelectorAll('div[tabindex]');
    for (const el of allDivs) {
        if (el.offsetParent === null) continue;
        const rect = el.getBoundingClientRect();
        const svg = el.querySelector('svg');
        // 播放按钮：在播放器区域左侧，有 SVG，无 aria-haspopup
        if (svg && rect.top > 350 && rect.top < 400 && rect.left > 270 && rect.left < 310 && rect.width > 20) {
            el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true, view: window}));
            el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true, view: window}));
            el.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));
            return 'played_' + el.tagName;
        }
    }
    return 'play_btn_not_found';
})();
"@ 2>$null

Write-Host "  -> JS播放结果: $playResult" -ForegroundColor Gray

# 等待下载按钮可用
$waited = 0
while ($waited -lt $WaitPlay) {
    Start-Sleep 5
    $waited += 5
    Write-Host "  -> 播放中... ($waited 秒)" -ForegroundColor Gray
}

# --- Step 7: 点击下载按钮（JS原生事件触发） ---
Write-Host "[7/8] 点击下载按钮..." -ForegroundColor Green

# 先记录当前 Downloads 目录的快照
$downloadCheckTime = Get-Date
$beforeSnapshot = @{}
Get-ChildItem $DownloadsDir -Filter "*.wav" | ForEach-Object { $beforeSnapshot[$_.Name] = $_.Length }

# 用 JS 原生事件触发下载（opencli browser click 有时无法触发）
$downloadResult = opencli browser eval @"
(function() {
    // 查找下载按钮：class 包含 downloadBtn 的 span[role=img] 的父 div[tabindex]
    const allDivs = document.querySelectorAll('div[tabindex]');
    for (const el of allDivs) {
        const span = el.querySelector('span[role=img]');
        if (span && span.className.includes('downloadBtn')) {
            // 使用原生 MouseEvent 和 PointerEvent 确保触发
            el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true, view: window}));
            el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true, view: window}));
            el.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));
            el.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, cancelable: true, view: window}));
            return 'dispatched_events_on_download_btn';
        }
    }
    return 'download_btn_not_found';
})();
"@ 2>$null

Write-Host "  -> JS触发结果: $downloadResult" -ForegroundColor Gray
Start-Sleep 3

# 等待下载完成（最多等20秒）
$waitDownload = 0
$downloadedFile = $null
while ($waitDownload -lt 20) {
    Start-Sleep 3
    $waitDownload += 3
    
    $currentFiles = Get-ChildItem $DownloadsDir -Filter "*.wav"
    foreach ($f in $currentFiles) {
        $oldSize = $beforeSnapshot[$f.Name]
        if (-not $oldSize) {
            $downloadedFile = $f.FullName
            Write-Host "  -> 检测到新文件: $($f.Name)" -ForegroundColor Gray
            break
        } elseif ($f.Length -ne $oldSize -and $f.LastWriteTime -ge $downloadCheckTime) {
            $downloadedFile = $f.FullName
            Write-Host "  -> 文件已更新: $($f.Name)" -ForegroundColor Gray
            break
        }
    }
    if ($downloadedFile) { break }
    Write-Host "  -> 等待下载... ($waitDownload 秒)" -ForegroundColor Gray
}

if ($downloadedFile -and (Test-Path $downloadedFile)) {
    Write-Host "  ✅ 下载成功: $downloadedFile" -ForegroundColor Green
    
    # 用时间戳命名，避免覆盖旧文件
    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $newFileName = "AI新闻播客_${timestamp}.wav"
    $outputFile = Join-Path $OutputDir $newFileName
    
    Copy-Item $downloadedFile $outputFile -Force
    Write-Host "  ✅ 已搬运到: $outputFile" -ForegroundColor Green
    
    $fileInfo = Get-Item $outputFile
    $sizeMB = [math]::Round($fileInfo.Length / 1MB, 2)
    Write-Host "  📊 文件大小: ${sizeMB}MB" -ForegroundColor Cyan
} else {
    Write-Host "  ❌ 下载失败：未检测到新文件" -ForegroundColor Red
    exit 1
}

# --- Step 8: 裁剪开头广告（ffmpeg） ---
if ($TrimSeconds -gt 0) {
    Write-Host "[8/8] 裁剪音频开头 ${TrimSeconds} 秒（去广告）..." -ForegroundColor Green
    
    $trimmedFile = Join-Path $OutputDir "$([System.IO.Path]::GetFileNameWithoutExtension($outputFile))_去广告.wav"
    
    $ffmpegCmd = "ffmpeg -y -i `"$outputFile`" -ss $TrimSeconds -c copy `"$trimmedFile`" 2>&1"
    $ffmpegResult = Invoke-Expression $ffmpegCmd
    
    if ($LASTEXITCODE -eq 0 -and (Test-Path $trimmedFile)) {
        # 删除原文件，用裁剪后的替换
        Remove-Item $outputFile -Force
        Rename-Item $trimmedFile -NewName ([System.IO.Path]::GetFileName($outputFile)) -Force
        
        $trimmedInfo = Get-Item $outputFile
        $trimmedSizeMB = [math]::Round($trimmedInfo.Length / 1MB, 2)
        Write-Host "  ✅ 裁剪完成！已切掉前 ${TrimSeconds} 秒" -ForegroundColor Green
        Write-Host "  📊 最终文件: $outputFile (${trimmedSizeMB}MB)" -ForegroundColor Cyan
        
        # 用 ffprobe 获取裁剪后时长
        $duration = ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$outputFile" 2>$null
        if ($duration) {
            $totalSec = [math]::Round([double]$duration)
            $mins = [math]::Floor($totalSec / 60)
            $secs = $totalSec % 60
            Write-Host "  ⏱️  裁剪后时长: ${mins}分${secs}秒" -ForegroundColor Cyan
        }
    } else {
        Write-Host "  ⚠️ 裁剪失败，保留原文件（请确保 ffmpeg 已安装）" -ForegroundColor Yellow
        Write-Host "  ffmpeg 输出: $ffmpegResult" -ForegroundColor Gray
    }
} else {
    Write-Host "[8/8] 跳过裁剪（TrimSeconds=0）" -ForegroundColor Gray
}

Write-Host ""
Write-Host "=== 完成 ===" -ForegroundColor Cyan

path: scripts/doubao_ppt_gen.ps1
content: <#
.SYNOPSIS
  豆包 AI PPT 生成与下载脚本 - 基于 opencli browser 实测流程

.DESCRIPTION
  使用 opencli browser 桥接复用 Edge 登录态，自动完成：
  1. 打开豆包聊天页，触发 PPT 生成
  2. 支持多种输入源：主题、大纲、草稿文档
  3. 监控生成阶段（思考→素材收集→图片生成→合成渲染→完成）
  4. 点击 PPT 标题打开编辑器
  5. 点击工具栏"下载"按钮 → 选择 PPTX → 自动下载

  实测总耗时：约 13-15 分钟（23页 PPT）
  下载方式：工具栏下载按钮 → 选择 PPTX 格式 → 自动保存到 Downloads

.PARAMETER Topic
  PPT 主题

.PARAMETER Outline
  PPT 大纲文本

.PARAMETER DraftFile
  草稿文档路径（支持 .txt/.md）

.PARAMETER OutputDir
  输出目录（默认: output/doubao_ppt/）

.PARAMETER WaitTimeout
  总等待超时（秒，默认: 1200 = 20分钟）

.PARAMETER MonitorInterval
  页面监控间隔（秒，默认: 15）

.EXAMPLE
  .\doubao_ppt_gen.ps1 -Topic "人工智能发展史"

  .\doubao_ppt_gen.ps1 -Outline "1. 引言`n2. 技术原理`n3. 应用场景"

  .\doubao_ppt_gen.ps1 -DraftFile "C:\docs\my_draft.md"
#>

param(
    [Parameter(ParameterSetName="Topic")]
    [string]$Topic,

    [Parameter(ParameterSetName="Outline")]
    [string]$Outline,

    [Parameter(ParameterSetName="Draft")]
    [string]$DraftFile,

    [string]$OutputDir = "",

    [int]$WaitTimeout = 1200,

    [int]$MonitorInterval = 15
)

# --- 路径配置 ---
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$Workspace = Resolve-Path "$ScriptDir\..\..\.."
if ($OutputDir -eq "") {
    $OutputDir = Join-Path $Workspace "output" | Join-Path -ChildPath "doubao_ppt"
}
$DownloadsDir = Join-Path $env:USERPROFILE "Downloads"
$LogDir = Join-Path $OutputDir "logs"

New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null

# --- 日志 ---
$LogFile = Join-Path $LogDir ("ppt_log_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".md")
function Write-Log {
    param([string]$Msg, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $line = "[$timestamp] [$Level] $Msg"
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
    switch ($Level) {
        "ERROR" { Write-Host $line -ForegroundColor Red }
        "WARN"  { Write-Host $line -ForegroundColor Yellow }
        "OK"    { Write-Host $line -ForegroundColor Green }
        "PHASE" { Write-Host $line -ForegroundColor Cyan }
        default { Write-Host $line -ForegroundColor Gray }
    }
}

# --- 辅助函数 ---
function Get-PageExtract {
    return opencli browser extract 2>$null
}

function Get-PageText {
    return opencli browser get text "main" 2>$null
}

function Wait-AndCheck {
    param([int]$Seconds = 5)
    Start-Sleep $Seconds
    return Get-PageExtract
}

# --- 输入验证 ---
$inputSource = ""
$inputValue = ""
if ($Topic) {
    $inputSource = "主题"
    $inputValue = $Topic
} elseif ($Outline) {
    $inputSource = "大纲"
    $inputValue = $Outline
} elseif ($DraftFile) {
    $inputSource = "草稿文档"
    if (-not (Test-Path $DraftFile)) {
        Write-Host "❌ 草稿文档不存在: $DraftFile" -ForegroundColor Red
        exit 1
    }
    $inputValue = Get-Content $DraftFile -Raw -Encoding UTF8
} else {
    Write-Host "❌ 请提供输入源：-Topic / -Outline / -DraftFile" -ForegroundColor Red
    Write-Host "用法: .\doubao_ppt_gen.ps1 -Topic `"PPT主题`"" -ForegroundColor Yellow
    exit 1
}

Write-Log "=== 豆包 AI PPT 生成器 ===" "PHASE"
Write-Log "输入源: $inputSource" "INFO"
Write-Log "输入值: $($inputValue.Substring(0, [Math]::Min(200, $inputValue.Length)))" "INFO"
Write-Log "输出目录: $OutputDir" "INFO"
Write-Log "总超时: ${WaitTimeout}s" "INFO"
Write-Log "日志文件: $LogFile" "INFO"
Write-Host ""

# ============================================================
# Step 1: 打开豆包聊天页
# ============================================================
Write-Log "[1/5] 打开豆包聊天页..." "PHASE"
$pageResult = opencli browser open "https://www.doubao.com/chat" 2>$null
if ($LASTEXITCODE -ne 0) {
    Write-Log "❌ 打开页面失败，请检查 Edge 登录状态" "ERROR"
    exit 1
}
Start-Sleep 5

# ============================================================
# Step 2: 触发 PPT 生成
# ============================================================
Write-Log "[2/5] 触发 PPT 生成..." "PHASE"

# 构造提示词
$promptText = ""
switch ($inputSource) {
    "主题" { $promptText = "请帮我生成一份关于「${Topic}」的PPT" }
    "大纲" { $promptText = "请根据以下大纲生成PPT：`n${Outline}" }
    "草稿文档" { $promptText = "请根据以下文档内容生成PPT：`n${inputValue}" }
}

# --- 2a: 填入提示词到 textarea ---
# 注意：使用普通聊天模式，不要点击 PPT 快捷按钮
# PPT 快捷按钮模式没有发送按钮，无法提交
Write-Log "  -> 找输入框（textarea）..." "INFO"

$state = opencli browser state 2>$null
$textareaRef = $null

# 找 textarea（普通聊天模式的输入框）
$findResult = opencli browser find --css "textarea[placeholder]" 2>$null
if ($LASTEXITCODE -eq 0 -and $findResult) {
    $textareaRef = ($findResult | Select-String -Pattern "ref=(\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
}

if ($textareaRef) {
    # 使用 eval 设置 textarea 的值（比 fill 更可靠）
    opencli browser eval "var ta = document.querySelector('textarea[placeholder]'); if(ta) { var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; setter.call(ta, '" + $promptText.Replace("'", "\'") + "'); ta.dispatchEvent(new Event('input', {bubbles:true})); 'filled' }" 2>$null
    Write-Log "  -> 填入 textarea ref [$textareaRef]" "INFO"
} else {
    Write-Log "  ⚠️ 找不到 textarea，尝试找 contenteditable div..." "WARN"
    $ceRef = $null
    $ceResult = opencli browser find --css "div[contenteditable=true]" 2>$null
    if ($LASTEXITCODE -eq 0 -and $ceResult) {
        $ceRef = ($ceResult | Select-String -Pattern "ref=(\d+)" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
    }
    if ($ceRef) {
        opencli browser eval "var tb = document.querySelector('div[contenteditable=true]'); if(tb) { tb.textContent = '" + $promptText.Replace("'", "\'") + "'; tb.dispatchEvent(new Event('input', {bubbles:true})); 'filled' }" 2>$null
        Write-Log "  -> 填入 contenteditable ref [$ceRef]" "INFO"
    } else {
        Write-Log "  ❌ 找不到任何输入框" "ERROR"
        exit 1
    }
}
Start-Sleep 2

# --- 2b: 点击发送按钮 ---
Write-Log "  -> 点击发送（#flow-end-msg-send）..." "INFO"
opencli browser click "#flow-end-msg-send" 2>$null
if ($LASTEXITCODE -ne 0) {
    Write-Log "  ⚠️ #flow-end-msg-send 未找到，尝试找发送按钮..." "WARN"
    # 备用：在 state 中找发送按钮
    $state = opencli browser state 2>$null
    $sendBtnRef = $null
    $sendLine = $state | Select-String -Pattern "flow-end-msg-send" | Select-Object -First 1
    if ($sendLine) {
        $sendBtnRef = ($sendLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
    }
    if ($sendBtnRef) {
        Write-Log "  -> 点击发送按钮 ref [$sendBtnRef]" "INFO"
        opencli browser click $sendBtnRef 2>$null
    } else {
        Write-Log "  ❌ 找不到发送按钮" "ERROR"
        exit 1
    }
}
Start-Sleep 5

# ============================================================
# Step 3: 监控生成进度
# ============================================================
Write-Log "[3/5] 监控生成进度（总超时 ${WaitTimeout}s）..." "PHASE"
Write-Log "开始监控时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" "INFO"
Write-Host ""

$startTime = Get-Date
$elapsed = 0
$lastPhase = 0
$phaseStartTime = $startTime
$phaseLog = @()
$stuckCount = 0
$lastContentHash = ""

while ($elapsed -lt $WaitTimeout) {
    Start-Sleep $MonitorInterval
    $elapsed = [math]::Round(((Get-Date) - $startTime).TotalSeconds)
    $now = Get-Date -Format "HH:mm:ss"

    $extract = Get-PageExtract
    $pageText = ""
    try { $pageText = (Get-PageText).value } catch {}

    $contentToCheck = $extract + " " + $pageText

    # 阶段检测
    $currentPhase = 0
    $phaseDesc = "等待开始..."

    if ($contentToCheck -match "已完成PPT生成|已完成.*PPT.*生成|PPT.*已完成") {
        $currentPhase = 5
        $phaseDesc = "PPT已完成"
    } elseif ($contentToCheck -match "正在生成PPT|正在生成.*PPT|PPT.*合成|PPT.*渲染|已生成PPT") {
        $currentPhase = 4
        $phaseDesc = "PPT合成与渲染"
    } elseif ($contentToCheck -match "生成.*图片|图片.*生成|已完成图片生成|配图|插图") {
        $currentPhase = 3
        $phaseDesc = "生成原创图片"
    } elseif ($contentToCheck -match "思考.*优化|补充.*收集|优化.*思考|深入思考|大纲.*整理") {
        $currentPhase = 2
        $phaseDesc = "思考优化 + 补充收集"
    } elseif ($contentToCheck -match "思考中|正在收集|收集素材|分析中|正在分析|PPT素材") {
        $currentPhase = 1
        $phaseDesc = "思考状态 + 素材收集"
    }

    # 阶段变更日志
    if ($currentPhase -ne $lastPhase) {
        if ($lastPhase -gt 0) {
            $phaseDuration = [math]::Round(((Get-Date) - $phaseStartTime).TotalSeconds)
            $phaseLog += "[阶段${lastPhase}] ${phaseDuration}s"
            Write-Log "  └─ 阶段${lastPhase}完成，耗时 ${phaseDuration}s" "OK"
        }
        if ($currentPhase -gt 0) {
            $phaseStartTime = Get-Date
            Write-Log "  ┌─ 进入阶段${currentPhase}: ${phaseDesc} [${now}]" "PHASE"
        }
        $lastPhase = $currentPhase
        $stuckCount = 0
    } else {
        $stuckCount++
    }

    # 完成检测
    if ($currentPhase -eq 5) {
        Write-Log "✅ PPT 生成完成！总耗时 ${elapsed}s" "OK"
        Write-Log "阶段记录: $($phaseLog -join ' → ')" "INFO"
        break
    }

    # 进度输出
    $progressPct = [math]::Round(($currentPhase / 5.0) * 100)
    Write-Host ("  [${now}] ${elapsed}s | ${progressPct}% | ${phaseDesc}") -ForegroundColor DarkGray

    # 卡住检测（连续 8 次无变化 ≈ 2 分钟）
    if ($stuckCount -ge 8) {
        $contentHash = ($extract | Get-FileHash -Algorithm MD5).Hash
        if ($contentHash -eq $lastContentHash) {
            Write-Log "⚠️ 页面 ${stuckCount} 次检查无变化（约 $($stuckCount * $MonitorInterval)s）" "WARN"
        }
        $lastContentHash = $contentHash
    }
}

if ($elapsed -ge $WaitTimeout) {
    Write-Log "❌ 超时 ${WaitTimeout}s，PPT 生成未完成" "ERROR"
    Write-Log "最后检测到的阶段: ${phaseDesc}" "INFO"
    Write-Host "`n⚠️ 超时了！请检查 Edge 浏览器中的豆包页面状态" -ForegroundColor Yellow
    exit 1
}

# ============================================================
# Step 4: 下载 PPT
# ============================================================
Write-Log "[4/5] 下载 PPT..." "PHASE"

$downloaded = $false
$downloadedFile = ""

# 记录下载前文件列表
$beforeFiles = @()
if (Test-Path $DownloadsDir) {
    $beforeFiles = @(Get-ChildItem $DownloadsDir -Filter "*.pptx" | Select-Object -ExpandProperty Name)
}

# --- 4a: 点击 PPT 卡片打开编辑器 ---
Write-Log "  -> 点击 PPT 卡片打开编辑器..." "INFO"

# 刷新 state
$state = opencli browser state 2>$null
$cardRef = $null

# 方法1: 找 PPT 卡片标题文字（通常包含 PPT 主题的关键字）
$titleLine = $state | Select-String -Pattern "创建时间" | Select-Object -First 1
if ($titleLine) {
    $cardRef = ($titleLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
}

# 方法2: 找 PPT 封面图片
if (-not $cardRef) {
    $imgLine = $state | Select-String -Pattern "alt=Asset cover" | Select-Object -First 1
    if ($imgLine) {
        $cardRef = ($imgLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
    }
}

# 方法3: 找 "已完成PPT生成" 附近的卡片区域
if (-not $cardRef) {
    $doneLine = $state | Select-String -Pattern "已完成PPT|已完成.*PPT" | Select-Object -First 1
    if ($doneLine) {
        $cardRef = ($doneLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
    }
}

if ($cardRef) {
    Write-Log "  -> 点击 PPT 卡片 ref [$cardRef]" "INFO"
    opencli browser click $cardRef 2>$null
    Start-Sleep 5
} else {
    Write-Log "  ⚠️ 找不到 PPT 卡片，尝试 JS 查找并点击..." "WARN"
    opencli browser eval "var imgs = document.querySelectorAll('img[alt*=cover]'); if(imgs.length > 0) { imgs[0].closest('[class*=card]')?.click() || imgs[0].parentElement?.click(); 'clicked' } else { 'no cover img' }" 2>$null
    Start-Sleep 5


# --- 4b: 找下载按钮并点击 ---
Write-Log "  -> 找下载按钮..." "INFO"

# 刷新 state
$state = opencli browser state 2>$null

# 方法1: 找包含"下载"文字的按钮（data-dbx-name="button"）
$downloadRef = $null
$downloadLine = $state | Select-String -Pattern "下载" | Select-Object -First 1
if ($downloadLine) {
    $downloadRef = ($downloadLine | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
}

# 方法2: 找 radix 菜单触发器（下载按钮有 aria-haspopup=menu）
if (-not $downloadRef) {
    # 找工具栏中第三个 button（放映、分享、下载）
    $toolbarBtns = $state | Select-String -Pattern "data-dbx-name=button" | Select-Object -First 5
    if ($toolbarBtns) {
        $btnLines = @($toolbarBtns)
        if ($btnLines.Count -ge 3) {
            $downloadRef = ($btnLines[2] | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
        }
    }
}

if ($downloadRef) {
    Write-Log "  -> 点击下载按钮 ref [$downloadRef]" "INFO"
    opencli browser click $downloadRef 2>$null
    Start-Sleep 2

    # --- 4c: 选择 PPTX 格式 ---
    Write-Log "  -> 选择 PPTX 格式..." "INFO"
    $state = opencli browser state 2>$null

    # 找菜单项（role=menuitem）
    $pptxRef = $null
    $menuItems = $state | Select-String -Pattern "role=menuitem" | Select-Object -First 5
    if ($menuItems) {
        $itemLines = @($menuItems)
        if ($itemLines.Count -ge 1) {
            # 第一个 menuitem 是 PPTX
            $pptxRef = ($itemLines[0] | Select-String -Pattern "\[(\d+)\]" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
        }
    }

    if ($pptxRef) {
        Write-Log "  -> 点击 PPTX 选项 ref [$pptxRef]" "INFO"
        opencli browser click $pptxRef 2>$null
        Start-Sleep 3

        # 等待下载完成（文件自动保存到 Downloads）
        Write-Log "  -> 等待下载完成..." "INFO"
        Start-Sleep 5

        # 检查下载
        if (Test-Path $DownloadsDir) {
            $afterFiles = @(Get-ChildItem $DownloadsDir -Filter "*.pptx" | Select-Object -ExpandProperty Name)
            $newFiles = $afterFiles | Where-Object { $_ -notin $beforeFiles }
            if ($newFiles) {
                $downloadedFile = Join-Path $DownloadsDir $newFiles[0]
                $downloaded = $true
                Write-Log "  ✅ 下载成功: $downloadedFile" "OK"
            } else {
                Write-Log "  ⚠️ 未检测到新 PPTX 文件，等待 10 秒再检查..." "WARN"
                Start-Sleep 10
                $afterFiles = @(Get-ChildItem $DownloadsDir -Filter "*.pptx" | Select-Object -ExpandProperty Name)
                $newFiles = $afterFiles | Where-Object { $_ -notin $beforeFiles }
                if ($newFiles) {
                    $downloadedFile = Join-Path $DownloadsDir $newFiles[0]
                    $downloaded = $true
                    Write-Log "  ✅ 下载成功: $downloadedFile" "OK"
                }
            }
        }
    } else {
        Write-Log "  ⚠️ 找不到 PPTX 菜单项" "WARN"
    }
} else {
    Write-Log "  ⚠️ 找不到下载按钮" "WARN"
}

# --- 下载失败处理 ---
if (-not $downloaded) {
    Write-Log "❌ 下载未成功" "ERROR"
    Write-Host ""
    Write-Host "⚠️ 下载未成功，可能原因：" -ForegroundColor Yellow
    Write-Host "  1. PPT 编辑器未正确打开" -ForegroundColor Yellow
    Write-Host "  2. 下载按钮位置变化" -ForegroundColor Yellow
    Write-Host "  3. 浏览器下载弹窗被拦截" -ForegroundColor Yellow
    Write-Host ""
    Write-Host "请手动在 Edge 浏览器中检查豆包页面，" -ForegroundColor Yellow
    Write-Host "找到下载按钮后点击下载 PPTX。" -ForegroundColor Yellow
    Write-Host "日志已保存到: $LogFile" -ForegroundColor Gray
    exit 1
}

# ============================================================
# Step 5: 搬运到输出目录
# ============================================================
Write-Log "[5/5] 搬运到输出目录..." "PHASE"

if ($downloadedFile -and (Test-Path $downloadedFile)) {
    $outputFileName = "ppt_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".pptx"
    $outputFile = Join-Path $OutputDir $outputFileName
    Copy-Item $downloadedFile $outputFile -Force
    Write-Log "  ✅ 已搬运到: $outputFile" "OK"

    $fileInfo = Get-Item $outputFile
    $sizeMB = [math]::Round($fileInfo.Length / 1MB, 2)
    Write-Log "  📊 文件大小: ${sizeMB}MB" "OK"
} else {
    Write-Log "  ⚠️ 下载文件不存在" "WARN"
    $latestPptx = Get-ChildItem $DownloadsDir -Filter "*.pptx" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
    if ($latestPptx) {
        $outputFileName = "ppt_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".pptx"
        $outputFile = Join-Path $OutputDir $outputFileName
        Copy-Item $latestPptx.FullName $outputFile -Force
        Write-Log "  ✅ 已搬运（从最近文件）: $outputFile" "OK"
    }
}

# --- 最终日志 ---
Write-Log "" "INFO"
Write-Log "=== 完成 ===" "PHASE"
Write-Log "总耗时: ${elapsed}s" "INFO"
Write-Log "阶段记录: $($phaseLog -join ' → ')" "INFO"
Write-Log "输出文件: $outputFile" "OK"
Write-Log "日志文件: $LogFile" "INFO"

Write-Host ""
Write-Host "=== 完成 ===" -ForegroundColor Cyan
Write-Host "总耗时: ${elapsed}s" -ForegroundColor Cyan
Write-Host "输出: $outputFile" -ForegroundColor Green
Write-Host "日志: $LogFile" -ForegroundColor Gray

path: scripts/doubao_toolkit.ps1
content: <#
.SYNOPSIS
    Doubao CLI Toolkit - based on opencli doubao
.DESCRIPTION
    Wrapper for opencli doubao commands with interactive usage.
    Requires Edge login at https://www.doubao.com/chat

    Usage:
        .\doubao_toolkit.ps1 status             Check login status
        .\doubao_toolkit.ps1 ask "question"      Ask Doubao
        .\doubao_toolkit.ps1 chat               Interactive chat mode
        .\doubao_toolkit.ps1 image "prompt"      Image generation (auto-download)
        .\doubao_toolkit.ps1 podcast "topic"    AI Podcast generation (auto-download)
        .\doubao_toolkit.ps1 ppt "topic"        PPT generation (auto-download)
        .\doubao_toolkit.ps1 meetings           List meetings
        .\doubao_toolkit.ps1 summary <id>       Get meeting summary
        .\doubao_toolkit.ps1 backup             Backup all conversations
        .\doubao_toolkit.ps1 batch <file.txt>   Batch questions
#>

param(
    [Parameter(Position = 0)]
    [ValidateSet('ask', 'chat', 'meetings', 'summary', 'backup', 'batch', 'status', 'image', 'podcast', 'ppt', 'help')]
    [string]$Action = 'help',

    [Parameter(Position = 1, ValueFromRemainingArguments = $true)]
    [string[]]$Args
)

# -- Config --
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$parentDir = Split-Path $scriptDir -Parent
$OUTPUT_DIR = $parentDir + '\output\doubao'
if (-not (Test-Path $OUTPUT_DIR)) { New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null }

# -- Helpers --
function Write-Result {
    param([string]$Title, [string]$Content)
    Write-Host "`n========================================" -ForegroundColor Cyan
    Write-Host "  $Title" -ForegroundColor Yellow
    Write-Host "========================================" -ForegroundColor Cyan
    Write-Host $Content
    Write-Host "----------------------------------------" -ForegroundColor DarkGray
}

# -- Actions --

function Invoke-Ask {
    param([string]$Question)
    if (-not $Question) {
        $Question = Read-Host 'Enter your question'
    }
    Write-Host '`n>> Asking Doubao...' -ForegroundColor Green
    $reply = opencli doubao ask "$Question" 2>$null
    if ($reply) { Write-Result -Title 'Doubao Reply' -Content $reply }
    else { Write-Host 'Ask failed' -ForegroundColor Red }
}

function Invoke-Chat {
    Write-Host '`n>> Doubao interactive mode (type exit to quit)' -ForegroundColor Green
    Write-Host '----------------------------------' -ForegroundColor DarkGray
    while ($true) {
        $q = Read-Host '`nYou'
        if ($q -eq 'exit' -or $q -eq 'quit') { break }
        $r = opencli doubao ask "$q" -f json 2>&1 | ConvertFrom-Json
        Write-Host '`nDoubao: ' -NoNewline
        Write-Host $r.Response -ForegroundColor Cyan
    }
}

function Invoke-Status {
    $raw = opencli doubao status -f json 2>$null
    if ($raw) { $s = $raw | ConvertFrom-Json; Write-Result -Title 'Doubao Connection Status' -Content ($s | Format-List | Out-String) }
    else { Write-Host 'Status fetch failed' -ForegroundColor Red }
}

function Invoke-Meetings {
    Write-Host '`n>> Fetching meeting list...' -ForegroundColor Green
    $history = opencli doubao history -f json 2>&1 | ConvertFrom-Json
    $meetings = $history | Where-Object { $_.Title -match 'meeting|Meeting|summary|transcript' }
    if ($meetings) {
        Write-Result -Title 'Meetings' -Content ($meetings | Format-Table Id, Title -AutoSize | Out-String)
    } else {
        Write-Host 'No meeting conversations found' -ForegroundColor Yellow
    }
}

function Invoke-Summary {
    param([string]$Id)
    if (-not $Id) {
        Write-Host 'Please provide a conversation ID, e.g.: summary abc123' -ForegroundColor Red
        return
    }
    Write-Host '`n>> Fetching meeting summary...' -ForegroundColor Green
    $summary = opencli doubao meeting-summary "$Id" -f json 2>&1 | ConvertFrom-Json
    Write-Result -Title 'Meeting Summary' -Content ($summary | Format-List | Out-String)

    $file = $OUTPUT_DIR + '\meeting_summary_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.md'
    $summary | ConvertTo-Json -Depth 5 | Out-File $file -Encoding utf8
    Write-Host 'Saved to: ' -NoNewline; Write-Host $file -ForegroundColor Green
}

function Invoke-Backup {
    Write-Host '`n>> Backing up Doubao conversations...' -ForegroundColor Green
    $history = opencli doubao history -f json 2>&1 | ConvertFrom-Json
    $backupFile = $OUTPUT_DIR + '\doubao_backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.json'

    $backup = @()
    $total = $history.Count
    $i = 0

    foreach ($item in $history) {
        $i = $i + 1
        $id = $item.Id
        if (-not $id) { continue }

        Write-Progress -Activity 'Backup' -Status ('Conversation ' + $i + '/' + $total) -PercentComplete (($i/$total)*100)
        try {
            $detail = opencli doubao detail "$id" -f json 2>&1 | ConvertFrom-Json
            $backup = $backup + @{
                Id = $id
                Title = $item.Title
                Detail = $detail
            }
        } catch {
            Write-Warning ('Skipping ' + $id + ' (fetch failed)')
        }
    }

    $backup | ConvertTo-Json -Depth 10 | Out-File $backupFile -Encoding utf8
    Write-Progress -Activity 'Backup' -Completed
    Write-Host ('Backup complete! ' + $backup.Count + ' conversations') -ForegroundColor Green
    Write-Host '  File: ' -NoNewline; Write-Host $backupFile -ForegroundColor Green
}

function Invoke-Batch {
    param([string]$FilePath)
    if (-not $FilePath -or -not (Test-Path $FilePath)) {
        Write-Host 'Please provide a question file path (one question per line)' -ForegroundColor Red
        return
    }

    $questions = Get-Content $FilePath | Where-Object { $_.Trim() -ne '' }
    $results = @()
    $i = 0

    Write-Host ('`n>> Batch processing ' + $questions.Count + ' questions...') -ForegroundColor Green

    foreach ($q in $questions) {
        $i = $i + 1
        Write-Progress -Activity 'Batch' -Status ('[' + $i + '/' + $questions.Count + '] ' + $q) -PercentComplete (($i/$questions.Count)*100)
        try {
            $r = opencli doubao ask "$q" -f json 2>&1 | ConvertFrom-Json
            $results = $results + @{ Question = $q; Response = $r.Response }
            Write-Host ('  [' + $i + '/' + $questions.Count + '] Done') -ForegroundColor DarkGreen
        } catch {
            $results = $results + @{ Question = $q; Response = '[FAIL] ' + $_.Exception.Message }
            Write-Host ('  [' + $i + '/' + $questions.Count + '] Failed') -ForegroundColor Red
        }
    }

    Write-Progress -Activity 'Batch' -Completed

    $outFile = $OUTPUT_DIR + '\batch_results_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.md'
    $md = '# Batch Results' + "`r`n"
    $md = $md + "`r`n"
    $md = $md + '| # | Question | Response |' + "`r`n"
    $md = $md + '|---|----------|----------|' + "`r`n"
    $j = 0
    while ($j -lt $results.Count) {
        $resp = $results[$j].Response -replace '\|', '\|'
        $num = $j + 1
        $trunc = $resp
        if ($trunc.Length -gt 100) { $trunc = $trunc.Substring(0, 100) + '...' }
        $md = $md + ('| ' + $num + ' | ' + $results[$j].Question + ' | ' + $trunc + ' |') + "`r`n"
        $j = $j + 1
    }
    [System.IO.File]::WriteAllText($outFile, $md, [System.Text.Encoding]::UTF8)
    Write-Host 'Batch complete! Results saved: ' -NoNewline; Write-Host $outFile -ForegroundColor Green
}

function Show-Help {
    Write-Host ''
    Write-Host '========================================' -ForegroundColor Cyan
    Write-Host '    Doubao Automation Toolkit' -ForegroundColor Yellow
    Write-Host '========================================' -ForegroundColor Cyan
    Write-Host ''
    Write-Host 'Usage:'
    Write-Host '  .\doubao_toolkit.ps1 status             Check login status'
    Write-Host '  .\doubao_toolkit.ps1 ask "question"      Ask Doubao'
    Write-Host '  .\doubao_toolkit.ps1 chat               Interactive chat mode'
    Write-Host '  .\doubao_toolkit.ps1 image "prompt"      Image generation (auto-download)'
    Write-Host '  .\doubao_toolkit.ps1 podcast "topic"    AI Podcast generation (auto-download)'
    Write-Host '  .\doubao_toolkit.ps1 ppt "topic"        PPT generation (auto-download)'
    Write-Host '  .\doubao_toolkit.ps1 meetings           List meetings'
    Write-Host '  .\doubao_toolkit.ps1 summary <id>       Get meeting summary'
    Write-Host '  .\doubao_toolkit.ps1 backup             Backup all conversations'
    Write-Host '  .\doubao_toolkit.ps1 batch <file.txt>   Batch questions'
    Write-Host ''
    Write-Host 'Prerequisites:'
    Write-Host '  Edge logged into https://www.doubao.com/chat'
    Write-Host '  opencli installed'
    Write-Host ''
}

# -- Main --
switch ($Action) {
    'ask'     { Invoke-Ask -Question ($Args -join ' ') }
    'chat'    { Invoke-Chat }
    'status'  { Invoke-Status }
    'meetings' { Invoke-Meetings }
    'summary' { Invoke-Summary -Id ($Args[0]) }
    'backup'  { Invoke-Backup }
    'batch'   { Invoke-Batch -FilePath ($Args[0]) }
    'image'   { $imgPrompt = $Args -join ' '; $imgScript = $scriptDir + '\doubao_image_gen.ps1'; & $imgScript $imgPrompt -download }
    'podcast' { $podcastText = $Args -join ' '; $podcastScript = $scriptDir + '\doubao_podcast_gen.ps1'; & $podcastScript -Text $podcastText }
    'ppt'     { $pptArg = $Args -join ' '; $pptScript = $scriptDir + '\doubao_ppt_gen.ps1'; & $pptScript -Topic $pptArg }
    default   { Show-Help }
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)