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

82 lines
6.6 KiB
JavaScript

// 09-04 R64 验证:内容定位首板块与头部大标题框间距加大(y 起点 22→34)
// 断言:① 头部框(y30 h170)底 → 第一张整宽米色卡顶 gap = 64(旧值 52)② 「内容定位」标题存在 ③ 原 R59 断言保持(白底/gap≥20/不越界/含内容结构)
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: 860, height: 1000, 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 fs = await import('node:fs');
const AUDIT = `(() => {
const svg = document.querySelector('.vk-view svg');
if (!svg) return { err: 'no svg in .vk-view' };
const vbH = svg.viewBox.baseVal.height;
const rects = [...svg.querySelectorAll('rect')].map((r) => ({ x: Math.round(r.x.baseVal.value * 10) / 10, y: Math.round(r.y.baseVal.value * 10) / 10, w: Math.round(r.width.baseVal.value), h: Math.round(r.height.baseVal.value), fill: r.getAttribute('fill') }));
const texts = [...svg.querySelectorAll('text')].map((t) => ({ x: +t.getAttribute('x'), y: +t.getAttribute('y'), size: +t.getAttribute('font-size'), wt: t.getAttribute('font-weight'), txt: (t.textContent || '').slice(0, 42) }));
const head = rects.find((r) => r.w === 520 && r.h === 170); /* 头部大标题框 */
const headBottom = head ? head.y + head.h : null;
const cards = rects.filter((r) => (r.fill || '').startsWith('#F') && r.w > 550 && r.h > 40); /* 整宽内容卡(w>550 排除 520 头部标题框) */
const firstCard = cards[0];
const titleYs = texts.filter((t) => t.txt.startsWith('▍')).map((t) => ({ t: t.txt.slice(1), y: t.y }));
const firstTitle = titleYs[0]; /* 应为「内容定位」 */
const audits = cards.map((r) => {
const inT = texts.filter((t) => t.y > r.y + 2 && t.y < r.y + r.h && t.x >= r.x - 1 && t.x <= r.x + r.w + 1).sort((a, b) => a.y - b.y);
const first = inT[0];
return { cardTop: r.y, h: r.h, firstTxt: first ? first.txt : '', gap: first ? Math.round((first.y - r.y) * 10) / 10 : null };
});
const structCard = cards[1];
const cardByTitle = {};
cards.forEach((r) => { const t = titleYs.find((x) => x.y < r.y && r.y - x.y < 60); if (t) cardByTitle[t.t] = r; });
const secCard = cardByTitle['内容结构'];
const secBoard = secCard ? texts.filter((t) => t.y > secCard.y && t.y < secCard.y + secCard.h && (t.wt || '') === '700' && t.x > secCard.x && t.x < secCard.x + 120).map((t) => t.txt) : [];
const bgStops = [...svg.querySelectorAll('#bgGrad stop')].map((s) => s.getAttribute('stop-color'));
const maxTextY = Math.max(...texts.map((t) => t.y));
return { vbH, headBottom, firstCardTop: firstCard ? firstCard.y : null, headGap: (headBottom != null && firstCard) ? Math.round((firstCard.y - headBottom) * 10) / 10 : null, firstTitle: firstTitle ? { t: firstTitle.t, y: firstTitle.y, dt: Math.round((firstTitle.y - headBottom) * 10) / 10 } : null, titles: titleYs.map((x) => x.t), audits, secBoard, bgStops, over: maxTextY > vbH - 4 };
})()`;
const VIDS = [93, 190];
const out = {};
for (const id of VIDS) {
await send('Page.navigate', { url: `http://127.0.0.1:8900/?t=${Date.now()}#/video/${id}` });
if (!(await waitFor(`location.hash === '#/video/${id}' && !!document.querySelector('#view .video-grid') && [...document.scripts].some((s) => (s.src || '').includes('data-pages.js?v=20260904m'))`, 15000))) fail(`v${id} route`);
await sleep(400);
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
const hasCard = await waitFor(`!!document.querySelector('#view .vk-view svg')`, 12000);
if (!hasCard) { out['v' + id] = { __err: 'no keycard svg' }; continue; }
await sleep(500);
const audit = await ev(AUDIT);
out['v' + id] = audit;
await ev(`document.querySelector('.vk-view svg').scrollIntoView({ block: 'start' })`); await sleep(250);
const rect = await ev(`(() => { const r = document.querySelector('.vk-view svg').getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; })()`);
if (rect && !rect.__err && rect.w > 100) {
const shot = await send('Page.captureScreenshot', { format: 'png', clip: { x: Math.max(0, rect.x - 4), y: Math.max(0, rect.y - 4), width: Math.min(rect.w + 8, 1200), height: Math.min(rect.h + 8, 4000), scale: 1 } });
fs.writeFileSync(`tmp/r64_gap_v${id}.png`, Buffer.from(shot.data, 'base64'));
out['v' + id].shot = `tmp/r64_gap_v${id}.png`;
}
}
console.log(JSON.stringify(out, null, 1));
const v93 = out.v93, v190 = out.v190;
const ok = v93 && !v93.__err && v190 && !v190.__err
&& v93.headGap === 64 && v190.headGap === 64
&& v93.firstTitle && v93.firstTitle.t === '内容定位' && v190.firstTitle && v190.firstTitle.t === '内容定位'
&& v93.bgStops && v93.bgStops.every((c) => c === '#FFFFFF') && v190.bgStops && v190.bgStops.every((c) => c === '#FFFFFF')
&& v93.titles.includes('内容结构') && v190.titles.includes('内容结构')
&& v93.secBoard && v93.secBoard.some((b) => b.includes('开场钩子')) && v190.secBoard && v190.secBoard.some((b) => b.includes('开场钩子'))
&& v93.audits.every((a) => a.gap >= 20) && v190.audits.every((a) => a.gap >= 20)
&& !v93.over && !v190.over;
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
ws.close();
process.exit(ok ? 0 : 1);