/* cdp-verify-aicreate.mjs — 验证 AI创作 弹窗 prompt 注入(09-01 改动闭环) * 覆盖点:①#/rewrites 页 AI创作 按钮(noPersonaNote P0 bug 修复后不再报错) * ②账号详情视频列表 AI创作 按钮(人设+视频选题注入) * 方法:CDP 9223 → 复用现有 8900 标签页 → hook window.confirmRun 捕获 opts(不真实执行任务) * 运行:node cdp-verify-aicreate.mjs */ const CDP = 'http://127.0.0.1:9223'; async function getWs(targetId) { const tabs = await (await fetch(CDP + '/json')).json(); const t = tabs.find((x) => x.id === targetId); if (!t) throw new Error('target not found: ' + targetId); return t.webSocketDebuggerUrl; } function cdp(ws, method, params = {}) { return new Promise((resolve, reject) => { const id = ++cdp._id; const w = new WebSocket(ws); const timer = setTimeout(() => { try { w.close(); } catch (e) {} reject(new Error('timeout: ' + method)); }, 30000); w.onopen = () => w.send(JSON.stringify({ id, method, params })); w.onmessage = (ev) => { const m = JSON.parse(ev.data); if (m.id !== id) return; clearTimeout(timer); try { w.close(); } catch (e) {} m.error ? reject(new Error(method + ': ' + JSON.stringify(m.error))) : resolve(m.result); }; w.onerror = () => { clearTimeout(timer); reject(new Error('ws error: ' + method)); }; }); } cdp._id = 0; async function evalJs(ws, expr) { const r = await cdp(ws, 'Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error('eval err: ' + JSON.stringify(r.exceptionDetails).slice(0, 300)); return r.result && r.result.value; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function waitFor(ws, expr, timeout = 40000, label = '') { const t0 = Date.now(); while (Date.now() - t0 < timeout) { try { if (await evalJs(ws, expr)) return true; } catch (e) {} await sleep(800); } throw new Error('waitFor timeout: ' + label + ' -> ' + expr); } async function main() { const tabs = await (await fetch(CDP + '/json')).json(); const wb = tabs.find((t) => t.url.includes('localhost:8900') || t.url.includes('127.0.0.1:8900')); if (!wb) throw new Error('未找到 8900 工作台标签页,请先在内置浏览器打开工作台'); const ws = await getWs(wb.id); console.log('▶ 复用标签页:', wb.url); // ---------- ① #/rewrites 页 ---------- console.log('\n【① #/rewrites 页 AI创作 按钮】'); await evalJs(ws, `location.hash = '#/rewrites'`); await cdp(ws, 'Page.reload', { ignoreCache: true }); await waitFor(ws, `!!document.querySelector('[data-rewrite]')`, 40000, 'rewrites 列表渲染'); await sleep(500); // hook confirmRun 捕获 opts await evalJs(ws, `window.__cap = null; window.__origConfirmRun = window.confirmRun; window.confirmRun = (o) => { window.__cap = o; }; true`); // 找一条「有人设账号」的已解析视频按钮(俊希 3 / 旧梦留声机 1629 优先),并判断其解析内容是否含「# 选题」段 const parsedInfo = await evalJs(ws, `(async () => { const prefer = [3, 1629]; const btns = [...document.querySelectorAll('[data-rewrite]')]; let fallback = null; for (const b of btns) { const r = JSON.parse(b.dataset.rewrite); if (!r.parsed) continue; let hasTopic = false; try { const vd = await api('/api/dsh/video-detail?videoId=' + encodeURIComponent(r.id)); hasTopic = !!extractVideoTopic((vd.parse && vd.parse.content) || ''); } catch (e) {} const info = { idx: btns.indexOf(b), name: r.name, title: r.title, accId: r.accountId, parsed: r.parsed, hasTopic }; if (!fallback) fallback = info; if (prefer.includes(r.accountId)) return info; } return fallback; })()`); console.log('已解析视频:', JSON.stringify(parsedInfo)); if (!parsedInfo) throw new Error('无已解析视频按钮可测'); await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${parsedInfo.idx}].click(); true`); await waitFor(ws, `!!window.__cap`, 30000, 'confirmRun 捕获(已解析)'); const cap1 = await evalJs(ws, `JSON.stringify(window.__cap)`); const c1 = JSON.parse(cap1); console.log(' title:', c1.title, '| label:', c1.label); console.log(' target:', c1.target); const p1 = c1.prompt || ''; console.log(' prompt 含【账号设定(人设卡)】:', p1.includes('【账号设定(人设卡)】')); console.log(' prompt 含【视频选题】:', p1.includes('【视频选题】')); // 有选题段 → 直接用选题;无选题段(分镜头表等)→ 带解析内容 + 提炼指令(09-01 用户要求) if (parsedInfo.hasTopic) { console.log(' prompt 含 # 选题:', p1.includes('# 选题'), '(视频实际含选题段: true)'); } else { console.log(' prompt 含【视频解析内容】:', p1.includes('【视频解析内容(供提炼选题)】'), '(视频实际含选题段: false)'); console.log(' prompt 含提炼指令:', p1.includes('提炼出该视频的选题')); } console.log(' prompt 含 noPersonaNote 泄漏:', p1.includes('undefined')); const ok1 = c1.title === 'AI创作' && p1.includes('【账号设定(人设卡)】') && (parsedInfo.hasTopic ? (p1.includes('【视频选题】') && p1.includes('# 选题')) : (p1.includes('【视频解析内容(供提炼选题)】') && p1.includes('提炼出该视频的选题'))) && !p1.includes('undefined'); console.log(' ✅ 已解析分支断言:', ok1 ? 'PASS' : 'FAIL'); // 未解析视频按钮(parsed=false) await evalJs(ws, `window.__cap = null; true`); const unparsedInfo = await evalJs(ws, `(() => { const btns = [...document.querySelectorAll('[data-rewrite]')]; for (const b of btns) { const r = JSON.parse(b.dataset.rewrite); if (!r.parsed) return { idx: btns.indexOf(b), name: r.name, title: r.title, url: r.url }; } return null; })()`); console.log('未解析视频:', JSON.stringify(unparsedInfo)); if (unparsedInfo) { await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${unparsedInfo.idx}].click(); true`); await waitFor(ws, `!!window.__cap`, 30000, 'confirmRun 捕获(未解析)'); const p2 = JSON.parse(await evalJs(ws, `JSON.stringify(window.__cap)`)).prompt || ''; // 账号设定在前:账号相关信息(【账号设定(人设卡)】块,或无人设时的「该账号暂无账号设定」提示)必须出现在解析要求之前(09-01 用户要求统一顺序) const iP2 = p2.indexOf('【账号设定(人设卡)】'); const iNote = p2.indexOf('该账号暂无账号设定'); const iUn = p2.indexOf('该视频尚未解析'); const acctPos = iP2 >= 0 ? iP2 : iNote; // 有 persona 取人设块位置;无 persona 取缺失提示位置 const ok2 = p2.includes('该视频尚未解析,请先解析') && acctPos >= 0 && iUn > acctPos // 账号相关信息在解析要求之前 && !p2.includes('undefined'); console.log(' 未解析分支:人设块 idx=', iP2, '| 缺失提示 idx=', iNote, '| 解析要求 idx=', iUn); console.log(' 未解析分支(账号信息前置+先解析再创作+无泄漏):', ok2 ? 'PASS' : 'FAIL'); } else { console.log(' 无未解析视频可测(跳过)'); } await evalJs(ws, `window.confirmRun = window.__origConfirmRun; true`); // ---------- ② 账号详情视频列表 ---------- console.log('\n【② 账号详情视频列表 AI创作 按钮】'); const accId = parsedInfo.accId || 1629; await evalJs(ws, `location.hash = '#/account/${accId}'`); await cdp(ws, 'Page.reload', { ignoreCache: true }); await waitFor(ws, `!!document.querySelector('[data-rewrite]')`, 40000, '账号详情视频列表渲染'); await sleep(500); await evalJs(ws, `window.__cap = null; window.__origConfirmRun = window.confirmRun; window.confirmRun = (o) => { window.__cap = o; }; true`); // 账号详情已解析视频(详情页视频都属于当前账号,取第一条 parsed 即可;同时判定其解析内容是否含选题段) const dt = await evalJs(ws, `(async () => { const btns = [...document.querySelectorAll('[data-rewrite]')]; for (const b of btns) { const r = JSON.parse(b.dataset.rewrite); if (!r.parsed || !r.id) continue; let hasTopic = false; try { const vd = await api('/api/dsh/video-detail?videoId=' + encodeURIComponent(r.id)); hasTopic = !!extractVideoTopic((vd.parse && vd.parse.content) || ''); } catch (e) {} return { idx: btns.indexOf(b), name: r.name, title: r.title, id: r.id, parsed: r.parsed, hasTopic }; } return null; })()`); console.log('账号详情已解析视频:', JSON.stringify(dt)); if (dt) { await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${dt.idx}].click(); true`); await waitFor(ws, `!!window.__cap`, 40000, '账号详情 confirmRun 捕获'); const c3 = JSON.parse(await evalJs(ws, `JSON.stringify(window.__cap)`)); const p3 = c3.prompt || ''; console.log(' title:', c3.title, '| label:', c3.label); console.log(' prompt 含【账号设定(人设卡)】:', p3.includes('【账号设定(人设卡)】')); console.log(' prompt 含【视频选题】:', p3.includes('【视频选题】')); if (dt.hasTopic) { console.log(' prompt 含 # 选题:', p3.includes('# 选题'), '(视频实际含选题段: true)'); } else { console.log(' prompt 含【视频解析内容】:', p3.includes('【视频解析内容(供提炼选题)】'), '(视频实际含选题段: false)'); console.log(' prompt 含提炼指令:', p3.includes('提炼出该视频的选题')); } console.log(' prompt 含 undefined 泄漏:', p3.includes('undefined')); console.log(' prompt 长度:', p3.length); const ok3 = c3.title === 'AI创作' && p3.includes('【账号设定(人设卡)】') && p3.includes('【视频选题】') && (dt.hasTopic ? p3.includes('# 选题') : (p3.includes('【视频解析内容(供提炼选题)】') && p3.includes('提炼出该视频的选题'))) && !p3.includes('undefined'); console.log(' ✅ 账号详情分支断言:', ok3 ? 'PASS' : 'FAIL'); } else { console.log(' 账号详情无可测已解析视频(跳过)'); } await evalJs(ws, `window.confirmRun = window.__origConfirmRun; true`); console.log('\n==== 验证完成 ===='); } main().catch((e) => { console.error('❌', e.message); process.exit(1); });