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 分钟超时,用户零感知)
87 lines
5.9 KiB
JavaScript
87 lines
5.9 KiB
JavaScript
// R28 冒烟:视频详情页——去「视频分析」tab;未解析=空态「解析」按钮;已解析=内容右上「刷新」按钮
|
||
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(700); };
|
||
|
||
await send('Page.navigate', { url: 'http://localhost:8900/#/' });
|
||
if (!(await waitFor(`!!document.querySelector('#view')`))) fail('app boot');
|
||
await ev(`document.querySelectorAll('.modal-mask').forEach((m) => m.remove())`); // 清 root 设置弹窗
|
||
|
||
const out = {};
|
||
/* ============ 1) 未解析视频 #/video/58 ============ */
|
||
await goto('#/video/58');
|
||
if (!(await waitFor(`!!document.querySelector('#view .tabs')`))) fail('tabs render');
|
||
out.unparsed = await ev(`(() => {
|
||
const tabs = [...document.querySelectorAll('#view .tabs .tab')].map((t) => ({ txt: t.textContent.trim(), active: t.classList.contains('tab-active') }));
|
||
const es = document.querySelector('#view .empty-state');
|
||
const pb = document.querySelector('#view [data-parse]');
|
||
return {
|
||
tabs,
|
||
hasAnalysisTab: tabs.some((t) => t.txt === '视频分析'),
|
||
emptyState: !!es,
|
||
parseBtn: pb ? { mode: pb.dataset.parse, txt: pb.textContent.trim() } : null,
|
||
};
|
||
})()`);
|
||
// 切「视频拆解」tab → 也应空态+解析按钮
|
||
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
||
if (!(await waitFor(`document.querySelector('#view [data-parse]')`))) fail('deconstruct parse btn');
|
||
out.unparsedDecon = await ev(`(() => {
|
||
const pb = document.querySelector('#view [data-parse]');
|
||
return { mode: pb.dataset.parse, empty: !!document.querySelector('#view .empty-state'), activeTab: document.querySelector('#view .tabs .tab-active').textContent.trim() };
|
||
})()`);
|
||
// 点「解析」→ confirmRun 弹窗 → 取消(不触发真实任务)
|
||
await ev(`document.querySelector('#view [data-parse]').click()`);
|
||
if (!(await waitFor(`[...document.querySelectorAll('.modal-mask .modal')].some((m) => m.textContent.includes('解析'))`))) fail('parse modal');
|
||
out.parseModal = await ev(`(() => {
|
||
const m = [...document.querySelectorAll('.modal-mask .modal')].find((x) => x.textContent.includes('视频拆解') || x.querySelector('h3'));
|
||
const r = { title: m.querySelector('h3') ? m.querySelector('h3').textContent.trim() : '', hasOk: !!m.querySelector('[data-ok]') };
|
||
m.querySelector('[data-cancel]').click();
|
||
return r;
|
||
})()`);
|
||
await sleep(300);
|
||
|
||
/* ============ 2) 已解析视频 #/video/117 ============ */
|
||
await goto('#/video/117');
|
||
if (!(await waitFor(`!!document.querySelector('#view .md-body')`))) fail('parsed md-body');
|
||
out.parsed = await ev(`(() => {
|
||
const tools = document.querySelector('#view .vd-tools');
|
||
const pb = tools ? tools.querySelector('[data-parse]') : null;
|
||
return {
|
||
hasTools: !!tools,
|
||
refreshBtn: pb ? { mode: pb.dataset.parse, txt: pb.textContent.trim(), hasIcon: !!pb.querySelector('svg') } : null,
|
||
mdLen: document.querySelector('#view .md-body').textContent.length,
|
||
};
|
||
})()`);
|
||
// 切「视频拆解」tab:有内容→右上刷新;无内容→空态解析按钮(两种均可,但必须出现 data-parse)
|
||
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
||
if (!(await waitFor(`document.querySelector('#view [data-parse]')`))) fail('parsed decon btn');
|
||
out.parsedDecon = await ev(`(() => {
|
||
const pb = document.querySelector('#view [data-parse]');
|
||
return { mode: pb.dataset.parse, hasTools: !!document.querySelector('#view .vd-tools'), hasMd: !!document.querySelector('#view .md-body') };
|
||
})()`);
|
||
|
||
console.log(JSON.stringify(out, null, 1));
|
||
const ok = out.unparsed && !out.unparsed.__err
|
||
&& out.unparsed.tabs.length === 2 && out.unparsed.tabs[0].txt === '视频脚本' && out.unparsed.tabs[1].txt === '视频拆解'
|
||
&& out.unparsed.hasAnalysisTab === false
|
||
&& out.unparsed.emptyState === true && out.unparsed.parseBtn && out.unparsed.parseBtn.mode === 'parse' && out.unparsed.parseBtn.txt.includes('解析')
|
||
&& out.unparsedDecon && out.unparsedDecon.mode === 'parse' && out.unparsedDecon.empty === true && out.unparsedDecon.activeTab === '视频拆解'
|
||
&& out.parseModal && out.parseModal.hasOk && !out.parseModal.title.includes('刷新')
|
||
&& out.parsed && !out.parsed.__err && out.parsed.hasTools === true
|
||
&& out.parsed.refreshBtn && out.parsed.refreshBtn.mode === 'refresh' && out.parsed.refreshBtn.txt.includes('刷新') && out.parsed.refreshBtn.hasIcon
|
||
&& out.parsedDecon && (out.parsedDecon.mode === 'refresh' || (out.parsedDecon.mode === 'parse' && out.parsedDecon.hasMd === false));
|
||
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
|
||
ws.close();
|
||
process.exit(ok ? 0 : 1);
|