Files
kefu/deploy/knowledge-v3/qa/browser.mjs
T
2026-09-21 10:34:06 +08:00

95 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRequire } from 'node:module';
import { readFileSync, writeFileSync } from 'node:fs';
const require=createRequire('C:/kefu/admin-web/package.json');
const { chromium }=require('playwright');
const root='C:/kefu/deploy/knowledge-v3/qa';
const fixture=JSON.parse(readFileSync(`${root}/fixture.json`,'utf8'));
const browser=await chromium.launch({executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true,args:['--no-proxy-server']});
const context=await browser.newContext({viewport:{width:1600,height:1040},locale:'zh-CN'});
await context.route('**/*', route=>new URL(route.request().url()).hostname==='127.0.0.1'?route.continue():route.abort());
const page=await context.newPage(); page.setDefaultTimeout(20000);
const errors=[]; const checks=[];
page.on('pageerror',error=>errors.push(error.message));
const origin='http://127.0.0.1:18867';
const login=await fetch(`${origin}/api/v2/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:'admin',password:'KnowledgeQA@2026'})}).then(r=>r.json());
const api=async(path)=>{
const r=await fetch(`${origin}/api/v2/knowledge/${path}?account_id=${fixture.account_id}`,{headers:{Authorization:`Bearer ${login.access_token}`}});
if(!r.ok) throw new Error(`API ${path}: ${r.status}`); return r.json();
};
const dialog=(title)=>page.getByRole('dialog').filter({has:page.getByText(title,{exact:true})});
const select=async(container,label,value)=>{
await container.locator('.ant-form-item').filter({has:page.locator('label').getByText(label,{exact:true})}).locator('.ant-select-selector').click();
await page.locator('.ant-select-dropdown:visible').getByText(value,{exact:true}).click();
};
try {
await page.goto(`${origin}/#/auth/login`);
await page.getByPlaceholder('用户名',{exact:true}).fill('admin');
await page.getByPlaceholder('密码',{exact:true}).fill('KnowledgeQA@2026');
await page.getByRole('button',{name:'login',exact:true}).click();
await page.waitForURL(url=>!url.hash.includes('/auth/login'));
await page.goto(`${origin}/#/knowledge/center`);
await page.getByRole('button',{name:'选择账号开始整理',exact:true}).click();
await page.locator('.ant-select-dropdown:visible').getByText('流程验收客服(QA001)',{exact:true}).click();
const wizard=dialog('从聊天整理知识'); await wizard.waitFor();
await select(wizard,'整理方式','模型整理:脱敏后调用指定模型');
await select(wizard,'整理模型','alpha · openai(默认)');
await wizard.locator('.ant-form-item').filter({hasText:'单次等待时间(秒)'}).locator('input').fill('60');
await wizard.getByRole('button',{name:'预览整理范围',exact:true}).click();
await wizard.getByRole('checkbox',{name:'确认来源账号由客服使用,所选聊天允许用于知识整理。'}).check();
await page.screenshot({path:`${root}/01-model-selection.png`,fullPage:true});
await wizard.getByRole('button',{name:'确认创建任务',exact:true}).click();
await wizard.waitFor({state:'hidden'});
await page.getByText('模型请求超时,请增加单次等待时间或更换模型后继续',{exact:true}).waitFor();
let jobs=await api('jobs');
if(jobs.items[0].created_items!==1 || jobs.items[0].pending_items!==2) throw new Error('First successful candidate not durable');
if(jobs.items[0].options.model_provider_id!=='alpha' || jobs.items[0].options.model_timeout_seconds!==60) throw new Error('Model settings not saved');
checks.push('创建任务指定模型和超时;第二条失败时第一条草稿已保存');
await page.getByRole('button',{name:'配置 / 继续',exact:true}).click();
const config=dialog('调整配置并继续任务');
await select(config,'整理模型','beta · dify');
await config.locator('.ant-form-item').filter({hasText:'单次等待时间(秒)'}).locator('input').fill('120');
await page.screenshot({path:`${root}/02-retry-model.png`,fullPage:true});
await config.getByRole('button',{name:'保存并继续',exact:true}).click();
await config.waitFor({state:'hidden'});
await page.getByText('所选范围已读取完成',{exact:true}).waitFor();
jobs=await api('jobs');
if(jobs.items[0].created_items!==3 || jobs.items[0].model_calls!==4 || jobs.items[0].options.model_provider_id!=='beta') throw new Error('Resume repeated successful candidates');
checks.push('更换模型后从待处理问答继续,已完成草稿不重跑');
await page.getByRole('tab',{name:'知识与审核',exact:true}).click();
const data=await api('items'); const single=data.items[0];
await page.getByRole('button',{name:single.title,exact:true}).click();
const editor=dialog('知识审核与来源');
if(await editor.getByRole('button',{name:'删除已驳回知识',exact:true}).count()) throw new Error('Draft can delete before rejection');
await editor.getByRole('button',{name:/驳\s*回/,exact:true}).click();
await editor.getByRole('button',{name:'删除已驳回知识',exact:true}).click();
await page.getByRole('button',{name:'确认删除',exact:true}).click();
await editor.waitFor({state:'hidden'});
if((await api('items')).total!==2) throw new Error('Single delete failed');
checks.push('草稿不可删除,驳回后可确认删除单条');
const remaining=(await api('items')).items;
for(const item of remaining) await page.locator('tbody tr').filter({has:page.getByRole('button',{name:item.title,exact:true})}).getByRole('checkbox').check();
if(await page.getByRole('button',{name:'删除已驳回',exact:true}).isEnabled()) throw new Error('Batch draft deletion enabled');
await page.getByRole('button',{name:'驳回选中',exact:true}).click();
const reject=dialog('批量驳回 · 2 条');
await reject.getByRole('button',{name:'确认驳回 2 条',exact:true}).click();
await reject.getByText('成功 2 · 失败 0 · 未执行 0',{exact:true}).waitFor();
await reject.getByRole('button',{name:/完\s*成/}).click();
for(const item of remaining) await page.locator('tbody tr').filter({has:page.getByRole('button',{name:item.title,exact:true})}).getByRole('checkbox').check();
await page.getByRole('button',{name:'删除已驳回',exact:true}).click();
const deletion=dialog('批量删除 · 2 条');
await deletion.getByText('可操作 2 条 · 需先处理 0 条',{exact:true}).waitFor();
await page.screenshot({path:`${root}/03-batch-delete.png`,fullPage:true});
await deletion.getByRole('button',{name:'确认删除 2 条',exact:true}).click();
await deletion.getByText('成功 2 · 失败 0 · 未执行 0',{exact:true}).waitFor();
await deletion.getByRole('button',{name:/完\s*成/}).click();
if((await api('items')).total!==0) throw new Error('Batch deletion failed');
checks.push('批量驳回后确认删除,逐条显示成功结果');
if(errors.length) throw new Error(JSON.stringify(errors));
writeFileSync(`${root}/result.json`,JSON.stringify({passed:true,checks,pageErrors:errors},null,2));
console.log(JSON.stringify({passed:true,checks,pageErrors:errors}));
} catch(error) {
await page.screenshot({path:`${root}/failure.png`,fullPage:true});
writeFileSync(`${root}/failure.txt`,String(error)+'\n'+await page.locator('body').innerText());
throw error;
} finally { await browser.close(); }