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, };