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

131 lines
6.1 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-footer.mjs — 验证账号列表渲染修复(09-01)
* 背景:上一轮误加裸赋值 accReset(严格模式 ReferenceError)+ list-footer 未包账号列表
* 验证:①账号列表页渲染成功(无 console 异常)②批量操作条与分页同一行(.list-footer 内)
* ③三列表均含 .list-footer ④批量模式切换正常
* 运行:node cdp-verify-footer.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 main() {
// 找 8900 标签页
const tabs = await (await fetch(CDP + '/json')).json();
const tab = tabs.find((x) => x.url.includes('localhost:8900')) || tabs.find((x) => x.type === 'page');
if (!tab) throw new Error('no 8900 tab found');
const ws = await getWs(tab.id);
await cdp(ws, 'Page.enable');
await cdp(ws, 'Runtime.enable');
await cdp(ws, 'Log.enable');
const errors = [];
const onErr = (ev) => { const m = JSON.parse(ev.data); if (m.method === 'Runtime.exceptionThrown') errors.push(JSON.stringify(m.params.exceptionDetails).slice(0, 200)); };
const ws2 = new WebSocket(ws);
await new Promise((r) => { ws2.onopen = r; });
ws2.onmessage = onErr;
let pass = 0, fail = 0;
const check = (name, cond) => { cond ? pass++ : fail++; console.log((cond ? 'PASS' : 'FAIL') + ' | ' + name); };
// 打开账号列表
await cdp(ws, 'Page.navigate', { url: 'http://localhost:8900/?v=verify#/accounts' });
await sleep(2500);
const acc = await evalJs(ws, `(() => {
const root = document.querySelector('#view, main, .view, #app');
const page = root ? root.innerHTML : document.body.innerHTML;
return {
hasError: /页面渲染失败|not defined|ReferenceError/.test(page),
hasTable: !!document.querySelector('.table'),
hasRows: document.querySelectorAll('.table tbody tr').length,
footer: !!document.querySelector('.list-footer'),
footerBoth: !!document.querySelector('.list-footer .list-batchbar') && !!document.querySelector('.list-footer .pager'),
batchBtn: !!document.querySelector('[data-batch-open]'),
title: document.querySelector('.page-title')?.textContent || ''
};
})()`);
check('账号列表渲染无错误文案', !acc.hasError);
check('账号列表表格存在', acc.hasTable);
check('列表有数据行', acc.hasRows > 0);
check('.list-footer 存在(批量+分页同行容器)', acc.footer);
check('.list-footer 内含批量条和分页器', acc.footerBoth);
check('批量操作图标按钮存在', acc.batchBtn);
console.log(' 页面标题:', acc.title, '| 行数:', acc.hasRows);
// 点批量操作图标 → 验证批量模式 + 同行不破坏
await evalJs(ws, `document.querySelector('[data-batch-open]').click()`);
await sleep(600);
const bm = await evalJs(ws, `(() => ({
cbs: document.querySelectorAll('.batch-cb').length,
delBtn: !!document.querySelector('[data-batch-del]'),
cancelBtn: !!document.querySelector('[data-batch-cancel]'),
footerStill: !!document.querySelector('.list-footer .list-batchbar') && !!document.querySelector('.list-footer .pager'),
rowHasCb: !!document.querySelector('.table tbody .cb-cell input.batch-cb')
}))()`);
check('批量模式每行有勾选框', bm.cbs > 0 && bm.rowHasCb);
check('删除(trash)图标按钮出现', bm.delBtn);
check('取消(x)图标按钮出现', bm.cancelBtn);
check('批量模式下批量条+分页仍同行', bm.footerStill);
// 取消恢复
await evalJs(ws, `document.querySelector('[data-batch-cancel]').click()`);
await sleep(500);
const back = await evalJs(ws, `({ cbs: document.querySelectorAll('.batch-cb').length, footer: !!document.querySelector('.list-footer') })`);
check('取消后恢复常态(无勾选框)', back.cbs === 0);
check('取消后 footer 仍在', back.footer);
// 顺手验证另外两个列表的 footer
for (const [name, hash, sel] of [
['账号详情视频列表', '#/account/3', '.list-footer'],
['AI写脚本列表', '#/rewrites', '.list-footer']
]) {
await cdp(ws, 'Page.navigate', { url: 'http://localhost:8900/?v=verify' + hash });
await sleep(2000);
const r = await evalJs(ws, `(() => {
const f = document.querySelector('.list-footer');
return { f: !!f, both: f ? !!f.querySelector('.list-batchbar') && !!f.querySelector('.pager') : false, err: /页面渲染失败|not defined/.test(document.body.innerHTML) };
})()`);
check(name + ' .list-footer 存在', r.f);
check(name + ' 批量条+分页同行', r.both);
check(name + ' 无渲染错误', !r.err);
}
console.log('--- console 异常:', errors.length ? errors.join('\n') : '无');
if (errors.length) fail++;
check('无 console 异常', errors.length === 0);
console.log(`\n结果: ${pass} PASS / ${fail} FAIL`);
ws2.close();
process.exit(fail ? 1 : 0);
}
main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });