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