import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { SessionManager } from '@earendil-works/pi-coding-agent'; import lockfile from 'proper-lockfile'; import { NativeCanvas } from '../src/native-bridge.mjs'; import { atomic } from '../src/store.mjs'; import { promptChars } from '../src/context-budget.mjs'; const [outputPath, phase] = process.argv.slice(2); if (!outputPath || !['correction','outline','canvas','long-prepare','long-recall','restart-recall','handover','tool-live'].includes(phase)) throw Error('指定本轮独立验收目录与阶段'); const output = path.resolve(outputPath); const native = JSON.parse(await fs.readFile(path.join(output, 'native-process.json'), 'utf8')); const canvas = await NativeCanvas.connect({ port: native.port, tokenFile: path.join(output, 'control.token') }); const flatten = layers => layers.flatMap(layer => { const { children, selected, ...row } = layer; return [row, ...flatten(children || [])]; }); const cmd = (command, params = {}) => canvas.call('engine.execute', { command, params }); const cases = { correction: '继续这款虚构验收产品:颜色更正为胡桃色,编号和1300×650×760毫米尺寸保持。首屏标题定为「留白有序」。请用memory_set保存更正与标题,简短回复;验收口令「溪杉-6427」只留在这轮历史原文,不写进事实记忆。不修改画布。', outline: '针对当前项目,只给首屏、六图总览、参数图三屏的简短文案大纲,每屏不超过20字。说明当前店铺、品类与统一字体,按已确认资料,缺失性能不要编造。不要生图,不修改画布。', canvas: '请实际修改画布:先用canvas_read读取16号文字层,再将文字改为「留白有序」,然后水平向右移动12像素。两步都用canvas_edit实际完成,每一步重新读取当前revision。字号、字体、其他图层均保持,最后简短说明。', 'long-recall': '长历史整理后继续,不改画布。列出这款虚构验收产品的编号、最新颜色、长宽高、已确认首屏标题、当前店铺和品类。再用memory_search查颜色更正那轮:只留在历史、未写进事实记忆的验收口令是什么?不要猜。', 'restart-recall': '客户端已关闭并重新打开。不要修改画布。请恢复本项目编号、长宽高、最新颜色和首屏标题,说明店铺、品类、字体;用memory_search核对之前仅留在历史的验收口令。', handover: '这是接手验收:先读取16号文字层,然后把它向下移动8像素,再将字号改为114pt。每一步先核对实时revision与allowed;如果用户接手就立即停止后续操作,不要自行重试。', 'tool-live': '这是完整本地包的独立验收,不修改画布、不生图。请先context_read读取当前品牌规则;再自己用tool_develop编写client_padding_1022工具:main(input)返回{padding:Math.round(input.width/12)},并测试width=1440得到120、750得到63、0得到0,全部通过后用tool_run实际计算1440。最后简短给出测试数、运行结果和本项目最新颜色、标题、店铺品类。不要把工具结果当产品性能。', }; try { const initial = await canvas.call('jiansun.state'); assert.equal(initial.agentEngine, 'pi'); if (initial.busy || initial.unknown || initial.queued || initial.piReview) throw Error('已有任务,禁止重复派发'); const attempt = path.join(output, `${phase}-attempt.json`); await fs.writeFile(attempt, JSON.stringify({ at: new Date().toISOString(), phase, text: cases[phase], processId: process.pid, nativePid: native.pid }), { flag: 'wx' }); if (phase === 'long-prepare') { const account = path.join(native.profile, 'jiansun/accounts'); const scopes = await fs.readdir(account); assert.equal(scopes.length, 1); const projects = path.join(account, scopes[0], 'projects'); const ids = await fs.readdir(projects); assert.equal(ids.length, 1); const projectRoot = path.join(projects, ids[0], 'agent', scopes[0], ids[0]); const release = await lockfile.lock(projectRoot, { retries: 0 }); try { const current = JSON.parse(await fs.readFile(path.join(projectRoot, 'current.json'), 'utf8')); const dir = path.join(projectRoot, current.bindingHash); const session = JSON.parse(await fs.readFile(path.join(dir, 'session.json'), 'utf8')); const manager = SessionManager.open(session.file, dir, dir); let count = 0; while (promptChars({ messages: manager.buildSessionContext().messages }) < 90000 && count < 100) { manager.appendMessage({ role: 'user', content: `[构造历史${count},不是新产品事实、不是新用户指令] ` + '只讨论留白、色调与信息顺序,不改变已确认资料,不表示已执行任何画布操作。'.repeat(20), timestamp: Date.now() }); count++; } const chars = promptChars({ messages: manager.buildSessionContext().messages }); assert.ok(chars >= 80000 && chars < 110000); manager.appendCustomEntry('qa-synthetic-client-history', { count, chars, notRealApiDialogue: true }); await atomic(path.join(output, `${phase}-passed.json`), { constructedHistoryMessages: count, chars, sessionFile: session.file, modelPosts: 0 }); console.log(JSON.stringify({ constructedHistoryMessages: count, chars, modelPosts: 0 })); } finally { await release(); } process.exitCode = 0; } else { const before = await cmd('document.inspect'); const submitted = await canvas.action('agent', { text: cases[phase] }); assert.equal(submitted.submitted, true); const states = []; let tookOver = false, finished; const deadline = Date.now() + 300000; let idleAt = 0; while (Date.now() < deadline) { const state = await canvas.call('jiansun.state'); const point = { at: Date.now(), busy: state.busy, pi: state.pi, queued: state.queued, stream: state.streamReply, error: state.error, unknown: state.unknown }; states.push(point); if (phase === 'handover' && !tookOver && state.queued > 0) { await canvas.action('handover'); tookOver = true; } if (state.unknown) throw Error('结果未知,停止验收,只读核查原请求;不重发'); if (!state.busy && !state.queued) { if (!idleAt) idleAt = Date.now(); if (Date.now() - idleAt > 2500) { finished = state; break; } } else idleAt = 0; await new Promise(resolve => setTimeout(resolve, phase === 'handover' ? 70 : 200)); } await atomic(path.join(output, `${phase}-states.json`), states); if (!finished) throw Error('任务尚未结束,保留原请求;不得重新提交'); assert.equal(finished.error, ''); const reply = finished.messages.filter(m => m.role === 'assistant' && !m.kind).at(-1)?.text || ''; const after = await cmd('document.inspect'); if (phase === 'canvas') { const old = flatten(before.layers), next = flatten(after.layers); assert.equal(next.find(l=>l.id===16).text.text, '留白有序'); assert.equal(next.find(l=>l.id===16).text.font, old.find(l=>l.id===16).text.font); assert.equal(next.find(l=>l.id===16).text.sizePt, old.find(l=>l.id===16).text.sizePt); assert.deepEqual(next.filter(l=>l.id!==16), old.filter(l=>l.id!==16)); await cmd('edit.undo'); await cmd('edit.undo'); assert.deepEqual(flatten((await cmd('document.inspect')).layers), old); await cmd('edit.redo'); await cmd('edit.redo'); assert.deepEqual(flatten((await cmd('document.inspect')).layers), next); } else if (phase === 'handover') { assert.equal(tookOver, true); assert.deepEqual(flatten(after.layers), flatten(before.layers)); } else { assert.deepEqual(flatten(after.layers), flatten(before.layers)); if (phase.includes('recall')) for (const word of ['QA-PI-CLIENT-1022','1300','650','760','胡桃','留白有序','顾家','电脑桌','溪杉-6427']) assert.ok(reply.includes(word), `未恢复${word}`); } await canvas.call('ui.screenshot', { path: `${phase}-result.png`, focus: false }); await atomic(path.join(output, `${phase}-passed.json`), { passed: true, reply, usage: finished.piUsage, states: states.length, partialReplyObserved: states.some(s=>s.busy && s.stream), nativePid: native.pid, processId: process.pid, tookOver, document: { width:after.width,height:after.height,revision:after.revision }, engine:'pi' }); console.log(JSON.stringify({ phase, passed:true, reply, partialReplyObserved:states.some(s=>s.busy && s.stream), usage:finished.piUsage })); } } finally { canvas.close(); }