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 分钟超时,用户零感知)
141 lines
11 KiB
JavaScript
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);
|