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 分钟超时,用户零感知)
136 lines
6.4 KiB
JavaScript
136 lines
6.4 KiB
JavaScript
/* cdp-verify-batch.mjs — 验证三列表(账号列表/视频列表/AI写脚本列表)批量操作 UI(09-01)
|
||
* 覆盖点:①左下角「批量操作」图标(check-square)存在
|
||
* ②点击后每行出现勾选框 + 删除(trash)/取消(x) 图标 + 全选 + 计数
|
||
* ③勾选计数正确、全选正确、取消恢复常态
|
||
* 方法:CDP 9223 → 复用 8900 标签页 → 模拟点击(不真实调用删除 API)
|
||
* 运行:node cdp-verify-batch.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 waitFor(ws, expr, timeout = 40000, label = '') {
|
||
const t0 = Date.now();
|
||
while (Date.now() - t0 < timeout) {
|
||
try { if (await evalJs(ws, expr)) return true; } catch (e) {}
|
||
await sleep(800);
|
||
}
|
||
throw new Error('waitFor timeout: ' + label + ' -> ' + expr);
|
||
}
|
||
|
||
let pass = 0, fail = 0;
|
||
function ok(cond, label) {
|
||
if (cond) { pass++; console.log(' ✅ ' + label); }
|
||
else { fail++; console.log(' ❌ FAIL: ' + label); }
|
||
}
|
||
|
||
// 对单个列表页执行批量操作 UI 全链路验证
|
||
async function verifyList(ws, hash, label, rowSelector) {
|
||
console.log(`\n【${label}】 ${hash}`);
|
||
await evalJs(ws, `location.hash = '${hash}'`);
|
||
await cdp(ws, 'Page.reload', { ignoreCache: true });
|
||
await waitFor(ws, `!!document.querySelector('${rowSelector}')`, 40000, label + ' 列表渲染');
|
||
await sleep(400);
|
||
|
||
// ① 常态:左下角批量操作图标按钮存在(含 svg 图标)
|
||
const init = await evalJs(ws, `(() => {
|
||
const bar = document.querySelector('.list-batchbar');
|
||
const btn = bar && bar.querySelector('[data-batch-open]');
|
||
return { hasBar: !!bar, hasBtn: !!btn, hasIcon: !!(btn && btn.querySelector('.ic')), btnText: btn ? btn.textContent.trim() : '' };
|
||
})()`);
|
||
ok(init.hasBar && init.hasBtn, `左下角批量操作条存在`);
|
||
ok(init.hasIcon, `批量操作按钮为图标(非文字,文本="${init.btnText}")`);
|
||
ok(init.btnText.trim() === '', `按钮无文字残留(纯图标)`);
|
||
|
||
// ② 点击批量操作图标 → 进入批量模式
|
||
await evalJs(ws, `document.querySelector('[data-batch-open]').click()`);
|
||
await waitFor(ws, `!!document.querySelector('.batch-cb-all')`, 20000, label + ' 批量模式进入');
|
||
await sleep(300);
|
||
const mode = await evalJs(ws, `(() => {
|
||
const rows = document.querySelectorAll('tbody tr .batch-cb').length;
|
||
const delBtn = document.querySelector('[data-batch-del]');
|
||
const cancelBtn = document.querySelector('[data-batch-cancel]');
|
||
return {
|
||
rowCbs: rows,
|
||
hasDel: !!delBtn, delIcon: !!(delBtn && delBtn.querySelector('.ic')),
|
||
hasCancel: !!cancelBtn, cancelIcon: !!(cancelBtn && cancelBtn.querySelector('.ic')),
|
||
hasAll: !!document.querySelector('.batch-cb-all'),
|
||
delText: delBtn ? delBtn.textContent.trim() : '',
|
||
};
|
||
})()`);
|
||
ok(mode.rowCbs > 0, `每行出现勾选框(${mode.rowCbs} 行)`);
|
||
ok(mode.hasDel && mode.delIcon, `删除按钮为图标(trash)`);
|
||
ok(mode.hasCancel && mode.cancelIcon, `取消按钮为图标(x)`);
|
||
ok(mode.hasAll, `表头全选框存在`);
|
||
ok(mode.delText === '', `删除按钮无文字残留(纯图标)`);
|
||
|
||
// ③ 勾选 2 行 → 计数更新
|
||
await evalJs(ws, `(() => { const cbs = document.querySelectorAll('.batch-cb'); cbs[0].click(); cbs[1].click(); return true; })()`);
|
||
await sleep(200);
|
||
const cnt = await evalJs(ws, `document.querySelector('.batch-count-n').textContent`);
|
||
ok(cnt === '2', `勾选 2 行计数显示 2(实际 ${cnt})`);
|
||
|
||
// ④ 全选 → 计数=行数
|
||
await evalJs(ws, `document.querySelector('.batch-cb-all').click()`);
|
||
await sleep(200);
|
||
const cntAll = await evalJs(ws, `document.querySelector('.batch-count-n').textContent`);
|
||
const totalRows = await evalJs(ws, `document.querySelectorAll('.batch-cb').length`);
|
||
ok(cntAll === String(totalRows), `全选计数=行数 ${totalRows}(实际 ${cntAll})`);
|
||
|
||
// ⑤ 取消 → 恢复常态(无勾选框)
|
||
await evalJs(ws, `document.querySelector('[data-batch-cancel]').click()`);
|
||
await sleep(400);
|
||
const back = await evalJs(ws, `(() => {
|
||
const bar = document.querySelector('.list-batchbar');
|
||
return { hasCbs: document.querySelectorAll('.batch-cb').length > 0, hasOpen: !!(bar && bar.querySelector('[data-batch-open]')) };
|
||
})()`);
|
||
ok(!back.hasCbs && back.hasOpen, `取消后恢复常态(勾选框消失,批量操作图标回归)`);
|
||
}
|
||
|
||
async function main() {
|
||
const tabs = await (await fetch(CDP + '/json')).json();
|
||
const wb = tabs.find((t) => t.url.includes('localhost:8900') || t.url.includes('127.0.0.1:8900'));
|
||
if (!wb) throw new Error('未找到 8900 工作台标签页,请先在内置浏览器打开工作台');
|
||
const ws = await getWs(wb.id);
|
||
console.log('▶ 复用标签页:', wb.url);
|
||
|
||
await verifyList(ws, '#/accounts', '账号列表', 'tbody tr a.link');
|
||
await verifyList(ws, '#/account/3', '账号详情-视频列表(俊希)', 'tbody tr .td-id a');
|
||
await verifyList(ws, '#/rewrites', 'AI写脚本列表', 'tbody tr [data-rewrite]');
|
||
|
||
console.log(`\n==== ${fail === 0 ? '全部 PASS' : '存在 FAIL ' + fail + ' 项'}(PASS ${pass} / FAIL ${fail})====`);
|
||
process.exit(fail === 0 ? 0 : 1);
|
||
}
|
||
|
||
main().catch((e) => { console.error('脚本异常:', e.message); process.exit(1); });
|