Files
memind/scripts/page-data-aider-dev-loop.mjs
john dab6140f99 feat: Page Data dev loop, adm Code Run UI, and customer order demo
Add verify→Aider→OpenHands dev loop, memindadm config/history UI, and john4
customer-order Page Data demo with scenario + verification scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 22:11:18 +08:00

459 lines
15 KiB
JavaScript
Raw Permalink 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.
#!/usr/bin/env node
/**
* Page Data 开发闭环:verify 失败 → Aider 修 workspace → 重跑 verifyPhase 2)。
*
* Usage:
* node scripts/page-data-aider-dev-loop.mjs
* node scripts/page-data-aider-dev-loop.mjs --preset children-hobby-diet-survey
* node scripts/page-data-aider-dev-loop.mjs --preset page-data-platform --max-attempts 2
* node scripts/page-data-aider-dev-loop.mjs --dry-run
* node scripts/page-data-aider-dev-loop.mjs --verify-cmd "node scripts/verify-children-hobby-diet-survey.mjs --no-insert"
*/
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadH5Environment } from './load-env.mjs';
import { createDbPool } from '../db.mjs';
import { createLlmProviderService } from '../llm-providers.mjs';
import { createToolGateway } from '../tool-gateway.mjs';
import { PUBLISH_ROOT_DIR } from '../user-publish.mjs';
import { CHILDREN_HOBBY_DIET_SURVEY } from './scenario-test-lib.mjs';
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const PAGE_DATA_DEV_TASK_TYPE = 'page_data_dev';
export const PAGE_DATA_DEV_COMPLEX_TASK_TYPE = 'page_data_dev_complex';
export const VERIFY_PRESETS = Object.freeze({
'children-hobby-diet-survey': {
label: 'john4 儿童爱好问卷饮食偏好',
scenario: 'workspace',
defaultUserId: CHILDREN_HOBBY_DIET_SURVEY.userId,
buildVerifyArgs: (options) => [
'scripts/verify-children-hobby-diet-survey.mjs',
'--user-id',
options.userId,
...(options.testInsert === false ? ['--no-insert'] : []),
...(options.checkRuns === false ? ['--no-runs'] : []),
...(options.port ? ['--port', String(options.port)] : []),
],
defaultTargetFiles: () => [
`public/${CHILDREN_HOBBY_DIET_SURVEY.surveyHtml}`,
`public/${CHILDREN_HOBBY_DIET_SURVEY.adminHtml}`,
],
verifyCommandLabel: 'npm run verify:children-hobby-diet-survey',
},
'page-data-platform': {
label: 'Memind Page Data 平台单测',
scenario: 'platform',
defaultUserId: null,
buildVerifyArgs: () => ['--test', 'page-data-public-service.test.mjs', 'page-data-integration.test.mjs'],
defaultTargetFiles: () => [
'page-data-public-service.mjs',
'page-data-service.mjs',
'public/assets/page-data-client.js',
],
verifyCommandLabel: 'npm run verify:page-data',
},
});
function printHelp() {
console.log([
'Usage:',
' node scripts/page-data-aider-dev-loop.mjs [options]',
'',
'Options:',
' --preset <name> children-hobby-diet-survey (default) | page-data-platform',
' --verify-cmd <shell> 自定义 verify 命令(覆盖 preset',
' --user-id <uuid> workspace 用户 IDworkspace 场景)',
' --cwd <dir> 覆盖 Aider 工作目录',
' --target-file <path> 可多次指定;未指定时用 preset 默认',
' --max-attempts <n> 最大修复轮次(默认 3)',
' --escalate-after <n> Aider 连续失败 N 轮后升级 OpenHands(默认 20=禁用)',
' --executor <aider|openhands> 强制全程使用指定执行器',
' --no-insert 传给 preset verify',
' --no-runs 传给 preset verify(跳过 orphan run 检查)',
' --port <n> Portal 端口(默认 H5_PORT 或 8081',
' --dry-run 只打印 instruction,不执行 Aider',
' --skip-initial-verify 跳过首轮 verify,直接跑 Aider(调试用)',
' -h, --help',
'',
'Requires: DATABASE_URLAider launch plan)、MEMIND_TOOL_GATEWAY_ENABLED=1(或 --dry-run',
].join('\n'));
}
export function parseDevLoopArgs(argv, env = process.env) {
const options = {
preset: 'children-hobby-diet-survey',
verifyCmd: null,
userId: null,
cwd: null,
targetFiles: [],
maxAttempts: 3,
escalateAfter: 2,
executor: 'aider',
testInsert: true,
checkRuns: true,
port: Number(env.H5_PORT ?? 8081),
dryRun: false,
skipInitialVerify: false,
help: false,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '-h' || arg === '--help') options.help = true;
else if (arg === '--preset' && argv[i + 1]) options.preset = argv[++i];
else if (arg === '--verify-cmd' && argv[i + 1]) options.verifyCmd = argv[++i];
else if (arg === '--user-id' && argv[i + 1]) options.userId = argv[++i];
else if (arg === '--cwd' && argv[i + 1]) options.cwd = path.resolve(argv[++i]);
else if (arg === '--target-file' && argv[i + 1]) options.targetFiles.push(argv[++i]);
else if (arg === '--max-attempts' && argv[i + 1]) options.maxAttempts = Math.max(1, Number(argv[++i]) || 3);
else if (arg === '--escalate-after' && argv[i + 1]) {
options.escalateAfter = Math.max(0, Number(argv[++i]) || 0);
}
else if (arg === '--executor' && argv[i + 1]) options.executor = String(argv[++i]).trim().toLowerCase();
else if (arg === '--port' && argv[i + 1]) options.port = Number(argv[++i]);
else if (arg === '--no-insert') options.testInsert = false;
else if (arg === '--no-runs') options.checkRuns = false;
else if (arg === '--dry-run') options.dryRun = true;
else if (arg === '--skip-initial-verify') options.skipInitialVerify = true;
else throw new Error(`未知参数: ${arg}`);
}
return options;
}
export function resolveVerifyPreset(presetName) {
const preset = VERIFY_PRESETS[presetName];
if (!preset) {
throw new Error(`未知 preset: ${presetName}(可选: ${Object.keys(VERIFY_PRESETS).join(', ')}`);
}
return preset;
}
export function resolveWorkspaceCwd({ repoRoot: root, userId, cwdOverride, scenario }) {
if (cwdOverride) return cwdOverride;
if (scenario === 'platform') return root;
if (!userId) {
throw new Error('workspace 场景需要 --user-id 或 preset 默认 userId');
}
return path.join(root, PUBLISH_ROOT_DIR, userId);
}
export function resolveDevLoopExecutor(
attempt,
{ executor = 'aider', escalateAfter = 2 } = {},
) {
if (executor !== 'aider') {
return {
executor,
taskType: executor === 'openhands' ? PAGE_DATA_DEV_COMPLEX_TASK_TYPE : PAGE_DATA_DEV_TASK_TYPE,
escalated: false,
};
}
if (escalateAfter > 0 && attempt > escalateAfter) {
return {
executor: 'openhands',
taskType: PAGE_DATA_DEV_COMPLEX_TASK_TYPE,
escalated: true,
};
}
return {
executor: 'aider',
taskType: PAGE_DATA_DEV_TASK_TYPE,
escalated: false,
};
}
export function buildPageDataDevInstruction({
verifyOutput,
cwd,
taskType = PAGE_DATA_DEV_TASK_TYPE,
targetFiles = [],
verifyCommandLabel = 'verify script',
escalated = false,
}) {
const targets = targetFiles.length
? targetFiles.map((item) => `- ${item}`).join('\n')
: '- public/*.html\n- .mindspace/page-data-policies/*.json';
return [
'[Page Data 开发修复任务]',
escalated ? '[Escalation] 前序 Aider 修复未通过 verify,现由 OpenHands 接手。' : '',
'',
`工作目录: ${cwd}`,
`taskType: ${taskType}`,
'',
'验证失败输出(必须原样附上):',
String(verifyOutput ?? '').trim() || '(verify 未返回输出)',
'',
'目标文件(仅可改这些):',
targets,
'',
'约束:',
'- 必须使用 /assets/page-data-client.js 公开 API',
'- 禁止 localStorage / sessionStorage / IndexedDB',
'- 禁止自建 Express 或独立后端',
'- 列名必须与 dataset register 的 insert/read 白名单一致',
'- 不要修改 Memind 平台源码(workspace 场景)',
'- 不要调用 private_data_execute 或重建 dataset(除非 verify 明确报 table_not_found',
'',
'验收:',
`- 修改后应能通过: ${verifyCommandLabel}`,
'- 完成后简要说明改了哪些文件、为何能修复 verify 失败项',
].join('\n');
}
function runVerifyCommand({ command, args, cwd, env }) {
const result = spawnSync(command, args, {
cwd,
env,
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
});
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
return {
ok: result.status === 0,
status: result.status,
output: output || `(exit ${result.status ?? 'unknown'}, no output)`,
commandLine: `${command} ${args.join(' ')}`.trim(),
};
}
function resolveVerifyRun(options, preset) {
if (options.verifyCmd) {
return {
label: options.verifyCmd,
...runVerifyCommand({
command: '/bin/sh',
args: ['-lc', options.verifyCmd],
cwd: repoRoot,
env: process.env,
}),
};
}
const userId = options.userId ?? preset.defaultUserId;
if (preset.scenario === 'workspace') {
options.userId = userId;
}
const verifyArgs = preset.buildVerifyArgs({
userId,
testInsert: options.testInsert,
checkRuns: options.checkRuns,
port: options.port,
});
const useNpmForPlatform = preset.scenario === 'platform'
&& verifyArgs[0] === '--test';
if (useNpmForPlatform) {
return {
label: preset.verifyCommandLabel,
...runVerifyCommand({
command: 'npm',
args: ['run', 'verify:page-data'],
cwd: repoRoot,
env: process.env,
}),
};
}
return {
label: preset.verifyCommandLabel,
...runVerifyCommand({
command: process.execPath,
args: verifyArgs,
cwd: repoRoot,
env: process.env,
}),
};
}
async function runCodeExecutorFix({
instruction,
cwd,
executor,
taskType,
dryRun,
llmProviderService,
}) {
const previousGatewayFlag = process.env.MEMIND_TOOL_GATEWAY_ENABLED;
if (!dryRun) {
process.env.MEMIND_TOOL_GATEWAY_ENABLED = '1';
}
const gateway = createToolGateway({ llmProviderService, env: process.env });
const status = gateway.getStatus();
if (!status.enabled && !dryRun) {
throw new Error('MEMIND_TOOL_GATEWAY_ENABLED 未开启;请设 1 或使用 --dry-run');
}
const userMessage = {
role: 'user',
content: [{ type: 'text', text: instruction }],
metadata: {
memindRun: {
executor,
taskType,
},
},
};
try {
return await gateway.executeJob({
runId: `dev-loop-${Date.now()}`,
requestId: `dev-loop-${Date.now()}`,
userId: 'dev-loop',
userMessage,
taskType,
cwd,
timeoutMs: Number(process.env.MEMIND_PAGE_DATA_DEV_LOOP_TIMEOUT_MS ?? 20 * 60 * 1000),
});
} finally {
if (previousGatewayFlag == null) {
delete process.env.MEMIND_TOOL_GATEWAY_ENABLED;
} else {
process.env.MEMIND_TOOL_GATEWAY_ENABLED = previousGatewayFlag;
}
}
}
async function main() {
loadH5Environment(import.meta.dirname);
const options = parseDevLoopArgs(process.argv.slice(2));
if (options.help) {
printHelp();
process.exit(0);
}
const preset = resolveVerifyPreset(options.preset);
const userId = options.userId ?? preset.defaultUserId;
const workspaceCwd = resolveWorkspaceCwd({
repoRoot,
userId,
cwdOverride: options.cwd,
scenario: preset.scenario,
});
if (preset.scenario === 'workspace' && !fs.existsSync(workspaceCwd)) {
throw new Error(`workspace 不存在: ${workspaceCwd}`);
}
const targetFiles = options.targetFiles.length
? options.targetFiles
: preset.defaultTargetFiles();
console.log('==> Page Data Aider dev loop');
console.log(` preset: ${options.preset} (${preset.label})`);
console.log(` cwd: ${workspaceCwd}`);
console.log(` max attempts: ${options.maxAttempts}`);
console.log(` escalate after: ${options.escalateAfter} aider round(s)`);
console.log(` dry-run: ${options.dryRun ? 'yes' : 'no'}\n`);
let pool = null;
let llmProviderService = null;
if (!options.dryRun) {
pool = createDbPool();
llmProviderService = createLlmProviderService(pool, {
apiTarget: process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006',
apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret',
});
}
const shutdown = async () => {
if (pool) await pool.end().catch(() => {});
};
try {
let verifyResult = options.skipInitialVerify
? { ok: false, output: '(跳过首轮 verify)', label: options.verifyCmd ?? preset.verifyCommandLabel }
: null;
if (!options.skipInitialVerify) {
console.log('--- 首轮 verify ---');
verifyResult = resolveVerifyRun(options, preset);
console.log(`$ ${verifyResult.commandLine ?? verifyResult.label}`);
if (verifyResult.ok) {
console.log('\n✔ verify 已通过,无需 Aider 修复');
await shutdown();
process.exit(0);
}
console.log(verifyResult.output.slice(-8000));
}
for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
const execution = resolveDevLoopExecutor(attempt, {
executor: options.executor,
escalateAfter: options.escalateAfter,
});
const instruction = buildPageDataDevInstruction({
verifyOutput: verifyResult.output,
cwd: workspaceCwd,
targetFiles,
verifyCommandLabel: verifyResult.label,
taskType: execution.taskType,
escalated: execution.escalated,
});
console.log(`\n--- 修复第 ${attempt}/${options.maxAttempts} 轮 (${execution.executor}) ---`);
console.log(instruction.slice(0, 2000));
if (instruction.length > 2000) {
console.log(`... (${instruction.length - 2000} more chars)`);
}
if (options.dryRun) {
console.log('\n(dry-run: 未执行 Aider)');
await shutdown();
process.exit(0);
}
console.log(`\n--- 执行 ${execution.executor} ---`);
const result = await runCodeExecutorFix({
instruction,
cwd: workspaceCwd,
executor: execution.executor,
taskType: execution.taskType,
dryRun: false,
llmProviderService,
});
console.log(`${execution.executor} 完成: exit=${result.exitCode ?? 0} taskType=${execution.taskType}`);
if (result.stderr) {
console.log(result.stderr.slice(-2000));
}
console.log('\n--- 重跑 verify ---');
verifyResult = resolveVerifyRun(options, preset);
console.log(`$ ${verifyResult.commandLine ?? verifyResult.label}`);
if (verifyResult.ok) {
console.log(`\n✔ 第 ${attempt} 轮修复后 verify 通过`);
await shutdown();
process.exit(0);
}
console.log(verifyResult.output.slice(-4000));
}
console.error(`\n${options.maxAttempts} 轮修复后 verify 仍未通过`);
await shutdown();
process.exit(1);
} catch (error) {
await shutdown();
throw error;
}
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
main().catch((error) => {
console.error(error instanceof Error ? error.stack ?? error.message : error);
process.exit(1);
});
}
export const pageDataAiderDevLoopInternals = {
PAGE_DATA_DEV_TASK_TYPE,
PAGE_DATA_DEV_COMPLEX_TASK_TYPE,
VERIFY_PRESETS,
};