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(); }