feat: complete Aider Page Data review workflow

This commit is contained in:
john
2026-07-25 09:47:25 +08:00
parent be9c25f1d0
commit 104fb4e370
15 changed files with 914 additions and 30 deletions
+103 -2
View File
@@ -1,5 +1,7 @@
import { spawn as nodeSpawn } from 'node:child_process';
import { EventEmitter } from 'node:events';
import fs from 'node:fs/promises';
import path from 'node:path';
const CODE_EXECUTORS = new Set(['aider', 'openhands']);
const DEFAULT_STDIO_LIMIT = 64 * 1024;
@@ -55,6 +57,92 @@ export function extractToolInstruction(userMessage) {
.trim();
}
function runValidation(userMessage) {
const metadata = userMessage?.metadata ?? {};
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
const validation = runMetadata.validation ?? metadata.toolGatewayValidation;
return validation && typeof validation === 'object' && !Array.isArray(validation)
? validation
: null;
}
function validationFilePaths(userMessage) {
const validation = runValidation(userMessage);
if (!validation) return [];
const candidates = [
validation.expectedFile ?? validation.expectedPath,
...(Array.isArray(validation.expectedFiles) ? validation.expectedFiles : []),
];
return candidates
.map((item) => {
if (typeof item === 'string') return item.trim();
if (!item || typeof item !== 'object' || Array.isArray(item)) return '';
return String(item.path ?? item.file ?? item.relativePath ?? '').trim();
})
.filter(Boolean);
}
export function resolveAiderReceiptPath(userMessage, requestId) {
const expected = `.memind/agent-runs/${String(requestId ?? '').trim()}.json`;
return validationFilePaths(userMessage).find((item) => item === expected) ?? null;
}
export async function prepareAiderReceiptFile(cwd, relativePath) {
if (!cwd || !relativePath) return null;
const root = path.resolve(String(cwd));
const target = path.resolve(root, relativePath);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
throw new Error(`Aider receipt path escapes working directory: ${relativePath}`);
}
await fs.mkdir(path.dirname(target), { recursive: true });
try {
await fs.access(target);
} catch {
await fs.writeFile(
target,
`${JSON.stringify({ status: 'pending', executor: 'aider' }, null, 2)}\n`,
{ flag: 'wx' },
);
}
return target;
}
async function resolveAiderContextFiles(userMessage, cwd) {
const metadata = userMessage?.metadata ?? {};
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
const candidates = Array.isArray(runMetadata.aiderContextFiles)
? runMetadata.aiderContextFiles
: [];
if (!cwd || candidates.length === 0) return [];
const root = path.resolve(String(cwd));
const resolved = [];
for (const relativePath of candidates.slice(0, 40)) {
const target = path.resolve(root, String(relativePath ?? ''));
if (target === root || !target.startsWith(`${root}${path.sep}`)) continue;
try {
const stat = await fs.stat(target);
if (stat.isFile()) resolved.push(target);
} catch {
// Review context is best-effort; delivery validation remains authoritative.
}
}
return resolved;
}
export function hardenAiderLaunchPlan(plan, receiptPath = null, contextFiles = []) {
const args = [...(plan?.args ?? [])];
for (const flag of ['--no-git', '--no-auto-commits', '--no-dirty-commits']) {
if (!args.includes(flag)) args.push(flag);
}
for (const contextFile of contextFiles) {
if (!args.includes(contextFile)) args.push('--file', contextFile);
}
if (receiptPath && !args.includes(receiptPath)) {
args.push('--file', receiptPath);
}
return { ...plan, args };
}
export function createToolGateway({
llmProviderService,
env = process.env,
@@ -110,16 +198,29 @@ export function createToolGateway({
throw new Error('Tool Gateway job missing instruction');
}
const executor = selectExecutor({ userMessage, taskType });
const plan = await llmProviderService.getExecutorLaunchPlan(executor, {
const receiptPath = executor === 'aider'
? resolveAiderReceiptPath(userMessage, requestId)
: null;
const executorInstruction = receiptPath
? `${instruction}\n\nThe validation receipt is already included in the Aider chat. Edit it directly; do not ask the user to add it.`
: instruction;
let plan = await llmProviderService.getExecutorLaunchPlan(executor, {
cwd,
mode: 'headless',
instruction,
instruction: executorInstruction,
purpose: 'default',
includeSecret: true,
});
if (!plan?.ok) {
throw new Error(plan?.message ?? `Tool Gateway launch plan unavailable for ${executor}`);
}
if (executor === 'aider') {
const preparedReceipt = dryRun
? (receiptPath ? path.resolve(String(cwd), receiptPath) : null)
: await prepareAiderReceiptFile(cwd, receiptPath);
const contextFiles = await resolveAiderContextFiles(userMessage, cwd);
plan = hardenAiderLaunchPlan(plan, preparedReceipt, contextFiles);
}
if (dryRun) {
return {
ok: true,