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.
95 lines
9.7 KiB
JavaScript
95 lines
9.7 KiB
JavaScript
import fs from 'node:fs/promises';
|
||
import path from 'node:path';
|
||
import assert from 'node:assert/strict';
|
||
import { randomUUID } from 'node:crypto';
|
||
import { SessionManager } from '@earendil-works/pi-coding-agent';
|
||
import { Native98kBroker } from '../src/native-bridge.mjs';
|
||
import { DetailPiAgent } from '../src/agent.mjs';
|
||
import { ProjectStore, atomic } from '../src/store.mjs';
|
||
import { resolveRules } from '../src/rules.mjs';
|
||
|
||
const [priorFile, outputDir, phase] = process.argv.slice(2);
|
||
if (!priorFile || !outputDir || !['prepare', 'A', 'B', 'C'].includes(phase)) throw Error('用法:<已有配置> <独立验收目录> <prepare|A|B|C>');
|
||
const output = path.resolve(outputDir), configFile = path.join(output, 'config.local.json');
|
||
if (phase === 'prepare') {
|
||
if (await fs.access(configFile).then(() => true, () => false)) throw Error('已有验收身份,不能生成新身份绕过原请求');
|
||
const prior = JSON.parse(await fs.readFile(priorFile, 'utf8'));
|
||
const catalog = JSON.parse(await fs.readFile('output/pi-compatibility-20261021/enterprise-rules.json', 'utf8'));
|
||
const combo = catalog.bindings.find(b => b.id === 'GJ-DESK-DNZ0385'), shop = catalog.shops.find(s => s.id === combo.shopId);
|
||
const model = JSON.parse(await fs.readFile('output/pi-compatibility-20261021/public-docs/grok-4.7.json', 'utf8'))[0];
|
||
await atomic(configFile, { output, broker: { ...prior.broker, project: randomUUID(), invocationHeaders: model.invocation.headers },
|
||
binding: resolveRules({ brandId: shop.brandId, shopId: shop.id, categoryId: combo.categoryId }, catalog),
|
||
protocolMode: 'native-tools', createdAt: new Date().toISOString(), expectedModel: model.id });
|
||
console.log('已建立固定验收身份,未调用模型');
|
||
process.exit(0);
|
||
}
|
||
const config = JSON.parse(await fs.readFile(configFile, 'utf8'));
|
||
if (phase !== 'A' && !await fs.access(path.join(output, `accepted-${phase === 'B' ? 'A' : 'B'}.json`)).then(() => true, () => false)) throw Error('前一阶段未通过,禁止继续批量调用');
|
||
const broker = new Native98kBroker(config.broker), info = await broker.info();
|
||
assert.equal(info.model.id, config.expectedModel);
|
||
if (info.pending.length) throw Error('本项目有待核查请求,禁止继续调用');
|
||
const store = await ProjectStore.open(output, { scope: info.accountScope, project: config.broker.project }, config.binding);
|
||
let host;
|
||
const events = [], turns = [];
|
||
const open = () => DetailPiAgent.open({ store, broker, callBudget: 4, onEvent: event => { events.push(event); if (event.type !== 'text_delta') console.log(JSON.stringify(event)); } });
|
||
const facts = text => { assert.match(text, /QA-PI-DESK-8264/); for (const n of ['1300', '650', '760']) assert.match(text, new RegExp(`(?<![0-9])${n}(?![0-9])`)); assert.match(text, /胡桃/); };
|
||
async function turn(id, text, verify, options) {
|
||
const attempt = path.join(output, `${phase}-${id}-attempt.json`);
|
||
if (await fs.access(attempt).then(() => true, () => false)) throw Error(`此步骤已尝试,禁止再次提交:${phase}-${id}`);
|
||
await atomic(attempt, { text, processId: process.pid, at: new Date().toISOString() });
|
||
const start = events.length, result = await host.prompt(text, options);
|
||
if (verify) verify(result, events.slice(start));
|
||
const entry = { id, text, ...result, events: events.slice(start).filter(e => e.type !== 'text_delta'), at: new Date().toISOString() };
|
||
turns.push(entry); await atomic(path.join(output, `${phase}-${id}-passed.json`), entry);
|
||
console.log(JSON.stringify({ phase, step: id, rounds: result.rounds, text: result.text }));
|
||
}
|
||
try {
|
||
host = await open();
|
||
if (phase === 'A') {
|
||
await turn('01', '这是虚构产品,仅验收上下文,不用于商品。产品编号QA-PI-DESK-8264;尺寸1300×650×760毫米;颜色米白色。请用memory_set记到product_summary,不记录下面的标记。起始验收标记为溪石-7384,只留在历史原文。记好后简短回复。', (_r, e) => assert.ok(e.some(x => x.toolName === 'memory_set' && x.type === 'tool_execution_end' && !x.isError)), { requiredTool: 'memory_set' });
|
||
// Seeded replies are explicitly marked, never counted as real model calls.
|
||
await host.dispose(); host = null;
|
||
const saved = await store.get('session.json'), manager = SessionManager.open(saved.file, store.dir, store.dir);
|
||
for (let i = 0; i < 40; i++) {
|
||
manager.appendMessage({ role: 'user', content: `[构造历史压力消息${i},不是新的产品事实] ` + '讨论页面留白、光影与文案节奏;不得改变已确认的产品信息。'.repeat(6), timestamp: Date.now() });
|
||
manager.appendMessage({ role: 'assistant', api: 'openai-completions', provider: 'jiansun-native-98k', model: info.model.id,
|
||
content: [{ type: 'text', text: `[构造历史回复${i},非模型输出] ` + '此消息只为测试容量,不代表已执行编辑,不新增产品参数。'.repeat(6) }], stopReason: 'stop', timestamp: Date.now(),
|
||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
|
||
}
|
||
manager.appendCustomEntry('qa-pressure', { seededMessages: 80, notRealModelReplies: true });
|
||
host = await open();
|
||
await turn('02', '首屏先突出办公环境,避免极端承诺;这次只回复收到,不操作工具。');
|
||
await turn('03', '首屏草稿标题用「留白有序」,请用memory_set记录到page_title,不改画布。', undefined, { requiredTool: 'memory_set' });
|
||
await turn('04', '如果资料没有承重数值,详情页应怎样处理?只回答一句,不新增事实。', r => assert.match(r.text, /待确认|不编造|不写|不添加|不展示|确认|核实/));
|
||
await turn('05', '这个虚构产品的颜色改为胡桃色,之前米白色作废;编号和尺寸不变。请更新memory_set中的product_summary,保留编号和尺寸,再简短回复。', undefined, { requiredTool: 'memory_set' });
|
||
const memory = (await store.context()).confirmedMemory;
|
||
facts(memory.product_summary.value); assert.match(memory.page_title.value, /留白有序/);
|
||
assert.ok(!JSON.stringify(memory).includes('溪石-7384'));
|
||
await turn('06', '先只做首屏、六图总览和参数图三个方案,记录到page_scope;这是策划范围,暂不操作画布。', undefined, { requiredTool: 'memory_set' });
|
||
await turn('07', '按当前真实品牌规范,人物是不是整套所有场景都必须同一张脸?简短回答。', r => assert.match(r.text, /不|连续/));
|
||
await turn('08', '现在核对最新编号、长宽高、有效颜色、草稿标题和当前店铺品类,简短回答。', r => { facts(r.text); assert.match(r.text, /留白有序/); assert.match(r.text, /顾家/); assert.match(r.text, /电脑桌/); });
|
||
} else if (phase === 'B') {
|
||
await turn('01', '进程已经重启。请通过memory_search查找「起始验收标记」的历史原文,再报告这个标记以及最新产品编号、尺寸和颜色,不要猜。', (r, e) => { facts(r.text); assert.match(r.text, /溪石-7384/); assert.ok(e.some(x => x.toolName === 'memory_search' && x.type === 'tool_execution_end' && !x.isError)); }, { requiredTool: 'memory_search' });
|
||
await turn('02', '当前用哪家店铺、哪种品类和哪份模板?字体是什么?不要改画布。', r => { assert.match(r.text, /顾家/); assert.match(r.text, /电脑桌/); assert.match(r.text, /GJ-DESK-DNZ0385/); assert.match(r.text, /Noto Sans SC/); });
|
||
await turn('03', '最初的米白色现在还有效吗?只回答当前颜色,不需要工具。', r => assert.match(r.text, /胡桃/));
|
||
await turn('04', '如果模板里有74厘米的固定标注,能否直接当作本款产品高度?简短回答。', r => assert.match(r.text, /不能|不可以|不可|760|76/));
|
||
await turn('05', '这轮只讨论信息,不生成图片、不修改画布,回复收到即可。');
|
||
await turn('06', '把已确认的产品编号、尺寸、颜色和三块策划范围重新核对一次,不增加参数。', r => { facts(r.text); assert.match(r.text, /首屏/); assert.match(r.text, /六图/); assert.match(r.text, /参数/); });
|
||
} else {
|
||
await turn('01', '再次重启后,检索原始「起始验收标记」,核对编号、尺寸、颜色、草稿标题和店铺品类。', (r, e) => { facts(r.text); assert.match(r.text, /溪石-7384/); assert.match(r.text, /留白有序/); assert.match(r.text, /顾家/); assert.match(r.text, /电脑桌/); assert.ok(e.some(x => x.toolName === 'memory_search' && x.type === 'tool_execution_end' && !x.isError)); }, { requiredTool: 'memory_search' });
|
||
await turn('02', '这次只汇报:是否操作过画布?结果未知的请求应如何处理?不要调用工具。', r => assert.match(r.text, /不.*重(发|试)|只读/));
|
||
}
|
||
let compaction;
|
||
if (phase !== 'C') {
|
||
const file = path.join(output, `${phase}-compaction-attempt.json`);
|
||
if (await fs.access(file).then(() => true, () => false)) throw Error('本阶段已经尝试压缩,禁止重复调用');
|
||
await atomic(file, { at: new Date().toISOString() });
|
||
compaction = await host.compact();
|
||
}
|
||
await atomic(path.join(output, `accepted-${phase}.json`), { phase, processId: process.pid, turns, compaction, sessionFile: host.manager.getSessionFile(), realUserTurns: turns.length, seededMessages: phase === 'A' ? 80 : 0, protocol: 'native-tools', at: new Date().toISOString() });
|
||
console.log(JSON.stringify({ phase, passed: true, realTurns: turns.length, compaction: Boolean(compaction) }));
|
||
} catch (error) {
|
||
await atomic(path.join(output, `failed-${phase}.json`), { phase, processId: process.pid, error: error.message, completedTurns: turns.length, sessionFile: host?.manager.getSessionFile(), inflight: host?.inflight, at: new Date().toISOString() });
|
||
console.error(error.message); process.exitCode = 1;
|
||
} finally { await host?.dispose(); }
|