utils.ts
1 import { 2 STARTER_KITS, 3 getDefaultModelForProvider, 4 type OnboardingPath, 5 type StarterKit, 6 type SetupProvider, 7 type StarterKitAgentTemplate, 8 } from '@/lib/setup-defaults' 9 import type { ConfiguredProvider, SetupStep, StarterDraftAgent } from './types' 10 import { STEP_ORDER } from './types' 11 12 export function stepIndex(step: SetupStep): number { 13 if (step === 'connect') return STEP_ORDER.indexOf('providers') 14 return STEP_ORDER.indexOf(step) 15 } 16 17 export function defaultKitForPath(path: OnboardingPath): string { 18 if (path === 'manual') return 'blank_workspace' 19 return 'personal_assistant' 20 } 21 22 export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] { 23 if (path === 'quick') { 24 const quickIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot']) 25 return STARTER_KITS.filter((kit) => quickIds.has(kit.id)) 26 } 27 if (path === 'intent') { 28 const intentIds = new Set([ 29 'personal_assistant', 30 'builder_studio', 31 'research_copilot', 32 'operator_swarm', 33 'inbox_triage', 34 'data_analyst', 35 ]) 36 return STARTER_KITS.filter((kit) => intentIds.has(kit.id)) 37 } 38 return STARTER_KITS 39 } 40 41 export function applyIntentContext(prompt: string, intentText: string): string { 42 const trimmed = intentText.trim() 43 if (!trimmed) return prompt 44 return `${prompt} 45 46 Current user intent: 47 - ${trimmed} 48 49 Keep your help aligned to this intent unless the user changes direction.` 50 } 51 52 export function formatAgentCount(count: number): string { 53 if (count === 0) return 'Blank' 54 if (count === 1) return '1 agent' 55 return `${count} agents` 56 } 57 58 export function withHttpScheme(value: string): string { 59 return /^(https?|wss?):\/\//i.test(value) ? value : `http://${value}` 60 } 61 62 export function parseProviderUrl(value: string | null | undefined): URL | null { 63 const trimmed = typeof value === 'string' ? value.trim() : '' 64 if (!trimmed) return null 65 try { 66 return new URL(withHttpScheme(trimmed)) 67 } catch { 68 return null 69 } 70 } 71 72 export function formatEndpointHost(value: string | null | undefined): string | null { 73 const parsed = parseProviderUrl(value) 74 if (!parsed) return null 75 return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname 76 } 77 78 export function isLocalOpenClawEndpoint(value: string | null | undefined): boolean { 79 const parsed = parseProviderUrl(value) 80 if (!parsed) return false 81 const host = parsed.hostname.trim().toLowerCase() 82 return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0' 83 } 84 85 export function resolveOpenClawDashboardUrl(value: string | null | undefined): string { 86 const parsed = parseProviderUrl(value) 87 if (!parsed) return 'http://localhost:18789' 88 const next = new URL(parsed.toString()) 89 if (next.protocol === 'wss:') next.protocol = 'https:' 90 if (next.protocol === 'ws:') next.protocol = 'http:' 91 next.pathname = '' 92 next.search = '' 93 next.hash = '' 94 return next.toString().replace(/\/+$/, '') 95 } 96 97 export function getOpenClawErrorHint(message: string): string | null { 98 const lower = message.toLowerCase() 99 if (lower.includes('timeout') || lower.includes('timed out')) { 100 return 'Ensure the port is open and reachable from this machine.' 101 } 102 if (lower.includes('401') || lower.includes('unauthorized')) { 103 return 'Check your gateway auth token.' 104 } 105 if (lower.includes('405') || lower.includes('method not allowed')) { 106 return 'Enable chatCompletions in your OpenClaw config: openclaw config set gateway.http.endpoints.chatCompletions.enabled true' 107 } 108 if (lower.includes('econnrefused') || lower.includes('connection refused') || lower.includes('connect econnrefused')) { 109 return 'Verify that the OpenClaw gateway is running on the target host.' 110 } 111 return null 112 } 113 114 export function requiresSetupProviderVerification(provider: SetupProvider | null | undefined): boolean { 115 return provider != null && provider !== 'openclaw' && provider !== 'custom' 116 } 117 118 export function preferredConfiguredProvider( 119 template: StarterKitAgentTemplate, 120 configuredProviders: ConfiguredProvider[], 121 fallbackProviderConfigId?: string | null, 122 fallbackProvider?: SetupProvider | null, 123 ): ConfiguredProvider | null { 124 if (fallbackProviderConfigId) { 125 const exact = configuredProviders.find((candidate) => candidate.id === fallbackProviderConfigId) 126 if (exact) return exact 127 } 128 129 if (fallbackProvider) { 130 const exact = configuredProviders.find((candidate) => candidate.setupProvider === fallbackProvider) 131 if (exact) return exact 132 } 133 134 for (const provider of template.recommendedProviders || []) { 135 const exact = configuredProviders.find((candidate) => candidate.setupProvider === provider) 136 if (exact) return exact 137 } 138 139 return configuredProviders[0] || null 140 } 141 142 export function buildStarterDrafts(args: { 143 starterKitId: string | null 144 intentText: string 145 configuredProviders: ConfiguredProvider[] 146 previousDrafts?: StarterDraftAgent[] 147 }): StarterDraftAgent[] { 148 const { starterKitId, intentText, configuredProviders, previousDrafts = [] } = args 149 const starterKit = STARTER_KITS.find((kit) => kit.id === starterKitId) 150 if (!starterKit) return [] 151 152 const previousById = new Map(previousDrafts.map((draft) => [draft.id, draft])) 153 154 return starterKit.agents.map((template) => { 155 const id = `${starterKit.id}:${template.id}` 156 const previous = previousById.get(id) 157 const configuredProvider = preferredConfiguredProvider( 158 template, 159 configuredProviders, 160 previous?.providerConfigId, 161 previous?.setupProvider, 162 ) 163 const oldProvider = previous?.setupProvider || null 164 const previousModel = previous?.model || '' 165 const oldProviderDefault = oldProvider ? getDefaultModelForProvider(oldProvider) : '' 166 const nextProviderDefault = configuredProvider?.defaultModel || '' 167 const shouldRefreshModel = 168 !previousModel.trim() 169 || (oldProvider !== configuredProvider?.setupProvider && previousModel === oldProviderDefault) 170 171 return { 172 id, 173 templateId: template.id, 174 name: previous?.name || template.name, 175 description: previous?.description || template.description, 176 systemPrompt: previous?.systemPrompt || applyIntentContext(template.systemPrompt, intentText), 177 soul: previous?.soul || '', 178 providerConfigId: configuredProvider?.id || null, 179 setupProvider: configuredProvider?.setupProvider || null, 180 provider: configuredProvider?.provider || null, 181 model: shouldRefreshModel ? nextProviderDefault : previousModel, 182 credentialId: configuredProvider?.credentialId || null, 183 apiEndpoint: configuredProvider?.endpoint || null, 184 gatewayProfileId: configuredProvider?.gatewayProfileId || null, 185 tools: template.tools, 186 capabilities: previous?.capabilities || template.capabilities || [], 187 delegationEnabled: previous?.delegationEnabled ?? template.delegationEnabled ?? false, 188 delegationTargetMode: previous?.delegationTargetMode || 'all', 189 delegationTargetAgentIds: previous?.delegationTargetAgentIds || [], 190 autoDraftSkillSuggestions: previous?.autoDraftSkillSuggestions ?? true, 191 orchestratorEnabled: previous?.orchestratorEnabled ?? false, 192 orchestratorMission: previous?.orchestratorMission || '', 193 avatarSeed: previous?.avatarSeed || Math.random().toString(36).slice(2, 10), 194 avatarUrl: previous?.avatarUrl || null, 195 enabled: previous?.enabled ?? true, 196 } 197 }) 198 }