Track the fixed PhotoCraft source baseline with the existing native client integration. Use the actual Pi SDK for normal sends, durable session recovery, demand-loaded enterprise rules and checked undoable canvas tools. Retain request identities and never resend unknown inference. Include local packaging and real-call acceptance evidence.
103 lines
8.5 KiB
JavaScript
103 lines
8.5 KiB
JavaScript
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(); }
|