| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import assert from 'node:assert/strict';
- import crypto from 'node:crypto';
- import { pathToFileURL } from 'node:url';
- import { manualFile, bankFile, compileDialogue } from './update-workshop-dialogue-manual.mjs';
- const outIndex = process.argv.indexOf('--out');
- const out = path.resolve(outIndex < 0 ? 'docs/workshop-dialogue-guide-20261111' : process.argv[outIndex + 1]);
- await fs.mkdir(out, { recursive: true });
- const html = await fs.readFile(manualFile, 'utf8');
- const data = JSON.parse(html.match(/window\.WORKSHOP_MANUAL_DATA = (\{[\s\S]*?\n\});\r?\n<\/script>/)[1]);
- const bank = JSON.parse(await fs.readFile(bankFile, 'utf8'));
- const map = compileDialogue(bank, data.steps);
- const embedded = JSON.parse(html.match(/ const dialogueMap = (\{[^\r\n]+\});/)[1]);
- assert.deepEqual(embedded, map, '界面复制文本必须来自当前提示词库');
- const protectedRoot = 'docs/ksf-workshop-skill-20261110-r13-r2/package/skills/ksf-workshop-runtime';
- const protectedFiles = ['runtime-lock.json', ...['ksf', 'ksf-secure', 'ksf-workshop', 'ksf-identity', 'ksf-identity-bridge', 'describe-business-schema'].map(file => 'scripts/' + file + '.mjs')];
- const protectedChecks = [];
- for (const file of protectedFiles) {
- const actual = await fs.readFile('skills/ksf-workshop-runtime/' + file);
- assert.deepEqual(actual, await fs.readFile(protectedRoot + '/' + file), '受保护文件变化:' + file);
- protectedChecks.push({ file, sha256: crypto.createHash('sha256').update(actual).digest('hex') });
- }
- const manifest = await fs.readFile('skills/ksf-workshop-runtime/FILES.sha256', 'utf8');
- let filesVerified = 0;
- for (const line of manifest.trim().split(/\r?\n/)) {
- const [, expected, file] = line.match(/^([a-f0-9]{64})\s+(.+)$/);
- assert.equal(crypto.createHash('sha256').update(await fs.readFile('skills/ksf-workshop-runtime/' + file)).digest('hex'), expected, file);
- filesVerified++;
- }
- for (const step of data.steps.filter(step => step.id !== 'topic')) {
- assert.ok(bank.steps[step.id].rounds.some(round => round.deliver), '环节缺少完整成报轮:' + step.id);
- }
- const { chromium } = await import(pathToFileURL(path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs')).href);
- const browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' });
- const origin = 'http://127.0.0.1:4181';
- const url = origin + '/' + manualFile.split('/').map(encodeURIComponent).join('/');
- const checks = [], errors = [], blocked = [];
- let copied = 0;
- try {
- const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
- await context.route('**/*', route => {
- const request = route.request();
- if (new URL(request.url()).origin === origin && request.method() === 'GET') return route.continue();
- blocked.push({ method: request.method(), url: request.url() });
- return route.abort();
- });
- await context.addInitScript(() => {
- window.__copiedPrompt = null;
- Object.defineProperty(navigator, 'clipboard', { configurable: true, value: {
- writeText: async text => { window.__copiedPrompt = text; }
- } });
- });
- const page = await context.newPage();
- page.on('pageerror', error => errors.push(error.message));
- await page.goto(url);
- for (const width of [1280, 390]) {
- await page.setViewportSize({ width, height: 900 });
- for (const step of data.steps) {
- await page.evaluate(id => { location.hash = id; }, step.id);
- await page.locator('#prompt-' + step.id + '-0').waitFor();
- const rounds = map[step.id];
- assert.equal(await page.locator('[data-copy-round]').count(), rounds.length);
- assert.equal(await page.locator('.round-feedback').count(), rounds.length);
- const duplicateIds = await page.evaluate(() => {
- const ids = [...document.querySelectorAll('[id]')].map(node => node.id);
- return ids.filter((id, i) => ids.indexOf(id) !== i);
- });
- assert.deepEqual(duplicateIds, []);
- const size = await page.evaluate(() => ({ innerWidth, scrollWidth: document.documentElement.scrollWidth }));
- assert.ok(size.scrollWidth <= size.innerWidth, step.id + ': ' + JSON.stringify(size));
- for (let i = 0; i < rounds.length; i++) {
- assert.equal(await page.locator('#prompt-' + step.id + '-' + i).textContent(), rounds[i].prompt);
- if (width === 1280) {
- await page.locator('[data-copy-round="' + i + '"]').click();
- assert.equal(await page.evaluate(() => window.__copiedPrompt), rounds[i].prompt, '实际点击复制了错误轮次');
- copied++;
- }
- }
- }
- checks.push(width + 'px:14个环节轮数、正文、反馈、唯一ID及无横向溢出');
- await page.evaluate(() => { location.hash = 'category'; });
- await page.locator('#prompt-category-0').waitFor();
- await page.evaluate(() => scrollTo(0, 0));
- await page.screenshot({ path: path.join(out, 'manual-' + width + '.png') });
- }
- checks.push(copied + '段按钮实际复制内容与唯一提示词源逐字一致');
- await page.evaluate(() => { location.hash = 'topic'; });
- await page.locator('#prompt-topic-0').waitFor();
- assert.equal(await page.locator('.round-title').first().textContent(), '第1轮 / 3 确认称呼与登录账号');
- await page.evaluate(() => scrollTo(0, 0));
- await page.screenshot({ path: path.join(out, 'account-first-round-390.png') });
- checks.push('01首位为独立称呼与账号轮,课题研究从第二轮开始');
- await page.locator('.menu-button').click();
- await page.locator('.nav-item[href="#rules"]').click();
- await page.locator('#prompt-rules-0').waitFor();
- assert.equal(await page.locator('.menu-button').getAttribute('aria-expanded'), 'false');
- await page.locator('#next').click();
- await page.locator('#prompt-competitors-0').waitFor();
- await page.locator('#previous').click();
- await page.locator('#prompt-rules-0').waitFor();
- checks.push('移动目录、前后环节导航能切换正确提示词');
- // Clipboard failures must select the clicked round, not the first prompt on the page.
- await page.evaluate(() => { location.hash = 'proposal'; });
- await page.locator('#prompt-proposal-3').waitFor();
- assert.equal(await page.locator('.round-prerequisite').textContent(), map.proposal[3].prerequisite);
- await page.locator('.round-prerequisite').scrollIntoViewIfNeeded();
- await page.screenshot({ path: path.join(out, 'proposal-confirmation-390.png') });
- await page.evaluate(() => {
- navigator.clipboard.writeText = async () => { throw Error('Clipboard denied for QA'); };
- document.execCommand = () => false;
- });
- await page.locator('[data-copy-round="3"]').click();
- assert.equal(await page.evaluate(() => getSelection().toString()), map.proposal[3].prompt);
- checks.push('剪贴板拒绝时仅选中所点的第四轮正文;确认前提在按钮前可见');
- await page.evaluate(() => {
- Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true });
- document.execCommand = command => {
- window.__fallbackCopied = command === 'copy' ? document.activeElement.value : null;
- return command === 'copy';
- };
- });
- await page.locator('[data-copy-round="1"]').click();
- assert.equal(await page.evaluate(() => window.__fallbackCopied), map.proposal[1].prompt);
- assert.equal(await page.locator('textarea').count(), 0);
- checks.push('非安全上下文复制回退传入正确段落并移除临时文本框');
- const before = await page.evaluate(() => Object.keys(localStorage));
- await page.locator('#completed').check();
- assert.ok(await page.evaluate(() => Object.keys(localStorage).length >= 1));
- await page.locator('#completed').uncheck();
- checks.push('勾选进度仅保留本机,不触发业务网络写入');
- assert.deepEqual(blocked, []);
- assert.deepEqual(errors, []);
- await context.close();
- const offline = await browser.newContext({ offline: true, viewport: { width: 390, height: 900 } });
- const filePage = await offline.newPage();
- filePage.on('pageerror', error => errors.push(error.message));
- await filePage.goto(pathToFileURL(path.resolve(manualFile)).href + '#category');
- assert.equal(await filePage.locator('[data-copy-round]').count(), 3);
- assert.equal(await filePage.locator('#prompt-category-2').textContent(), map.category[2].prompt);
- checks.push('直接以file://离线打开时,分轮正文及导航无需联网加载');
- await offline.close();
- } finally {
- await browser.close();
- }
- assert.deepEqual(errors, []);
- const result = { ok: true, checks, stepCount: data.steps.length, roundCount: Object.values(map).flat().length,
- copiedRoundsVerified: copied, publicFilesVerified: filesVerified + 1, protectedFiles: protectedChecks,
- browserErrors: errors, blockedRequests: blocked, productionWrites: 0,
- limitations: ['浏览器与脚本核验不证明实际课堂研究质量提升', '未发起真实采集、模型对照试验或业务入库'] };
- await fs.writeFile(path.join(out, 'verification.json'), JSON.stringify(result, null, 2) + '\n');
- console.log(JSON.stringify({ ok: true, checks: checks.length, copiedRoundsVerified: copied, publicFilesVerified: filesVerified + 1 }));
|