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 分钟超时,用户零感知)
122 lines
8.1 KiB
JavaScript
122 lines
8.1 KiB
JavaScript
// 模拟点击测试:俊希「AI创作」界面链路(#/accounts → AI创作弹窗 → AI 选题生成)
|
|
// 覆盖测试用例 TC-09 的界面交互环节(真实模拟用户点击,非仅文字步骤)
|
|
// 2026-09-01 更新:按钮/弹窗标题已由「AI写脚本」统一改名为「AI创作」(cdp-test 断言同步)
|
|
//
|
|
// 前置:
|
|
// 1. 工作台服务运行中(node server.js,默认端口 8900;可传参指定)
|
|
// 2. 浏览器 9223 调试实例已启动(browser-harness 调试实例)
|
|
// 3. 账号列表(hot_accounts)含「俊希」
|
|
// 用法:
|
|
// node cdp-test-junxi.mjs # 只验证到「选题生成」,不触发创作任务(安全模式)
|
|
// node cdp-test-junxi.mjs --confirm # 选题生成后继续点「确认执行」,真实触发创作任务
|
|
// node cdp-test-junxi.mjs --port 8900 # 指定工作台端口(默认 8900)
|
|
// 说明:脚本在 9223 实例新开标签页操作,不干扰用户已打开的页面;结束后自动关闭新开标签页。
|
|
|
|
const argPort = process.argv.indexOf('--port');
|
|
const PORT = argPort > -1 && process.argv[argPort + 1] ? process.argv[argPort + 1] : '8900';
|
|
const CONFIRM = process.argv.includes('--confirm');
|
|
const BASE = 'http://localhost:' + PORT;
|
|
|
|
// 1) 连接 9223 调试实例
|
|
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
|
const page = list.find((t) => t.type === 'page');
|
|
if (!page) { console.log('ERR: 9223 无可用页面,请先启动 browser-harness 调试实例'); 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, 300) }; return r.result.value; };
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
await send('Page.enable'); await send('Runtime.enable');
|
|
|
|
// 2) 新开标签页打开账号列表(不干扰现有页面)
|
|
const { targetId } = await send('Target.createTarget', { url: BASE + '/#/accounts' });
|
|
await sleep(2500);
|
|
let targetPage = list2 => list2.find((t) => t.type === 'page' && t.url.includes('localhost:' + PORT) && t.id === targetId);
|
|
let pages = await fetch('http://localhost:9223/json').then((r) => r.json());
|
|
let tp = targetPage(pages);
|
|
if (!tp) { console.log('ERR: 新开标签页未找到 ' + PORT + ' 页面'); process.exit(1); }
|
|
// 切换 WS 到新页面
|
|
const ws2 = new WebSocket(tp.webSocketDebuggerUrl);
|
|
let id2 = 0; const pending2 = new Map();
|
|
const send2 = (method, params = {}) => new Promise((res, rej) => { const i = ++id2; pending2.set(i, { res, rej }); ws2.send(JSON.stringify({ id: i, method, params })); });
|
|
ws2.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending2.has(m.id)) { const p = pending2.get(m.id); pending2.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
|
|
await new Promise((r) => (ws2.onopen = r));
|
|
const ev2 = async (expr) => { const r = await send2('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; };
|
|
const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
await send2('Page.enable'); await send2('Runtime.enable');
|
|
await send2('Page.reload', { ignoreCache: true }); await sleep2(3500);
|
|
|
|
// 3) 账号列表页:定位俊希行「AI写脚本」按钮
|
|
const findBtn = await ev2(`(() => {
|
|
const btns = [...document.querySelectorAll('[data-create]')];
|
|
const b = btns.find((x) => x.dataset.create === '俊希');
|
|
if (!b) return { found: false, total: btns.length, names: btns.slice(0, 20).map((x) => x.dataset.create) };
|
|
return { found: true, accId: b.dataset.accId, txt: b.textContent.trim() };
|
|
})()`);
|
|
console.log('【Step1 账号列表定位俊希】', JSON.stringify(findBtn));
|
|
if (!findBtn.found) {
|
|
console.log('FAIL: 账号列表未找到俊希「AI创作」按钮(若账号未关注,请先到周榜/账号列表关注俊希)');
|
|
await send('Target.closeTarget', { targetId }); ws.close(); ws2.close(); process.exit(1);
|
|
}
|
|
|
|
// 4) 模拟点击「AI创作」→ 轮询等待弹窗出现(confirmAiScript 先 await persona 接口再建弹窗,需等待)
|
|
await ev2(`(() => { const b = [...document.querySelectorAll('[data-create]')].find((x) => x.dataset.create === '俊希'); b.click(); return true; })()`);
|
|
const modal = await ev2(`(async () => {
|
|
for (let i = 0; i < 24; i++) {
|
|
// 只认非 hidden 且含 #aiTopicGen 的弹窗(index.html 有 hidden 的 #settingsModal 干扰)
|
|
const m = [...document.querySelectorAll('.modal-mask')].find((x) => !x.hidden && x.querySelector('#aiTopicGen'));
|
|
if (m) {
|
|
return { open: true, title: m.querySelector('h3')?.textContent.trim() || '', desc: m.querySelector('.dim')?.textContent.trim() || '', hasTopicInput: !!m.querySelector('#aiTopicInput'), hasGenBtn: !!m.querySelector('#aiTopicGen'), hasOk: !!m.querySelector('[data-ok]') };
|
|
}
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
return { open: false };
|
|
})()`);
|
|
console.log('【Step2 AI创作弹窗】', JSON.stringify(modal, null, 1));
|
|
const descOk = modal.open && modal.title.includes('AI创作') && modal.hasTopicInput && modal.hasGenBtn;
|
|
const branch = modal.open && (modal.desc || '').includes('账号设定');
|
|
// 09-01 修正:有 persona 时 desc=「基于当前账号设定…」;无 persona 时才提示先分析账号设定
|
|
const firstTime = modal.open && /先.*账号设定|暂无账号设定|尚未.*设定/.test(modal.desc || '');
|
|
console.log(firstTime ? ' → 文案分支:先进行账号设定分析(首次场景)' : branch ? ' → 文案分支:基于当前账号设定(有设定场景)' : ' → 文案分支:未识别(需人工确认)');
|
|
|
|
// 5) 模拟点击「AI」选题按钮 → 轮询选题输入框(Dify 约 50s)
|
|
await ev2(`(() => { const g = document.querySelector('#aiTopicGen'); g.click(); return true; })()`);
|
|
console.log('【Step3 AI 选题生成中…(最长等待 100s)】');
|
|
let topics = '';
|
|
const t0 = Date.now();
|
|
while (Date.now() - t0 < 100000) {
|
|
await sleep2(3000);
|
|
topics = (await ev2(`(() => document.querySelector('#aiTopicInput')?.value || '')()`)) || '';
|
|
if (topics && !topics.includes('(未生成内容)')) break;
|
|
}
|
|
console.log('【Step4 选题结果】');
|
|
console.log(topics || '(超时未返回)');
|
|
const topicOk = topics.split('\n').filter((l) => l.trim()).length >= 3 || /选题|方向/.test(topics);
|
|
|
|
// 6) --confirm 模式:确认执行触发创作任务
|
|
if (CONFIRM) {
|
|
console.log('【Step5 --confirm】点击确认执行,触发创作任务…');
|
|
await ev2(`(() => { document.querySelector('[data-ok]').click(); return true; })()`);
|
|
await sleep2(1500);
|
|
const toast = await ev2(`(() => document.querySelector('.toast')?.textContent || '')()`);
|
|
console.log(' toast:', toast || '(未捕获)');
|
|
} else {
|
|
console.log('【Step5 安全模式】未触发创作任务(加 --confirm 参数可真实触发)');
|
|
await ev2(`(() => { document.querySelector('[data-cancel]')?.click(); return true; })()`);
|
|
}
|
|
|
|
// 7) 汇总
|
|
console.log('\n===== 测试结果 =====');
|
|
console.log('Step1 定位俊希按钮:', findBtn.found ? 'PASS' : 'FAIL');
|
|
console.log('Step2 弹窗结构:', descOk ? 'PASS' : 'FAIL');
|
|
console.log('Step3-4 AI 选题:', topicOk ? 'PASS' : 'CHECK');
|
|
console.log(CONFIRM ? 'Step5 任务触发: 已提交(到左侧会话栏查看)' : 'Step5 任务触发: 未触发(安全模式)');
|
|
|
|
// 关闭新开标签页
|
|
await send('Target.closeTarget', { targetId });
|
|
ws.close(); ws2.close();
|
|
process.exit(descOk && findBtn.found ? 0 : 1);
|