feat: complete Aider Page Data review workflow
This commit is contained in:
+205
-7
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user