import crypto from 'node:crypto'; const ANALYTICS_MARKER = 'data-memind-analytics="1"'; export function resolveMindSpaceAnalyticsConfig(env = process.env) { const enabled = String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true'; const websiteId = String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim(); const secret = String(env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim(); return { enabled: enabled && Boolean(websiteId) && Boolean(secret), websiteId, idSecret: secret, identityMode: String(env.MEMIND_ANALYTICS_IDENTITY_MODE ?? 'pseudonymous').trim().toLowerCase() === 'raw' ? 'raw' : 'pseudonymous', analyticsUrl: String(env.MEMIND_ANALYTICS_URL ?? 'http://127.0.0.1:3100').trim() || 'http://127.0.0.1:3100', scriptPath: String(env.MEMIND_ANALYTICS_SCRIPT_PATH ?? '/analytics/script.js').trim() || '/analytics/script.js', hostPath: String(env.MEMIND_ANALYTICS_HOST_PATH ?? '/analytics').trim() || '/analytics', domains: String(env.MEMIND_ANALYTICS_DOMAINS ?? '').trim(), }; } export function resolveProductAnalyticsConfig( env = process.env, baseConfig = resolveMindSpaceAnalyticsConfig(env), ) { const websiteId = String(env.MEMIND_PRODUCT_ANALYTICS_WEBSITE_ID ?? '').trim(); const enabled = String(env.MEMIND_PRODUCT_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true'; return { enabled: enabled && Boolean(websiteId) && Boolean(baseConfig?.idSecret), websiteId, idSecret: String(baseConfig?.idSecret ?? '').trim(), identityMode: baseConfig?.identityMode === 'raw' ? 'raw' : 'pseudonymous', scriptPath: String(baseConfig?.scriptPath ?? '/analytics/script.js').trim() || '/analytics/script.js', hostPath: String(baseConfig?.hostPath ?? '/analytics').trim() || '/analytics', domains: String(baseConfig?.domains ?? '').trim(), }; } export function pseudonymizeAnalyticsId(value, secret) { const normalized = String(value ?? '').trim(); const key = String(secret ?? '').trim(); if (!normalized || !key) return ''; return crypto.createHmac('sha256', key).update(normalized).digest('hex').slice(0, 32); } export function resolveAnalyticsIdentity(value, config = {}) { const normalized = String(value ?? '').trim().replace(/[\r\n\t]+/g, '').slice(0, 128); if (!normalized) return ''; return config?.identityMode === 'raw' ? normalized : pseudonymizeAnalyticsId(normalized, config?.idSecret); } export function resolveAnalyticsOwnerSegment(user = {}) { if (user?.role === 'admin') return 'admin'; const plan = String(user?.planType ?? user?.plan_type ?? 'free').trim().toLowerCase(); return `plan:${plan || 'free'}`; } export function resolveAnalyticsPlan(user = {}) { if (user?.role === 'admin') return 'admin'; return String(user?.planType ?? user?.plan_type ?? 'free').trim().toLowerCase() || 'free'; } export function resolveAnalyticsOwnerLabel(user = {}) { const label = String(user?.displayName ?? user?.display_name ?? user?.username ?? '').trim(); return label.replace(/[\r\n\t]+/g, ' ').slice(0, 80) || '未命名用户'; } export function buildProductAnalyticsContext({ config, user = null } = {}) { if (!config?.enabled || !config.websiteId) return { enabled: false }; const distinctId = user?.id ? resolveAnalyticsIdentity(user.id, config) : ''; return { enabled: true, websiteId: config.websiteId, scriptPath: config.scriptPath || '/analytics/script.js', hostPath: config.hostPath || '/analytics', domains: config.domains || '', identity: distinctId ? { distinctId, username: resolveAnalyticsOwnerLabel(user), ownerSegment: resolveAnalyticsOwnerSegment(user), planType: resolveAnalyticsPlan(user), channel: 'h5', surface: 'product', identityMode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous', } : null, }; } export function sendMindSpaceAnalyticsEvent({ config, eventName, ownerId, pageId = '', publicationId = '', agentRunId = '', channel = 'h5', ownerSegment = 'unknown', ownerLabel = '未命名用户', planType = 'unknown', generatedAt = '', url = '', } = {}) { if (!config?.enabled || !config.websiteId || !config.idSecret || !eventName) return Promise.resolve(false); const owner = resolveAnalyticsIdentity(ownerId, config); if (!owner) return Promise.resolve(false); const endpoint = `${String(config.analyticsUrl || 'http://127.0.0.1:3100').replace(/\/$/, '')}/api/send`; const payload = { website: config.websiteId, id: owner, hostname: '127.0.0.1', url: url || '/', name: String(eventName), data: { owner_id: owner, page_id: String(pageId || ''), publication_id: String(publicationId || ''), agent_run_id: String(agentRunId || ''), channel, owner_segment: String(ownerSegment || 'unknown'), plan_type: String(planType || 'unknown'), owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), generated_at: String(generatedAt || ''), identity_mode: config.identityMode === 'raw' ? 'raw' : 'pseudonymous', }, }; return fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': 'Memind/local-analytics' }, body: JSON.stringify({ type: 'event', payload }), signal: AbortSignal.timeout(1500), }).then((response) => response.ok).catch(() => false); } function jsonForInlineScript(value) { return JSON.stringify(value) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026'); } export function injectMindSpaceAnalytics(html, { ownerId, pageId = '', publicationId = '', ownerSegment = 'unknown', ownerLabel = '未命名用户', planType = 'unknown', generatedAt = '', channel = 'h5', config = resolveMindSpaceAnalyticsConfig(), } = {}) { const source = String(html ?? ''); if (!config?.enabled || !config.websiteId || !/^\s*(`; if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}`); return source.replace(/