Files
mcn-short-video/project/短视频脚本创作/V1.0/mcn-workshop/cdp-verify-r69.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

141 lines
11 KiB
JavaScript

// R69 验证:三类「刷新」任务的 prompt 均注入「强制重新获取并覆盖」语义
// ①视频详情 刷新 → prompt 含 VIDEO_REFRESH_OVERRIDE 覆盖刷新段(禁判断/禁复用旧结果/防截断覆盖)
// ②榜单 冷却窗口内「仍要更新」(force) → prompt 追加 覆盖刷新段(忽略幂等跳过)
// ③榜单 常规「确认更新」(非 force) → prompt 不含 force 覆盖段(幂等铁律保留)
// 实现:页面内拦截 fetch——捕获 POST /api/run 与 /api/dsh/ranking-update 的 body,伪造响应使流程走完但不真建任务
const CDP = 9223;
const list = await fetch(`http://localhost:${CDP}/json`).then((r) => r.json());
const page = list.find((t) => t.type === 'page' && (t.url || '').includes('8900')) || list.find((t) => t.type === 'page');
if (!page) { console.log('ERR: no page'); process.exit(1); }
const ws = new WebSocket(page.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
await new Promise((r) => (ws.onopen = r));
const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable');
await send('Network.setCacheDisabled', { cacheDisabled: true });
await send('Emulation.setDeviceMetricsOverride', { width: 900, height: 1100, deviceScaleFactor: 1, mobile: false });
const waitFor = async (expr, ms = 15000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; };
const fail = (m) => { console.log('FAIL@' + m); process.exit(1); };
const V = '20260904t';
const loaded = `[...document.scripts].some((s) => (s.src || '').includes('data-pages.js?v=${V}')) && typeof VIDEO_REFRESH_OVERRIDE === 'string'`;
// ---- 注入 fetch 拦截:捕获 run/ranking-update 请求体,伪造响应(不真建任务)----
const INJECT = `(() => {
window.__r69 = { runs: [], rankPost: [], videoPrompt: null, rankGetMock: 1 };
const realFetch = window.fetch.bind(window);
const json = (obj, ok = true) => ({ ok, json: async () => obj });
window.fetch = async (url, opts) => {
const u = String(url); const m = (opts && opts.method) || 'GET';
if (u.includes('/api/run/status')) return json({ ok: true, id: 'probe', state: 'notfound' });
if (u.endsWith('/api/run') && m.toUpperCase() === 'POST') {
let body = ''; try { body = JSON.parse((opts.body) || '{}'); } catch (e) {}
if (body && body.prompt) window.__r69.videoPrompt = body.prompt;
if (body && body.name && String(body.name).startsWith('更新账号·')) window.__r69.accPrompt = body.prompt;
window.__r69.runs.push({ name: body.name || '', skills: body.skills || [], connectors: body.connectorIds || null, prompt: (body.prompt || '').slice(0, 2000) });
return json({ ok: true, id: 'probe-run' });
}
if (u.endsWith('/api/dsh/ranking-update') && m.toUpperCase() === 'GET') {
const recent = window.__r69.rankGetMock === 1;
return json({ ok: true, exists: recent, files: [], lastUpdateAt: recent ? Date.now() - 60000 : null, lastUpdateDate: recent ? '2026-09-04' : '', lastUpdateAgeMin: recent ? 1 : null, cooldownMin: 30, task: null });
}
if (u.endsWith('/api/dsh/ranking-update') && m.toUpperCase() === 'POST') {
let body = {}; try { body = JSON.parse((opts.body) || '{}'); } catch (e) {}
window.__r69.rankPost.push({ force: !!body.force, prompt: (body.prompt || '').slice(0, 2000) });
return json({ ok: true, id: 'probe-rank' });
}
return realFetch(url, opts);
};
return 'injected';
})()`;
// ---- ① 视频详情 刷新 prompt ----
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}#/video/93` });
if (!(await waitFor(`location.hash === '#/video/93' && !!document.querySelector('#view .video-grid') && ${loaded}`, 15000))) fail('video93 route+load');
await sleep(300);
await ev(`localStorage.removeItem('wk_video_task_93')`); // 清残留执行中记录,确保刷新按钮可点
await ev(INJECT);
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
if (!(await waitFor(`!!document.querySelector('#view [data-parse="refresh"]')`, 10000))) fail('v93 refresh btn');
await ev(`document.querySelector('#view [data-parse="refresh"]').click()`);
if (!(await waitFor(`!!document.querySelector('.modal-mask [data-ok]')`, 8000))) fail('refresh modal');
await ev(`document.querySelector('.modal-mask [data-ok]').click()`);
if (!(await waitFor(`!!window.__r69.videoPrompt`, 8000))) fail('video prompt captured');
const vp = await ev(`window.__r69.videoPrompt`);
const vOk = typeof vp === 'string' && vp.includes('【覆盖刷新模式(用户点击「刷新」触发,非首次解析)】')
&& vp.includes('强制重新获取数据并覆盖') && vp.includes('禁止判断「是否需更新」')
&& vp.includes('严禁用不完整/截断结果覆盖旧的完整版本') && vp.includes('POST /api/import/account')
&& vp.includes('mcn-dou-analysis');
console.log('[1] video refresh prompt = SKILL_HINT + override:', vOk ? 'PASS' : 'FAIL');
if (!vOk) { console.log(String(vp).slice(0, 900)); process.exit(1); }
// 同时验证 connectors 挂载未回退
const vc = await ev(`window.__r69.runs[0] ? JSON.stringify(window.__r69.runs[0].connectors) : 'none'`);
console.log('[1b] video refresh connectors:', vc === '["custom-mcp:myai-mcp-production"]' ? 'PASS' : 'FAIL ' + vc);
// ---- ② 榜单更新 prompt:force(冷却窗口内「仍要更新」) ----
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}#/ranking` });
if (!(await waitFor(`location.hash.startsWith('#/ranking') && ${loaded}`, 15000))) fail('ranking route');
await sleep(400);
await ev(INJECT); // 重置捕获(rankGetMock 默认 1 = 近期已更新 → 冷却 → force 分支)
await ev(`(() => { const b = document.querySelector('#rankUpdate'); if (!b) return 'no-btn'; b.click(); return 'ok'; })()`);
if (!(await waitFor(`!!document.querySelector('.modal-mask [data-ok]')`, 8000))) fail('rank modal');
const okTxt1 = await ev(`(document.querySelector('.modal-mask [data-ok]') || {}).textContent`);
if (!String(okTxt1).includes('仍要更新')) { console.log('FAIL: expected force btn (仍要更新), got', JSON.stringify(okTxt1)); process.exit(1); }
console.log('[2] cooldown → 仍要更新 (force):', 'PASS');
await ev(`document.querySelector('.modal-mask [data-ok]').click()`);
if (!(await waitFor(`window.__r69.rankPost.length >= 1`, 8000))) fail('rank POST captured');
await sleep(200);
const rp1 = await ev(`window.__r69.rankPost[0]`);
const fOk = rp1 && rp1.force === true && typeof rp1.prompt === 'string'
&& rp1.prompt.includes('【覆盖刷新模式(09-04 R69:用户已确认强制刷新)】')
&& rp1.prompt.includes('重新调用 API 抓取最新数据并覆盖写入同名文件');
console.log('[3] force refresh prompt override:', fOk ? 'PASS' : 'FAIL');
if (!fOk) { console.log(JSON.stringify(rp1)); process.exit(1); }
// ---- ③ 榜单更新 prompt:非 force(无近期落盘 → 「确认更新」)----
await ev(`window.__r69.rankGetMock = 0; window.__r69.rankPost = [];`); // 下次 GET=无近期落盘 → 非冷却 → 普通更新
// ② 完成触发 router() 整页重建:重试点击直到弹窗出现(新 #rankUpdate 渲染绑定完成)
const t0 = Date.now();
while (Date.now() - t0 < 12000) {
const r = await ev(`(() => { const b = document.querySelector('#rankUpdate'); if (!b) return 'no-btn'; b.click(); return 'ok'; })()`);
if (await ev(`!!document.querySelector('.modal-mask [data-ok]')`)) break;
await sleep(400);
}
if (!(await ev(`!!document.querySelector('.modal-mask [data-ok]')`))) fail('rank modal2');
const okTxt2 = await ev(`(document.querySelector('.modal-mask [data-ok]') || {}).textContent`);
if (!String(okTxt2).includes('确认更新')) { console.log('FAIL: expected 确认更新, got', JSON.stringify(okTxt2)); process.exit(1); }
await ev(`document.querySelector('.modal-mask [data-ok]').click()`);
if (!(await waitFor(`window.__r69.rankPost.length >= 1`, 8000))) fail('rank normal POST captured');
await sleep(200);
const rp2 = await ev(`window.__r69.rankPost[0]`);
const nOk = rp2 && rp2.force === false && typeof rp2.prompt === 'string'
&& !rp2.prompt.includes('用户已确认强制刷新') && rp2.prompt.includes('查询幂等铁律');
console.log('[4] normal refresh keeps idempotency (no force override):', nOk ? 'PASS' : 'FAIL');
if (!nOk) { console.log(JSON.stringify(rp2)); process.exit(1); }
// ---- ④ 账号列表「更新关注账号」 prompt:含 覆盖刷新模式 ----
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}#/accounts` });
if (!(await waitFor(`location.hash.startsWith('#/accounts') && !!document.querySelector('#view #accUpdate') && ${loaded}`, 15000))) fail('accounts route');
await sleep(400);
await ev(INJECT); // 重置捕获(accPrompt 按 name 前缀 更新账号· 捕获)
await ev(`document.querySelector('#view #accUpdate').click()`);
if (!(await waitFor(`!!document.querySelector('.modal-mask h3') && (document.querySelector('.modal-mask h3').textContent || '').includes('更新关注账号')`, 8000))) fail('acc modal');
await ev(`document.querySelector('.modal-mask [data-ok]').click()`);
if (!(await waitFor(`!!window.__r69.accPrompt`, 10000))) fail('acc prompt captured');
const ap = await ev(`window.__r69.accPrompt`);
const aOk = typeof ap === 'string' && ap.includes('【覆盖刷新模式(09-04 R69)】')
&& ap.includes('强制重新抓取并覆盖') && ap.includes('不要因库中已有该账号记录')
&& ap.includes('覆盖更新该账号登记数据与视频列表记录');
console.log('[5] account refresh prompt override:', aOk ? 'PASS' : 'FAIL');
if (!aOk) { console.log(String(ap).slice(0, 900)); process.exit(1); }
console.log('\n=== PASS ===');
// 清理:整页重载丢弃 fetch 拦截(页面若留覆盖会 mock 掉真实任务提交),回到首页
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}` });
await waitFor(`location.hash === '' || location.hash === '#/'`, 8000);
ws.close();
process.exit(0);