Files
mcn-short-video/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs
T
maogeigei 79b8a3d036 feat(工作台): AI创作输入输出梳理+顺序统一(账号设定前置)+类型列签约改内部
- data-pages.js: 未解析两分支账号设定前置; 账号详情 data-rewrite 补 url; 类型列 badge+筛选下拉 签约→内部(后端 type=signed 不变)
- docs/AI会话任务输入输出对照.md: 8类功能入口输入输出+触发链路+prompt结构模板
- cdp-verify-aicreate.mjs: 断言升级(hasTopic动态分支+顺序断言)
- cdp-verify-typebadge.mjs: 新增类型列改名验证(排除bio数据文本)
2026-09-01 12:20:20 +08:00

192 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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); });