feat(工作台): AI创作 分镜头表老视频先提炼选题再创作 + 修复 #/rewrites noPersonaNote 未定义 P0
- buildTopicBlock(): 有#选题段直接用;无(分镜头表等)带解析内容+提炼指令;空只带标题 - 账号详情视频列表 + #/rewrites 两入口同步改造,desc 注明分镜头表先提炼 - 修复 renderRewrites 分支引用未定义 noPersonaNote 导致弹窗打不开(ReferenceError) - 验证脚本 cdp-verify-aicreate.mjs(hook confirmRun 捕获 prompt,按 hasTopic 动态断言)
This commit is contained in:
1 parent
787ececedb
commit
9589680dc3
5 files changed
+394
-35
No files matched your search
@@ -0,0 +1,183 @@
|
||||
/* 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 || '';
|
||||
const ok2 = p2.includes('请先解析这个抖音视频') && !p2.includes('undefined');
|
||||
console.log(' 未解析分支(先解析再创作+无 undefined 泄漏):', 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); });
|
||||
Reference in new issue
Block a user