import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { fileURLToPath, pathToFileURL } from 'node:url'; const require = createRequire(import.meta.url); const SOURCE_DIR = path.dirname(fileURLToPath(import.meta.url)); export const PACKAGE_ROOT = path.resolve(SOURCE_DIR, '..', '..', '..'); const { resolveWorkspaceRoot } = require(path.join(PACKAGE_ROOT, 'mcp', 'src', 'core', 'runtime-context.js')); const { getProductMode, normalizeProductMode } = require(path.join(PACKAGE_ROOT, 'mcp', 'src', 'core', 'product-mode.js')); const DEFAULT_CONFIG = Object.freeze({ edition: 'auto', dashboard: Object.freeze({ enabled: true, port: 4320, }), personal: Object.freeze({ polling: Object.freeze({ enabled: true, retryMs: 10000, friendPollingEnabled: true, }), }), enterprise: Object.freeze({ relay: Object.freeze({ enabled: true, batchSize: 100, waitMs: 30000, retryMinMs: 1000, retryMaxMs: 60000, upstreamReconnectMs: 300000, upstreamRetryMs: 60000, deviceRefreshMs: 60000, }), }), }); function mergeConfig(base, override) { if (!override || typeof override !== 'object' || Array.isArray(override)) return base; const output = { ...base }; for (const [key, value] of Object.entries(override)) { output[key] = value && typeof value === 'object' && !Array.isArray(value) ? mergeConfig(base?.[key] || {}, value) : value; } return output; } function normalizeConfig(config = {}) { const merged = mergeConfig(structuredClone(DEFAULT_CONFIG), config); merged.dashboard.port = Math.max(1, Math.min(65535, Number(merged.dashboard.port) || 4320)); merged.personal.polling.retryMs = Math.max(3000, Number(merged.personal.polling.retryMs) || 10000); merged.enterprise.relay.batchSize = Math.max(1, Math.min(500, Number(merged.enterprise.relay.batchSize) || 100)); merged.enterprise.relay.waitMs = Math.max(1000, Math.min(60000, Number(merged.enterprise.relay.waitMs) || 30000)); merged.enterprise.relay.retryMinMs = Math.max(250, Number(merged.enterprise.relay.retryMinMs) || 1000); merged.enterprise.relay.retryMaxMs = Math.max(merged.enterprise.relay.retryMinMs, Number(merged.enterprise.relay.retryMaxMs) || 60000); merged.enterprise.relay.upstreamReconnectMs = Math.max(30000, Number(merged.enterprise.relay.upstreamReconnectMs) || 300000); merged.enterprise.relay.upstreamRetryMs = Math.max(5000, Number(merged.enterprise.relay.upstreamRetryMs) || 60000); merged.enterprise.relay.deviceRefreshMs = Math.max(10000, Number(merged.enterprise.relay.deviceRefreshMs) || 60000); return merged; } export function resolveRuntimeMode(config = {}, forceMode = '') { const forced = normalizeProductMode(forceMode); if (forced) return forced; const configured = normalizeProductMode(config.edition); if (configured) return configured; return getProductMode().mode; } export async function loadRuntimeConfig(options = {}) { const workspaceRoot = resolveWorkspaceRoot({ packageRoot: PACKAGE_ROOT, workspaceRoot: options.workspaceRoot, }); const explicitPath = String(options.configPath || process.env.QIWEI_RUNTIME_CONFIG || '').trim(); const projectPath = path.join(workspaceRoot, 'qiwei.runtime.config.mjs'); const examplePath = path.join(PACKAGE_ROOT, 'qiwei.runtime.config.example.mjs'); const configPath = explicitPath ? path.resolve(workspaceRoot, explicitPath) : fs.existsSync(projectPath) ? projectPath : examplePath; let loaded = {}; if (fs.existsSync(configPath)) { const version = fs.statSync(configPath).mtimeMs; const module = await import(`${pathToFileURL(configPath).href}?v=${version}`); loaded = module.default || {}; } const config = normalizeConfig(loaded); return { config, configPath, workspaceRoot, packageRoot: PACKAGE_ROOT, mode: resolveRuntimeMode(config, options.forceMode), }; } export { DEFAULT_CONFIG, mergeConfig, normalizeConfig };