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 分钟超时,用户零感知)
88 lines
5.6 KiB
JavaScript
88 lines
5.6 KiB
JavaScript
// R27 冒烟 v3:AI写脚本页布局(标题行干净/tab+筛选下行)+ 状态列浅绿 + 账号列表「更新」按钮弹窗
|
|
// 注意:hash 不能带 ?query(会被路由当作账号名 scope 过滤)→ 切页用 location.hash
|
|
const list = await fetch('http://localhost:9225/json').then((r) => r.json());
|
|
const page = 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, 200) }; return r.result.value; };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
await send('Page.enable'); await send('Runtime.enable');
|
|
const waitFor = async (expr, ms = 12000) => { 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 goto = async (hash) => { await ev(`location.hash = ${JSON.stringify(hash)}`); await sleep(600); };
|
|
|
|
/* ============ 0) 先加载应用 ============ */
|
|
await send('Page.navigate', { url: 'http://localhost:8900/#/' });
|
|
if (!(await waitFor(`!!document.querySelector('#view')`))) fail('app boot');
|
|
|
|
/* ============ 1) AI写脚本页布局 + 状态列 ============ */
|
|
await goto('#/rewrites');
|
|
if (!(await waitFor(`!!document.querySelector('#view .rw-tabs')`))) fail('rw-tabs render');
|
|
if (!(await waitFor(`!!document.querySelector('#view table tbody tr')`))) fail('table rows: ' + JSON.stringify(await ev(`document.querySelector('#view .table-wrap') ? document.querySelector('#view .table-wrap').textContent.slice(0,80) : 'no wrap'`)));
|
|
const rw = await ev(`(() => {
|
|
const tb = document.querySelector('#view .page-toolbar');
|
|
const tabs = document.querySelector('#view .rw-tabs');
|
|
const tr = tabs.querySelector('.toolbar-right');
|
|
return {
|
|
toolbarChildren: [...tb.children].map((c) => (c.className && String(c.className)) || c.tagName),
|
|
toolbarHasFilter: !!tb.querySelector('.toolbar-right'),
|
|
toolbarHasTabs: !!tb.querySelector('.rw-tabs'),
|
|
tabsIsNextSibling: tb.nextElementSibling === tabs,
|
|
tabsCount: tabs.querySelectorAll('button[data-tab]').length,
|
|
tabsRowRight: !!tr,
|
|
rightInputs: tr ? [...tr.querySelectorAll('input,select')].map((x) => x.id) : [],
|
|
parsedCells: [...document.querySelectorAll('#view tbody td')].filter((td) => td.textContent.trim() === '已解析').slice(0, 3).map((td) => {
|
|
const sp = td.querySelector('span.st-parsed');
|
|
return { hasSpan: !!sp, color: sp ? getComputedStyle(sp).color : null };
|
|
}),
|
|
hasEmojiParsed: document.querySelector('#view tbody').textContent.includes('✅已解析'),
|
|
};
|
|
})()`);
|
|
|
|
/* ============ 2) 账号列表「更新」按钮 + 弹窗 ============ */
|
|
await goto('#/accounts');
|
|
// 全新测试 profile 无 localStorage:应用可能自动弹出「设置产出根目录」弹窗 → 先清掉杂项弹窗,避免误匹配
|
|
await ev(`document.querySelectorAll('.modal-mask').forEach((m) => m.remove())`);
|
|
if (!(await waitFor(`!!document.querySelector('#accUpdate')`))) fail('accUpdate render');
|
|
const acc = await ev(`(() => {
|
|
const b = document.querySelector('#accUpdate');
|
|
const r = { txt: b.textContent.trim(), hasIcon: !!b.querySelector('svg'), title: b.title };
|
|
b.click();
|
|
return r;
|
|
})()`);
|
|
await sleep(300);
|
|
if (!(await waitFor(`[...document.querySelectorAll('.modal-mask .modal')].some((m) => m.textContent.includes('更新关注账号'))`))) fail('update modal open');
|
|
const md = await ev(`(() => {
|
|
const m = [...document.querySelectorAll('.modal-mask .modal')].find((x) => x.textContent.includes('更新关注账号'));
|
|
const r = {
|
|
title: m.querySelector('h3').textContent.trim(),
|
|
target: m.querySelector('.confirm-target') ? m.querySelector('.confirm-target').textContent.trim() : null,
|
|
desc: m.querySelector('p.dim').textContent.trim(),
|
|
hasCancel: !!m.querySelector('[data-cancel]'),
|
|
hasOk: !!m.querySelector('[data-ok]'),
|
|
maskCount: document.querySelectorAll('.modal-mask').length,
|
|
};
|
|
m.querySelector('[data-cancel]').click();
|
|
return r;
|
|
})()`);
|
|
await sleep(400);
|
|
const closed = await ev(`![...document.querySelectorAll('.modal-mask')].some((m) => m.textContent.includes('更新关注账号'))`);
|
|
|
|
console.log(JSON.stringify({ rw, acc, md, closed }, null, 1));
|
|
const ok = rw && !rw.__err
|
|
&& rw.toolbarHasFilter === false && rw.toolbarHasTabs === false
|
|
&& rw.tabsIsNextSibling === true && rw.tabsCount === 3
|
|
&& rw.tabsRowRight === true && rw.rightInputs.length >= 2
|
|
&& rw.hasEmojiParsed === false
|
|
&& rw.parsedCells.length > 0 && rw.parsedCells.every((c) => c.hasSpan && c.color === 'rgb(82, 183, 136)')
|
|
&& acc && !acc.__err && acc.txt === '更新' && acc.hasIcon
|
|
&& md && !md.__err && md.title.includes('更新关注账号') && md.target.includes('做夢') && md.target.includes('胡德禄') && md.target.includes('旧梦')
|
|
&& md.hasCancel && md.hasOk && closed === true;
|
|
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
|
|
ws.close();
|
|
process.exit(ok ? 0 : 1);
|