2026-09-01 12:03:47 +08:00
/* 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 || '' ;
2026-09-01 12:20:20 +08:00
// 账号设定在前:账号相关信息(【账号设定(人设卡)】块,或无人设时的「该账号暂无账号设定」提示)必须出现在解析要求之前(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' );
2026-09-01 12:03:47 +08:00
} 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 ); });