import * as fs from 'node:fs'; import * as path from 'node:path'; import logManager from '@electron/service/logger'; import { providerApiService } from '@electron/service/provider-api-service'; import { listAgentsSnapshot } from '@electron/utils/agent-config'; import { ensureDir, ensureOpenClawRuntimeLayout, getOpenClawRuntimePaths } from '@electron/utils/paths'; import type { AgentSummary, AgentsSnapshot } from '@runtime/lib/agents'; import type { ProviderAccount } from '@runtime/lib/providers'; interface AgentRuntimeSyncMeta { agentId: string; providerAccountId: string | null; modelRef: string | null; inheritedModel: boolean; runtimeDir: string; } export interface ProviderRuntimeSyncResult { syncedAt: string; agentCount: number; defaultAccountId: string | null; globalRegistryPath: string; agents: AgentRuntimeSyncMeta[]; warnings: string[]; } interface RuntimeProviderProfile { id: string; vendorId: string; label: string; authMode: string; apiKey: string | null; hasKey: boolean; baseUrl?: string; apiProtocol?: string; headers?: Record; metadata?: ProviderAccount['metadata']; } const AGENT_RUNTIME_DIR_NAME = 'runtime'; const AUTH_PROFILES_FILE_NAME = 'auth-profiles.json'; const MODELS_FILE_NAME = 'models.json'; const OPENCLAW_FILE_NAME = 'openclaw.json'; const GLOBAL_REGISTRY_FILE_NAME = 'agents-runtime.json'; function writeJson(filePath: string, data: unknown): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); } function getEnabledAccounts(accounts?: ProviderAccount[]): ProviderAccount[] { const source = Array.isArray(accounts) ? accounts : providerApiService.getAccounts(); return source.filter((account) => account.enabled !== false); } function resolveEffectiveAccount( agent: AgentSummary, accounts: ProviderAccount[], defaultAccountId: string | null, ): ProviderAccount | null { if (agent.providerAccountId) { return accounts.find((account) => account.id === agent.providerAccountId) ?? null; } if (defaultAccountId) { return accounts.find((account) => account.id === defaultAccountId) ?? null; } return accounts[0] ?? null; } function buildRuntimeProfile(account: ProviderAccount | null): RuntimeProviderProfile[] { if (!account) return []; const apiKey = providerApiService.getApiKey(account.id).apiKey; return [{ id: account.id, vendorId: account.vendorId, label: account.label, authMode: account.authMode, apiKey: apiKey || null, hasKey: Boolean(apiKey), baseUrl: account.baseUrl, apiProtocol: account.apiProtocol, headers: account.headers, metadata: account.metadata, }]; } function buildRuntimeModelEntry( agent: AgentSummary, effectiveAccount: ProviderAccount | null, defaultModelRef: string | null, ) { const effectiveModelRef = agent.modelRef ?? effectiveAccount?.model ?? defaultModelRef ?? null; if (!effectiveModelRef) { return []; } return [{ id: effectiveModelRef, modelRef: effectiveModelRef, providerAccountId: effectiveAccount?.id ?? null, vendorId: effectiveAccount?.vendorId ?? agent.vendorId ?? null, baseUrl: effectiveAccount?.baseUrl ?? null, apiProtocol: effectiveAccount?.apiProtocol ?? null, headers: effectiveAccount?.headers ?? {}, fallbackModels: effectiveAccount?.fallbackModels ?? [], fallbackAccountIds: effectiveAccount?.fallbackAccountIds ?? [], inheritedModel: Boolean(agent.inheritedModel), source: agent.overrideModelRef ? 'agent-override' : 'provider-default', }]; } function ensureAgentRuntimeDir(agent: AgentSummary): string { const agentDir = agent.agentDir?.trim(); if (!agentDir) { throw new Error(`Agent "${agent.id}" is missing agentDir`); } return ensureDir(path.join(agentDir, AGENT_RUNTIME_DIR_NAME)); } function writeAgentRuntimeFiles( agent: AgentSummary, snapshot: AgentsSnapshot, accounts: ProviderAccount[], defaultAccountId: string | null, syncedAt: string, warnings: string[], ): AgentRuntimeSyncMeta { const runtimeDir = ensureAgentRuntimeDir(agent); const effectiveAccount = resolveEffectiveAccount(agent, accounts, defaultAccountId); const profiles = buildRuntimeProfile(effectiveAccount); const models = buildRuntimeModelEntry(agent, effectiveAccount, snapshot.defaultModelRef); if (agent.providerAccountId && !effectiveAccount) { warnings.push(`Agent "${agent.id}" references missing provider account "${agent.providerAccountId}"`); } if (!effectiveAccount) { warnings.push(`Agent "${agent.id}" has no enabled provider account available`); } if (models.length === 0) { warnings.push(`Agent "${agent.id}" has no model configured after runtime sync`); } writeJson(path.join(runtimeDir, AUTH_PROFILES_FILE_NAME), { version: 1, syncedAt, agentId: agent.id, defaultProfileId: effectiveAccount?.id ?? null, profiles, }); writeJson(path.join(runtimeDir, MODELS_FILE_NAME), { version: 1, syncedAt, agentId: agent.id, defaultModelRef: models[0]?.modelRef ?? null, models, }); writeJson(path.join(runtimeDir, OPENCLAW_FILE_NAME), { version: 1, syncedAt, agentId: agent.id, agentName: agent.name, mainSessionKey: agent.mainSessionKey, workspace: agent.workspace ?? null, agentDir: agent.agentDir ?? null, providerAccountId: effectiveAccount?.id ?? null, defaultProviderAccountId: snapshot.defaultProviderAccountId, modelRef: models[0]?.modelRef ?? null, defaultModelRef: snapshot.defaultModelRef, inheritedModel: Boolean(agent.inheritedModel), }); return { agentId: agent.id, providerAccountId: effectiveAccount?.id ?? null, modelRef: models[0]?.modelRef ?? null, inheritedModel: Boolean(agent.inheritedModel), runtimeDir, }; } export function syncProviderRuntimeSnapshot(options?: { accounts?: ProviderAccount[]; defaultAccountId?: string | null; snapshot?: AgentsSnapshot; }): ProviderRuntimeSyncResult { const syncedAt = new Date().toISOString(); const accounts = getEnabledAccounts(options?.accounts); const defaultAccountId = options?.defaultAccountId ?? providerApiService.getDefault().accountId; const snapshot = options?.snapshot ?? listAgentsSnapshot(accounts, defaultAccountId); const warnings: string[] = []; const runtimePaths = ensureOpenClawRuntimeLayout(getOpenClawRuntimePaths()); const agents = snapshot.agents.map((agent) => writeAgentRuntimeFiles(agent, snapshot, accounts, defaultAccountId, syncedAt, warnings), ); const globalRegistryPath = path.join(runtimePaths.runtimeDir, GLOBAL_REGISTRY_FILE_NAME); writeJson(globalRegistryPath, { version: 1, syncedAt, defaultAccountId, defaultModelRef: snapshot.defaultModelRef, agents, }); if (warnings.length > 0) { logManager.warn('Provider runtime sync completed with warnings', warnings); } else { logManager.info('Provider runtime sync completed', { agentCount: agents.length, globalRegistryPath, }); } return { syncedAt, agentCount: agents.length, defaultAccountId, globalRegistryPath, agents, warnings, }; }