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 分钟超时,用户零感知)
192 lines
10 KiB
JavaScript
192 lines
10 KiB
JavaScript
/* 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); });
|