Files
memind/agent-code-run-admin-config.mjs
T
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

367 lines
12 KiB
JavaScript

const CONFIG_TABLE = 'h5_agent_code_run_config';
const CONFIG_SCOPE = 'global';
const POLICY_SOURCE_ENV = 'env';
const POLICY_SOURCE_ENV_OVERRIDE = 'env-override';
const POLICY_SOURCE_ENV_MIGRATION = 'env-migration';
const POLICY_SOURCE_ADMIN_DB = 'admin-db';
const POLICY_SOURCE_DEFAULT = 'default';
const DEFAULT_TASK_TYPES = Object.freeze([
'page_data_dev',
'page_data_dev_complex',
'h5_chat_code_task',
'page_edit_code_task',
]);
function defaultConfigShape() {
return {
codeRun: {
enabled: false,
clientEnabled: false,
generalAutodetect: false,
requireValidation: true,
userAllowlist: [],
taskTypeAllowlist: [...DEFAULT_TASK_TYPES],
},
pageDataDev: {
autodetect: false,
},
meta: {
notes: '',
},
};
}
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
function normalizeStringList(value) {
if (!Array.isArray(value)) {
return String(value ?? '')
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
return value.map((item) => String(item ?? '').trim()).filter(Boolean);
}
function cloneConfig(config = null) {
return structuredClone?.(config ?? defaultConfigShape())
?? JSON.parse(JSON.stringify(config ?? defaultConfigShape()));
}
function parseJsonLike(value, fallback) {
if (value == null || value === '') return fallback;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
if (typeof value === 'object') return value;
return fallback;
}
function policySourceFromEnv(env = process.env) {
return String(env.MEMIND_CODE_RUN_POLICY_SOURCE ?? '').trim().toLowerCase() === 'env'
? POLICY_SOURCE_ENV_OVERRIDE
: null;
}
function buildConfigFromEnv(env = process.env) {
const config = defaultConfigShape();
config.codeRun.enabled = envFlag(env.MEMIND_AGENT_CODE_RUNS_ENABLED);
config.codeRun.clientEnabled = envFlag(env.VITE_AGENT_CODE_RUNS_ENABLED, config.codeRun.enabled);
config.codeRun.generalAutodetect = envFlag(env.VITE_AGENT_CODE_RUNS_AUTODETECT);
config.codeRun.requireValidation = envFlag(env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION, true);
config.codeRun.userAllowlist = normalizeStringList(env.MEMIND_AGENT_CODE_RUNS_USER_IDS);
const taskTypes = normalizeStringList(env.MEMIND_AGENT_CODE_RUN_TASK_TYPES);
config.codeRun.taskTypeAllowlist = taskTypes.length ? taskTypes : [...DEFAULT_TASK_TYPES];
config.pageDataDev.autodetect = envFlag(env.VITE_AGENT_PAGE_DATA_DEV_AUTODETECT);
return config;
}
function envPolicyActive(config) {
return Boolean(config?.codeRun?.enabled)
|| Boolean(config?.codeRun?.clientEnabled)
|| Boolean(config?.pageDataDev?.autodetect)
|| (config?.codeRun?.userAllowlist?.length ?? 0) > 0;
}
function mergePatch(currentConfig, patch = {}) {
const next = cloneConfig(currentConfig);
const codeRunPatch = patch?.codeRun ?? {};
const pageDataDevPatch = patch?.pageDataDev ?? {};
if ('enabled' in codeRunPatch) next.codeRun.enabled = normalizeBoolean(codeRunPatch.enabled, false);
if ('clientEnabled' in codeRunPatch) {
next.codeRun.clientEnabled = normalizeBoolean(codeRunPatch.clientEnabled, false);
}
if ('generalAutodetect' in codeRunPatch) {
next.codeRun.generalAutodetect = normalizeBoolean(codeRunPatch.generalAutodetect, false);
}
if ('requireValidation' in codeRunPatch) {
next.codeRun.requireValidation = normalizeBoolean(codeRunPatch.requireValidation, true);
}
if ('userAllowlist' in codeRunPatch) {
next.codeRun.userAllowlist = normalizeStringList(codeRunPatch.userAllowlist);
}
if ('taskTypeAllowlist' in codeRunPatch) {
const taskTypes = normalizeStringList(codeRunPatch.taskTypeAllowlist);
next.codeRun.taskTypeAllowlist = taskTypes.length ? taskTypes : [...DEFAULT_TASK_TYPES];
}
if ('autodetect' in pageDataDevPatch) {
next.pageDataDev.autodetect = normalizeBoolean(pageDataDevPatch.autodetect, false);
}
if (patch?.meta && typeof patch.meta.notes === 'string') {
next.meta.notes = patch.meta.notes;
}
return next;
}
function flattenPolicy(config, source) {
const taskTypeAllowlist = normalizeStringList(config.codeRun.taskTypeAllowlist);
return {
source,
enabled: Boolean(config.codeRun.enabled),
clientEnabled: Boolean(config.codeRun.clientEnabled),
generalAutodetect: Boolean(config.codeRun.generalAutodetect),
requireValidation: Boolean(config.codeRun.requireValidation),
pageDataDevAutodetect: Boolean(config.pageDataDev.autodetect),
userAllowlist: normalizeStringList(config.codeRun.userAllowlist),
taskTypeAllowlist: taskTypeAllowlist.length ? taskTypeAllowlist : [...DEFAULT_TASK_TYPES],
};
}
export function isUserAllowedByCodeRunPolicy(userId, policy) {
if (!policy?.enabled) return false;
const allowlist = policy.userAllowlist ?? [];
if (allowlist.length === 0) return true;
return allowlist.includes(String(userId ?? '').trim());
}
export function isTaskTypeAllowedByCodeRunPolicy(taskType, policy) {
const allowlist = (policy?.taskTypeAllowlist ?? []).map((item) => String(item).toLowerCase());
if (allowlist.length === 0) return true;
const normalized = String(taskType ?? '').trim().toLowerCase();
if (!normalized) return false;
return allowlist.includes(normalized);
}
async function ensureConfigTable(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
config_scope VARCHAR(32) PRIMARY KEY,
config_json JSON NOT NULL,
updated_by CHAR(36) NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
async function loadStoredState(pool) {
await ensureConfigTable(pool);
const [rows] = await pool.query(
`SELECT config_json, updated_by, updated_at
FROM ${CONFIG_TABLE}
WHERE config_scope = ?
LIMIT 1`,
[CONFIG_SCOPE],
);
const row = rows[0];
if (!row) return null;
const parsed = parseJsonLike(row.config_json, {});
return {
config: mergePatch(defaultConfigShape(), parsed),
updatedAt: Number(row.updated_at ?? 0) || null,
updatedBy: row.updated_by ?? null,
};
}
export function createAgentCodeRunAdminConfigService(pool, { env = process.env } = {}) {
async function loadEffectiveConfig() {
const forcedEnvSource = policySourceFromEnv(env);
if (forcedEnvSource) {
const config = buildConfigFromEnv(env);
return {
config,
updatedAt: null,
updatedBy: null,
source: forcedEnvSource,
};
}
const stored = await loadStoredState(pool);
if (stored) {
return {
config: cloneConfig(stored.config),
updatedAt: stored.updatedAt,
updatedBy: stored.updatedBy,
source: POLICY_SOURCE_ADMIN_DB,
};
}
const envConfig = buildConfigFromEnv(env);
if (envPolicyActive(envConfig)) {
return {
config: envConfig,
updatedAt: null,
updatedBy: null,
source: POLICY_SOURCE_ENV_MIGRATION,
};
}
return {
config: defaultConfigShape(),
updatedAt: null,
updatedBy: null,
source: POLICY_SOURCE_DEFAULT,
};
}
return {
async getAdminConfig() {
const state = await loadEffectiveConfig();
return {
config: state.config,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
source: state.source,
envOverrideActive: state.source === POLICY_SOURCE_ENV_OVERRIDE,
};
},
async updateAdminConfig(patch = {}, { updatedBy = null } = {}) {
if (policySourceFromEnv(env)) {
throw Object.assign(new Error('MEMIND_CODE_RUN_POLICY_SOURCE=env 时不允许通过后台修改'), {
code: 'CODE_RUN_POLICY_ENV_LOCKED',
});
}
const stored = await loadStoredState(pool);
const base = stored?.config ?? defaultConfigShape();
const nextConfig = mergePatch(base, patch.config ?? patch);
await ensureConfigTable(pool);
const now = Date.now();
await pool.query(
`INSERT INTO ${CONFIG_TABLE}
(config_scope, config_json, updated_by, updated_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[CONFIG_SCOPE, JSON.stringify(nextConfig), updatedBy, now],
);
return this.getAdminConfig();
},
async getRuntimeState() {
const state = await loadEffectiveConfig();
const policy = flattenPolicy(state.config, state.source);
return {
source: state.source,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
config: state.config,
policy,
envOverrideActive: state.source === POLICY_SOURCE_ENV_OVERRIDE,
};
},
async getEffectivePolicy(userId) {
const state = await loadEffectiveConfig();
const policy = flattenPolicy(state.config, state.source);
return {
...policy,
userAllowed: isUserAllowedByCodeRunPolicy(userId, policy),
};
},
async getPublicClientPolicy(userId) {
const policy = await this.getEffectivePolicy(userId);
const enabled = policy.enabled && policy.clientEnabled && policy.userAllowed;
return {
codeRun: {
enabled,
pageDataDevAutodetect: enabled && policy.pageDataDevAutodetect,
generalAutodetect: enabled && policy.generalAutodetect,
},
};
},
async getRuntimeStatusSummary() {
const state = await loadEffectiveConfig();
const policy = flattenPolicy(state.config, state.source);
return {
source: policy.source,
enabled: policy.enabled,
clientEnabled: policy.clientEnabled,
userAllowlist: policy.userAllowlist,
taskTypeAllowlist: policy.taskTypeAllowlist,
requireValidation: policy.requireValidation,
pageDataDevAutodetect: policy.pageDataDevAutodetect,
generalAutodetect: policy.generalAutodetect,
envOverrideActive: policy.source === POLICY_SOURCE_ENV_OVERRIDE,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
};
},
async listRecentPageDataDevRuns({ limit = 50 } = {}) {
const pageDataTaskTypes = new Set(['page_data_dev', 'page_data_dev_complex']);
const capped = Math.min(Math.max(Number(limit) || 50, 1), 200);
const [rows] = await pool.query(
`SELECT id, user_id, request_id, status, error_message, created_at, updated_at, user_message_json
FROM h5_agent_runs
ORDER BY updated_at DESC
LIMIT ?`,
[capped * 4],
);
const runs = [];
for (const row of rows) {
if (runs.length >= capped) break;
const parsed = parseJsonLike(row.user_message_json, {});
const metadata = parsed?.metadata ?? {};
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
const taskType = String(runMetadata.taskType ?? metadata.taskType ?? '').trim().toLowerCase();
if (!pageDataTaskTypes.has(taskType)) continue;
runs.push({
id: row.id,
userId: row.user_id,
requestId: row.request_id,
status: row.status,
taskType,
executor: String(runMetadata.executor ?? metadata.executor ?? '').trim() || null,
errorMessage: row.error_message ?? null,
createdAt: Number(row.created_at ?? 0) || null,
updatedAt: Number(row.updated_at ?? 0) || null,
});
}
return { runs, limit: capped };
},
};
}
export const agentCodeRunAdminConfigInternals = {
CONFIG_SCOPE,
CONFIG_TABLE,
DEFAULT_TASK_TYPES,
defaultConfigShape,
buildConfigFromEnv,
mergePatch,
flattenPolicy,
};