mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-24 00:51:34 +00:00
360 lines
9.9 KiB
TypeScript
360 lines
9.9 KiB
TypeScript
import { ExtensionContext } from "vscode"
|
|
import { z, ZodError } from "zod"
|
|
|
|
import { providerSettingsSchema, ApiConfigMeta, ProviderSettings } from "../../schemas"
|
|
import { Mode, modes } from "../../shared/modes"
|
|
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
|
|
|
const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
|
|
|
|
type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
|
|
|
|
export const providerProfilesSchema = z.object({
|
|
currentApiConfigName: z.string(),
|
|
apiConfigs: z.record(z.string(), providerSettingsWithIdSchema),
|
|
modeApiConfigs: z.record(z.string(), z.string()).optional(),
|
|
migrations: z
|
|
.object({
|
|
rateLimitSecondsMigrated: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
|
|
export type ProviderProfiles = z.infer<typeof providerProfilesSchema>
|
|
|
|
export class ProviderSettingsManager {
|
|
private static readonly SCOPE_PREFIX = "roo_cline_config_"
|
|
private readonly defaultConfigId = this.generateId()
|
|
|
|
private readonly defaultModeApiConfigs: Record<string, string> = Object.fromEntries(
|
|
modes.map((mode) => [mode.slug, this.defaultConfigId]),
|
|
)
|
|
|
|
private readonly defaultProviderProfiles: ProviderProfiles = {
|
|
currentApiConfigName: "default",
|
|
apiConfigs: { default: { id: this.defaultConfigId } },
|
|
modeApiConfigs: this.defaultModeApiConfigs,
|
|
migrations: {
|
|
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
|
|
},
|
|
}
|
|
|
|
private readonly context: ExtensionContext
|
|
|
|
constructor(context: ExtensionContext) {
|
|
this.context = context
|
|
|
|
// TODO: We really shouldn't have async methods in the constructor.
|
|
this.initialize().catch(console.error)
|
|
}
|
|
|
|
public generateId() {
|
|
return Math.random().toString(36).substring(2, 15)
|
|
}
|
|
|
|
// Synchronize readConfig/writeConfig operations to avoid data loss.
|
|
private _lock = Promise.resolve()
|
|
private lock<T>(cb: () => Promise<T>) {
|
|
const next = this._lock.then(cb)
|
|
this._lock = next.catch(() => {}) as Promise<void>
|
|
return next
|
|
}
|
|
|
|
/**
|
|
* Initialize config if it doesn't exist and run migrations.
|
|
*/
|
|
public async initialize() {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
|
|
if (!providerProfiles) {
|
|
await this.store(this.defaultProviderProfiles)
|
|
return
|
|
}
|
|
|
|
let isDirty = false
|
|
|
|
// Ensure all configs have IDs.
|
|
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
|
|
if (!apiConfig.id) {
|
|
apiConfig.id = this.generateId()
|
|
isDirty = true
|
|
}
|
|
}
|
|
|
|
// Ensure migrations field exists
|
|
if (!providerProfiles.migrations) {
|
|
providerProfiles.migrations = { rateLimitSecondsMigrated: false } // Initialize with default values
|
|
isDirty = true
|
|
}
|
|
|
|
if (!providerProfiles.migrations.rateLimitSecondsMigrated) {
|
|
await this.migrateRateLimitSeconds(providerProfiles)
|
|
providerProfiles.migrations.rateLimitSecondsMigrated = true
|
|
isDirty = true
|
|
}
|
|
|
|
if (isDirty) {
|
|
await this.store(providerProfiles)
|
|
}
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to initialize config: ${error}`)
|
|
}
|
|
}
|
|
|
|
private async migrateRateLimitSeconds(providerProfiles: ProviderProfiles) {
|
|
try {
|
|
let rateLimitSeconds: number | undefined
|
|
|
|
try {
|
|
rateLimitSeconds = await this.context.globalState.get<number>("rateLimitSeconds")
|
|
} catch (error) {
|
|
console.error("[MigrateRateLimitSeconds] Error getting global rate limit:", error)
|
|
}
|
|
|
|
if (rateLimitSeconds === undefined) {
|
|
// Failed to get the existing value, use the default.
|
|
rateLimitSeconds = 0
|
|
}
|
|
|
|
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
|
|
if (apiConfig.rateLimitSeconds === undefined) {
|
|
apiConfig.rateLimitSeconds = rateLimitSeconds
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`[MigrateRateLimitSeconds] Failed to migrate rate limit settings:`, error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all available configs with metadata.
|
|
*/
|
|
public async listConfig(): Promise<ApiConfigMeta[]> {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
|
|
return Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({
|
|
name,
|
|
id: apiConfig.id || "",
|
|
apiProvider: apiConfig.apiProvider,
|
|
}))
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to list configs: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save a config with the given name.
|
|
* Preserves the ID from the input 'config' object if it exists,
|
|
* otherwise generates a new one (for creation scenarios).
|
|
*/
|
|
public async saveConfig(name: string, config: ProviderSettingsWithId) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
// Preserve the existing ID if this is an update to an existing config.
|
|
const existingId = providerProfiles.apiConfigs[name]?.id
|
|
providerProfiles.apiConfigs[name] = { ...config, id: config.id || existingId || this.generateId() }
|
|
await this.store(providerProfiles)
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to save config: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a config by name and set it as the current config.
|
|
*/
|
|
public async loadConfig(name: string) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
const providerSettings = providerProfiles.apiConfigs[name]
|
|
|
|
if (!providerSettings) {
|
|
throw new Error(`Config '${name}' not found`)
|
|
}
|
|
|
|
providerProfiles.currentApiConfigName = name
|
|
await this.store(providerProfiles)
|
|
|
|
return providerSettings
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to load config: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a config by ID and set it as the current config.
|
|
*/
|
|
public async loadConfigById(id: string) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
const providerSettings = Object.entries(providerProfiles.apiConfigs).find(
|
|
([_, apiConfig]) => apiConfig.id === id,
|
|
)
|
|
|
|
if (!providerSettings) {
|
|
throw new Error(`Config with ID '${id}' not found`)
|
|
}
|
|
|
|
const [name, apiConfig] = providerSettings
|
|
providerProfiles.currentApiConfigName = name
|
|
await this.store(providerProfiles)
|
|
|
|
return { config: apiConfig, name }
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to load config by ID: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a config by name.
|
|
*/
|
|
public async deleteConfig(name: string) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
|
|
if (!providerProfiles.apiConfigs[name]) {
|
|
throw new Error(`Config '${name}' not found`)
|
|
}
|
|
|
|
if (Object.keys(providerProfiles.apiConfigs).length === 1) {
|
|
throw new Error(`Cannot delete the last remaining configuration`)
|
|
}
|
|
|
|
delete providerProfiles.apiConfigs[name]
|
|
await this.store(providerProfiles)
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to delete config: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a config exists by name.
|
|
*/
|
|
public async hasConfig(name: string) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
return name in providerProfiles.apiConfigs
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to check config existence: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the API config for a specific mode.
|
|
*/
|
|
public async setModeConfig(mode: Mode, configId: string) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const providerProfiles = await this.load()
|
|
const { modeApiConfigs = {} } = providerProfiles
|
|
modeApiConfigs[mode] = configId
|
|
await this.store(providerProfiles)
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to set mode config: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the API config ID for a specific mode.
|
|
*/
|
|
public async getModeConfigId(mode: Mode) {
|
|
try {
|
|
return await this.lock(async () => {
|
|
const { modeApiConfigs } = await this.load()
|
|
return modeApiConfigs?.[mode]
|
|
})
|
|
} catch (error) {
|
|
throw new Error(`Failed to get mode config: ${error}`)
|
|
}
|
|
}
|
|
|
|
public async export() {
|
|
try {
|
|
return await this.lock(async () => providerProfilesSchema.parse(await this.load()))
|
|
} catch (error) {
|
|
throw new Error(`Failed to export provider profiles: ${error}`)
|
|
}
|
|
}
|
|
|
|
public async import(providerProfiles: ProviderProfiles) {
|
|
try {
|
|
return await this.lock(() => this.store(providerProfiles))
|
|
} catch (error) {
|
|
throw new Error(`Failed to import provider profiles: ${error}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset provider profiles by deleting them from secrets.
|
|
*/
|
|
public async resetAllConfigs() {
|
|
return await this.lock(async () => {
|
|
await this.context.secrets.delete(this.secretsKey)
|
|
})
|
|
}
|
|
|
|
private get secretsKey() {
|
|
return `${ProviderSettingsManager.SCOPE_PREFIX}api_config`
|
|
}
|
|
|
|
private async load(): Promise<ProviderProfiles> {
|
|
try {
|
|
const content = await this.context.secrets.get(this.secretsKey)
|
|
|
|
if (!content) {
|
|
return this.defaultProviderProfiles
|
|
}
|
|
|
|
const providerProfiles = providerProfilesSchema
|
|
.extend({
|
|
apiConfigs: z.record(z.string(), z.any()),
|
|
})
|
|
.parse(JSON.parse(content))
|
|
|
|
const apiConfigs = Object.entries(providerProfiles.apiConfigs).reduce(
|
|
(acc, [key, apiConfig]) => {
|
|
const result = providerSettingsWithIdSchema.safeParse(apiConfig)
|
|
return result.success ? { ...acc, [key]: result.data } : acc
|
|
},
|
|
{} as Record<string, ProviderSettingsWithId>,
|
|
)
|
|
|
|
return {
|
|
...providerProfiles,
|
|
apiConfigs: Object.fromEntries(
|
|
Object.entries(apiConfigs).filter(([_, apiConfig]) => apiConfig !== null),
|
|
),
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
telemetryService.captureSchemaValidationError({ schemaName: "ProviderProfiles", error })
|
|
}
|
|
|
|
throw new Error(`Failed to read provider profiles from secrets: ${error}`)
|
|
}
|
|
}
|
|
|
|
private async store(providerProfiles: ProviderProfiles) {
|
|
try {
|
|
await this.context.secrets.store(this.secretsKey, JSON.stringify(providerProfiles, null, 2))
|
|
} catch (error) {
|
|
throw new Error(`Failed to write provider profiles to secrets: ${error}`)
|
|
}
|
|
}
|
|
}
|