import cloud from '@lafjs/cloud' const db = cloud.database(); const COLLECTION = "ab_config"; const DEFAULT_ADMIN_TOKEN = "abtest-admin-token"; export default async function (ctx: FunctionContext) { const action = ctx.body?.action || ctx.query?.action || "list"; const adminToken = ctx.body?.adminToken || ctx.query?.adminToken; if (!checkAdminToken(adminToken)) { return { code: 0, data: null, msg: "unauthorized" }; } switch (action) { case "list": return await listConfigs(); case "save": return await saveConfig(ctx.body?.config); case "disable": return await setEnabled(ctx.body?.experimentId, false); case "enable": return await setEnabled(ctx.body?.experimentId, true); case "delete": return await deleteConfig(ctx.body?.experimentId); default: return { code: 0, data: null, msg: "invalid action" }; } } function checkAdminToken(adminToken: string) { const env = (globalThis as any).process?.env || {}; const token = env.AB_ADMIN_TOKEN || DEFAULT_ADMIN_TOKEN; return !!adminToken && adminToken === token; } async function listConfigs() { const ret = await db.collection(COLLECTION).get(); return { code: 1, data: ret.data || [], msg: "ok" }; } async function saveConfig(config: any) { if (typeof config == "string") { try { config = JSON.parse(config); } catch { return { code: 0, data: null, msg: "config json invalid" }; } } if (Array.isArray(config)) { const list = []; for (let i = 0; i < config.length; i++) { const ret = await saveOneConfig(config[i]); if (ret.code !== 1) { return ret; } list.push(ret.data); } return { code: 1, data: list, msg: "saved" }; } return await saveOneConfig(config); } async function saveOneConfig(config: any) { const error = validateConfig(config); if (error) { return { code: 0, data: null, msg: error }; } const now = Date.now(); const data = { experimentId: config.experimentId, layer: String(config.layer || ""), name: config.name || config.experimentId, enabled: config.enabled !== false, resetAssignments: config.resetAssignments === true, startTime: Number(config.startTime || 0), endTime: Number(config.endTime || 0), variants: normalizeVariants(config.variants), updatedAt: now, }; const old = await db.collection(COLLECTION).where({ experimentId: data.experimentId }).getOne(); if (old.data) { await db.collection(COLLECTION).where({ _id: old.data._id }).update(data); return { code: 1, data: data, msg: "updated" }; } await db.collection(COLLECTION).add({ ...data, createdAt: now }); return { code: 1, data: data, msg: "created" }; } async function setEnabled(experimentId: string, enabled: boolean) { if (!experimentId) { return { code: 0, data: null, msg: "experimentId required" }; } const ret = await db.collection(COLLECTION).where({ experimentId: experimentId }).update({ enabled: enabled, updatedAt: Date.now(), }); return { code: 1, data: ret, msg: "ok" }; } async function deleteConfig(experimentId: string) { if (!experimentId) { return { code: 0, data: null, msg: "experimentId required" }; } const ret = await db.collection(COLLECTION).where({ experimentId: experimentId }).remove({ multi: true }); return { code: 1, data: ret, msg: "ok" }; } function validateConfig(config: any) { if (!config) { return "config required"; } if (!config.experimentId) { return "experimentId required"; } if (!config.layer) { return "layer required"; } if (!Array.isArray(config.variants) || config.variants.length < 2) { return "variants length must be >= 2"; } let total = 0; const groups = {}; for (let i = 0; i < config.variants.length; i++) { const variant = config.variants[i]; if (!variant.group) { return "variant.group required"; } if (groups[variant.group]) { return "variant.group duplicated"; } groups[variant.group] = true; const ratio = Number(variant.ratio); if (!(ratio > 0)) { return "variant.ratio must be positive"; } total += ratio; } if (total <= 0) { return "ratio total must be positive"; } if (config.startTime && config.endTime && Number(config.endTime) <= Number(config.startTime)) { return "endTime must be greater than startTime"; } return ""; } function normalizeVariants(variants: any[]) { return variants.map(variant => ({ group: variant.group, ratio: Number(variant.ratio), })); }