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
+205 -7
View File
@@ -230,6 +230,15 @@ function summarizeText(value, limit = TOOL_GATEWAY_SUMMARY_LIMIT) {
return text.slice(text.length - limit);
}
export function buildCodeRunCompletionReply(result) {
const executor = String(result?.executor ?? 'code executor').trim() || 'code executor';
const output = summarizeText(result?.stdout, 2400).trim();
return [
`已由 ${executor} 完成执行,并通过平台文件验收。`,
output ? `\n${output}` : '',
].join('').trim();
}
function normalizeExpectedFileCheck(value) {
if (typeof value === 'string') {
const expectedPath = value.trim();
@@ -379,6 +388,12 @@ function getRunOptionsFromMessage(userMessage) {
toolMode,
taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType),
requiredExecutor: resolveRequiredCodeExecutor(userMessage),
reviewExecutor: ['aider', 'openhands'].includes(
String(runMetadata?.reviewExecutor ?? '').trim().toLowerCase(),
)
? String(runMetadata.reviewExecutor).trim().toLowerCase()
: null,
pageDataAiderWorkflow: runMetadata?.pageDataAiderWorkflow === true,
forceDeepReasoning: runMetadata?.forceDeepReasoning === true || metadata?.forceDeepReasoning === true,
validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation),
sessionMessageCount: normalizeSessionMessageCount(
@@ -387,6 +402,48 @@ function getRunOptionsFromMessage(userMessage) {
};
}
export async function collectAiderReviewFiles(cwd, sinceMs = 0) {
if (!cwd) return [];
const root = path.resolve(String(cwd));
const candidates = [
{ relativeDir: 'public', extensions: new Set(['.html', '.css', '.js']) },
{ relativeDir: '.mindspace/page-data-policies', extensions: new Set(['.json']) },
];
const files = [];
async function walk(baseDir, relativeDir, extensions) {
let entries = [];
try {
entries = await fs.readdir(baseDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (files.length >= 40) return;
const absolute = path.join(baseDir, entry.name);
const relative = path.posix.join(relativeDir.split(path.sep).join('/'), entry.name);
if (entry.isDirectory()) {
await walk(absolute, relative, extensions);
continue;
}
if (!entry.isFile() || !extensions.has(path.extname(entry.name).toLowerCase())) continue;
try {
const stat = await fs.stat(absolute);
if (Number(stat.mtimeMs) + 5_000 >= Number(sinceMs || 0)) files.push(relative);
} catch {
// File disappeared between directory listing and stat.
}
}
}
for (const candidate of candidates) {
await walk(
path.join(root, candidate.relativeDir),
candidate.relativeDir,
candidate.extensions,
);
}
return files;
}
function projectRun(row) {
if (!row) return null;
return {
@@ -858,6 +915,89 @@ export function createAgentRunGateway({
}
}
async function runRequiredCodeReview({
row,
runId,
userMessage,
runOptions,
sessionId,
}) {
if (!runOptions.reviewExecutor) return;
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
assertRequiredCodeExecutorAvailable(runOptions.reviewExecutor, toolGatewayStatus);
const workingDir = userAuth?.resolveWorkingDir
? await userAuth.resolveWorkingDir(row.user_id)
: undefined;
const claimedRun = await getRunById(runId);
const contextFiles = await collectAiderReviewFiles(
workingDir,
claimedRun?.started_at ?? row.started_at ?? 0,
);
const reviewMessage = {
...userMessage,
content: [{
type: 'text',
text: [
'[Mandatory Aider Page Data review]',
`Original user request: ${extractRunDisplayText(row)}`,
`Session: ${sessionId}`,
'Review the provided workspace files, fix concrete HTML/client-policy defects if needed,',
'do not recreate PostgreSQL tables or datasets, and update the required validation receipt.',
'Do not commit, push, publish, or modify files outside the current user workspace.',
contextFiles.length
? `Review files:\n${contextFiles.map((item) => `- ${item}`).join('\n')}`
: 'No recent page files were detected; record that fact in the receipt so platform delivery validation can fail closed.',
`Receipt requestId: ${row.request_id}`,
].join('\n'),
}],
metadata: {
...(userMessage?.metadata ?? {}),
memindRun: {
...(userMessage?.metadata?.memindRun ?? {}),
executor: runOptions.reviewExecutor,
aiderContextFiles: contextFiles,
},
},
};
await appendEvent(runId, 'required_code_review_dispatch', {
executor: runOptions.reviewExecutor,
contextFiles,
});
const result = await toolGateway.executeJob({
runId,
userId: row.user_id,
requestId: row.request_id,
userMessage: reviewMessage,
taskType: 'page_data_dev',
cwd: workingDir,
timeoutMs: runTimeoutMs,
});
if (
String(result?.executor ?? '').trim().toLowerCase() !== runOptions.reviewExecutor
) {
const error = new Error(
`审查执行器不匹配:要求 ${runOptions.reviewExecutor},实际 ${result?.executor ?? 'unknown'}`,
);
error.code = 'REQUIRED_REVIEW_EXECUTOR_MISMATCH';
error.retryable = false;
throw error;
}
await appendEvent(runId, 'required_code_review_result', {
executor: result.executor ?? null,
exitCode: result.exitCode ?? null,
stdoutTail: summarizeText(result.stdout),
stderrTail: summarizeText(result.stderr),
});
const validation = await validateToolGatewayResult({
result,
validation: runOptions.validation,
cwd: workingDir,
});
if (validation) {
await appendEvent(runId, 'required_code_review_validation', validation);
}
}
async function resolveRunRouting(row, userMessage, runOptions) {
if (!chatIntentRouter?.classify) return null;
const enabled = chatIntentRouter.isEnabled
@@ -880,7 +1020,13 @@ export function createAgentRunGateway({
let userMessage = safeJsonParse(row.user_message_json, {});
const runOptions = getRunOptionsFromMessage(userMessage);
const toolGatewayStatus = toolGateway?.getStatus ? toolGateway.getStatus() : null;
assertRequiredCodeExecutorAvailable(runOptions.requiredExecutor, toolGatewayStatus);
assertRequiredCodeExecutorAvailable(
runOptions.requiredExecutor ?? runOptions.reviewExecutor,
toolGatewayStatus,
);
const effectiveToolMode = runOptions.pageDataAiderWorkflow
? 'chat'
: runOptions.toolMode;
let disclosureDecision = null;
try {
disclosureDecision = systemDisclosurePolicyService?.evaluate?.({
@@ -1040,7 +1186,11 @@ export function createAgentRunGateway({
}
}
}
if (runOptions.toolMode === 'code' && toolGatewayStatus?.enabled) {
if (
runOptions.toolMode === 'code' &&
!runOptions.pageDataAiderWorkflow &&
toolGatewayStatus?.enabled
) {
const workingDir = userAuth?.resolveWorkingDir
? await userAuth.resolveWorkingDir(row.user_id)
: undefined;
@@ -1094,6 +1244,38 @@ export function createAgentRunGateway({
});
throw err;
}
if (runOptions.requiredExecutor) {
if (!directChatService?.respondDeterministically) {
const error = new Error('代码任务已执行,但结果回传服务不可用');
error.code = 'CODE_RUN_RESULT_DELIVERY_UNAVAILABLE';
error.retryable = false;
throw error;
}
const delivery = await directChatService.respondDeterministically({
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
requestId: row.request_id,
userMessage,
reply: buildCodeRunCompletionReply(result),
metadata: {
source: 'tool-gateway-code-run',
executor: result.executor ?? null,
validated: true,
},
onSessionReady: async (activeSessionId) => {
await pool.query(
`UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
[activeSessionId, nowMs(), runId],
);
await appendRunSnapshot(runId);
},
});
await appendEvent(runId, 'tool_gateway_result_delivered', {
sessionId: delivery.sessionId,
executor: result.executor ?? null,
});
return { sessionId: delivery.sessionId, routing };
}
return { sessionId: row.agent_session_id ?? null, routing };
}
@@ -1112,7 +1294,7 @@ export function createAgentRunGateway({
}
if (!sessionId) {
const sessionOptions = {};
if (runOptions.toolMode === 'code' && userAuth?.getCodeAgentSessionPolicy) {
if (effectiveToolMode === 'code' && userAuth?.getCodeAgentSessionPolicy) {
sessionOptions.sessionPolicy = await userAuth.getCodeAgentSessionPolicy(row.user_id);
}
const session = await tkmindProxy.startSessionForUser(row.user_id, sessionOptions);
@@ -1123,7 +1305,7 @@ export function createAgentRunGateway({
);
await appendEvent(runId, 'session_started', {
sessionId,
toolMode: runOptions.toolMode,
toolMode: effectiveToolMode,
taskType: runOptions.taskType,
});
await appendRunSnapshot(runId);
@@ -1165,8 +1347,17 @@ export function createAgentRunGateway({
await invalidatePortalDirectChatSnapshot(sessionId);
let toolEvidence = null;
const awaitSessionFinish = envFlag(process.env.MEMIND_AGENT_RUN_AWAIT_SESSION_FINISH, true)
&& runOptions.toolMode === 'chat'
&& effectiveToolMode === 'chat'
&& typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser === 'function';
if (
runOptions.pageDataAiderWorkflow &&
typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser !== 'function'
) {
const error = new Error('Page Data + Aider 工作流需要等待 Page Data Agent 完成');
error.code = 'PAGE_DATA_AIDER_FINISH_UNAVAILABLE';
error.retryable = false;
throw error;
}
if (awaitSessionFinish) {
let submitMessage = ensureGooseUserMessageMetadata(userMessage);
let replacedPoisonedSession = false;
@@ -1178,7 +1369,7 @@ export function createAgentRunGateway({
row.request_id,
submitMessage,
{
toolMode: runOptions.toolMode,
toolMode: effectiveToolMode,
forceDeepReasoning: runOptions.forceDeepReasoning,
timeoutMs: runTimeoutMs,
},
@@ -1244,11 +1435,18 @@ export function createAgentRunGateway({
row.request_id,
ensureGooseUserMessageMetadata(userMessage),
{
toolMode: runOptions.toolMode,
toolMode: effectiveToolMode,
forceDeepReasoning: runOptions.forceDeepReasoning,
},
);
}
await runRequiredCodeReview({
row,
runId,
userMessage,
runOptions,
sessionId,
});
return { sessionId, routing, toolEvidence };
}