gongxiangp2/apps/detail-agent-pi/scripts/long-context-live.mjs
WuYongJie 1b6425b552 feat(desktop): connect native Pi sessions with scoped rules and recovery
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.
2026-10-22 10:49:58 +08:00

95 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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