豆包 15 秒桥接 —— 本项目集成说明
==================================

来源
----
E:\AIGC\豆包15秒视频桥接插件\content-doubao.js
（原始形态是 Chrome/Edge MV3 扩展，manifest 里以 world:"MAIN"、document_start 注入）

本目录只保留 content-doubao.js 一个文件，刻意**不放 manifest.json**：
本项目不是把它当浏览器扩展加载，而是由主进程读文件后直接注入豆包页面。
若这里再放一份 manifest 并误用 session.loadExtension() 加载，会与注入路径
重复安装（两边都 hook window.fetch），因此不放。

接入方式
--------
文件：electron/doubao-account.ts
- 常量 DOUBAO_DURATION_BRIDGE_PATH 解析本目录（打包后在 process.resourcesPath 下）
- getDurationBridgeCode() 读取内容并包成 IIFE 后返回；打包版走缓存，dev 下每次重读
  （缓存是模块级，改完脚本不重启主进程不会刷新 ⇒ 重开豆包窗口即可生效）
- injectDurationBridge() 在 did-finish-load / did-navigate 时 executeJavaScript 注入

为什么必须包 IIFE
-----------------
本脚本顶层有 const/let 声明（CHAT_COMPLETION_PATH 等）。executeJavaScript 直接在
页面主世界求值，第二次注入会抛「Identifier ... has already been declared」。
包成函数作用域后可反复注入；脚本自带 window.__doubaoWorkbenchDurationMenuVersion
作为幂等守卫。

为什么不改动去水印插件
----------------------
去水印插件走的是另一条链路（扩展 + chrome.debugger 的 Fetch 域）：
  - 拦截 .com/alice/slot/action_bar_v3/get_item_conf 响应，往「时长」option_list
    里插 15s 选项（service-worker.js）
  - 拦截 im/chain/single 响应提取无水印资源
本脚本不碰这两条链路，只做「出站请求改写」：发送时把时长/模型/画面比例写进
提示词前缀，并移除原生 chat_ability 参数；4-10 秒不写前缀（走原生时长，不受影响）。

副作用边界
----------
插件默认处于惰性状态：未把滑杆拖到 11-15 秒时（window.__doubaoWorkbenchSelectedDuration
不是 11-15），fetch hook 直接透传，不修改任何请求。它挂在 window.fetch 外层，
内层是去水印插件的 hook，两者链式调用，互不覆盖。

自动确认（与时长解耦）
----------------------
早先的自动确认挂在「发送时捕获的握手状态」上，而那个状态要求「滑杆在 11-15 秒」
且「发送那一刻点中发送按钮」才建立 —— 10 秒、或漏点按钮时完全没人监听，
表现就是「豆包发了『参数确认』，但没人回」。

现在改为常驻监听：脚本始终盯住 .v_list_row 会话区，新出现的行只要命中
「参数确认 / 确认后 / 再开始」任一话术，就自动写入「确认，开始生成视频。」
并点发送 —— 不看时长，也不看发送状态。

三道防误发闸门：
  1. 安装后 6 秒宽限期：did-finish-load 时 SPA 的历史会话还没渲染完，
     这期间出现的行一律当历史消息处理；
  2. 一次涌入超过 10 行：判定为整块重渲染，同样按历史处理；
  3. 新行「文字静默期」：锚点是「文字最后一次变化」，静默满 5 秒才下结论，
     静默期内既不判定也不锁定。
     ⚠️ 锚点绝不能用「首次出现时刻」：那样一行出现满 N 秒就被拉黑，之后即使
        吐出关键词也不会再看一眼。豆包长回复要吐十几秒、关键词常在中后段出现，
        用「首次出现」必然误判（2026-10-07 真机事故的根因）。
     静默期设长的代价为零：命中关键词的行不走这条路，它只影响「判无关」的快慢。
已锁定的行不再回头 —— 即便之后内容变了也不响应（防历史行反复触发）。
另外，若同一批新行里已经出现「生成中」，则不补发。

聚焦 + 发送怎么做（2026-10-07 第三次修正：改成 Enter 发送）
------------------------------------------------------------
豆包输入框的键盘处理（线上前端 chat.<hash>.js 原文）：
  onKeyDown: e => { …; c && ("Enter"===e.key || 13===e.keyCode)
                    && !e.nativeEvent.isComposing && !e.shiftKey && !i
                    && (e.preventDefault(), n.trim() && c()) }
⇒ **发送是 Enter 驱动的，跟发送按钮没关系**。之前一直去点按钮，方向本身就错了：
  生成中豆包把发送/中断按钮二选一渲染，发送按钮整个不渲染（连 data-testid 都没有），
  于是永远报 send-unavailable —— 而 Enter 这条路压根不受影响。

所以改成：
  - 聚焦：直接照搬主进程 electron/doubao-account.ts#focusDoubaoInput 那套
    （它每天在跑「切窗口 → 聚焦 → Ctrl+V 贴图/贴提示词」，是唯一被实践验证过的）：
    ① focus() ② 在元素上派发真鼠标 mousedown/mouseup/click
    （豆包输入引擎容器上挂着 onClick → controller.focus()，只调 focus() 编辑器不接管光标）
    ③ 光标移到末尾
  - 写入：React 受控通道（原型链上的 native value setter + 派发 input 事件）
  - 提交：派发合成 KeyboardEvent（keydown/keypress/keyup，key=Enter、keyCode=13）。
    React 合成事件不校验 isTrusted，能正常触发上面那段 onKeyDown。
  - ⚠️ 写入与提交之间必须隔 200ms：onKeyDown 里判空读的是 React state 的 n.trim()，
    同一 tick 内派发 Enter 会读到空串然后静默不提交（真机现象正是「字在框里、发不出去」）。
  - 受理判据 dbwbIsSubmitAccepted：输入框被清空 / disabled / 进入生成态，任一成立即算发出。
  - 兜底：Enter 没受理且按钮确实可用时，才退回点按钮。

失败不再永久报废
----------------
原先一旦 composer 不可用，该行立刻被永久标记为已处理，再也不会回头。
现在失败（composer-busy / composer-write-failed / send-unavailable /
send-not-accepted）会回退标记并延迟 2.5 秒重试，每行最多 8 次；成功提交后绝不重入。
⚠️ 配套约束：隐藏握手行必须在「已提交」之后再做 —— 若提前 display:none，
   innerText 会读成空串，下一轮 isConfirmationReply 判 false，重试无从命中。
   （回归用例 case14 反向验证过：把这个顺序放回去，用例立刻 FAIL）

诊断：window.__doubaoWorkbenchAutoConfirm.phase
取值 sent（带 via=enter|click、focused） / stream-settling / composer-busy /
composer-write-failed / send-unavailable / send-not-accepted /
generation-already-started / generation-running-wait / generation-running-skip

日志取证：脚本用 console.warn 打 [DBWB-AC] 前缀（crash-probe 只收 warning/error 落盘），
落点 %APPDATA%\seedance-desktop\logs\diagnose-YYYYMMDD.log。
含 install-begin / installed / install-skip / dom（DOM 形状侦察）/ pending / idle /
skip-bulk / settling / stream-settling / ignore / targets / retry-later /
composer-candidates / button-candidates / phase=*。
（composer-candidates / button-candidates 里每一项都带 tid=data-testid 与 dis=禁用态）

必须先等这句「吐完」再动手（2026-10-07 第二次真机事故）
--------------------------------------------------------
豆包前端 chat.<hash>.js 里这块的真实渲染是：
  <textarea data-testid="chat_input_input" disabled={r} value={n} onChange=… />
  <p data-testid="chat_input_send_button" disableToSend={d}
     className={cx(u, { "!hidden": o, small: r })} onClick={h} onDisabledClick={m} />
紧挨着的中断按钮是 className={cx({ "!hidden": !o })} ⇒ **o 为真 = 正在生成/正在流式输出**。
即：豆包还在吐字时，输入框 disabled、发送按钮被 display:none 藏起来（让位给中断按钮）。

真机事故（14:11）：关键词在该条回复第 90 字符处就出现（整条 591 字符），脚本抢着写输入框 ——
字写进去了（disabled 不挡程序设值），但 React 不收（onChange 不派发）、按钮也不解禁，
用户看到的就是「输入框里有字、点击编辑不了、发送按钮不见了」，
日志只会报 send-disabled → send-unavailable。

修法：命中关键词的行**也**必须过「流式结束」这一关 —— 文字连续
dbwbAutoConfirmStreamSettleMs(1500ms) 没再变化，才认为这行吐完、UI 交还出来；
之后还要先等 dbwbIsComposerUsable()（框解禁），再写入 + Enter 提交。
（发送按钮的可用性只在「Enter 没受理」的兜底分支里判，不再是主路径）
重试上限 3 → 8（2.5s/轮，覆盖长回复 + UI 解禁的空窗）。

失败不再永久报废
----------------
原先一旦 composer 不可用，该行立刻被永久标记为已处理，再也不会回头。
现在失败（composer-busy / composer-write-failed / send-unavailable /
send-not-usable）会回退标记并延迟 2.5 秒重试，每行最多 8 次；成功点击后绝不重入。
⚠️ 配套约束：隐藏握手行必须在「已点下去」之后再做 —— 若提前 display:none，
   innerText 会读成空串，下一轮 isConfirmationReply 判 false，重试无从命中。
   （回归用例 case14 反向验证过：把这个顺序放回去，用例立刻 FAIL）

诊断：window.__doubaoWorkbenchAutoConfirm.phase
取值 sent / stream-settling / composer-busy / composer-write-failed /
send-unavailable / send-not-usable / generation-already-started

日志取证：脚本用 console.warn 打 [DBWB-AC] 前缀（crash-probe 只收 warning/error 落盘），
落点 %APPDATA%\seedance-desktop\logs\diagnose-YYYYMMDD.log。
含 install-begin / installed / install-skip / dom（DOM 形状侦察）/ pending / idle /
skip-bulk / settling / stream-settling / ignore / targets / retry-later /
composer-candidates / button-candidates / phase=*。
（composer-candidates / button-candidates 里每一项都带 tid=data-testid 与 dis=禁用态）

旧路径已拆除：dbwbResendOriginalPrompt（发完确认后自动重发原提示词）已删除，
避免与新监听各发一条；旧握手 state 现在只负责「发送后识别生成是否已开始」。
