Files
mcn-short-video/project/短视频脚本创作/V1.0/mcn-workshop/cdp-verify-aicreate.mjs
T
maogeigei f77735b206 工作台:目录改名 mcn-workshop + 任务进度可见 + 会话归属配置化
1. 目录 mcn-work-shop -> mcn-workshop(空间分组名取会话 cwd 的目录名,只改字符串无效,必须真改名)
2. 全仓替换 mcn-work-shop -> mcn-workshop:37 文件 121 处;历史日志按沿革句规矩保留当时目录名
3. 任务进度可见:/api/run/status 产出 已运行时长/工具调用/最近动作/停滞判定(读会话日志尾部),前端新增右下角常驻面板,四处任务入口接入
4. 会话归属目录配置化:新增 config.sessionCwd(留空=工作台自身目录),现指向 D://AI技能//mcn-workshop,使任务显示为命名分组而非「未分组任务」
5. 修前端轮询静默缺陷:连续 3 次查询失败即提示服务断开(原逻辑静默空转到 15 分钟超时,用户零感知)
2026-10-08 13:09:06 +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); });