100 lines
4.3 KiB
TypeScript
100 lines
4.3 KiB
TypeScript
import type { Disposable } from 'vscode'
|
||
import type { ClaudeProfile } from '../../cc/profiles'
|
||
import { getDefaultProfilePath, getProfilesDir, listClaudeProfiles } from '../../cc/profiles'
|
||
|
||
/** 下拉里的一项:id 是 profile 名;「默认」(不带 `--settings`,claude 用 ~/.claude 的登录)的 id 是 ''。 */
|
||
export interface ProfileOption {
|
||
id: string
|
||
label: string
|
||
}
|
||
|
||
export interface ProfileChoice {
|
||
options: ProfileOption[]
|
||
selected: string
|
||
}
|
||
|
||
export const DEFAULT_OPTION: ProfileOption = { id: '', label: '默认' }
|
||
|
||
/** 下拉显示临时选择或设置默认值,失效的选择明确标出,避免回答时偷偷换模型。 */
|
||
export function profileChoice(profiles: readonly ClaudeProfile[], defaultPath: string, temporaryPath: string | undefined, configuredPath = ''): ProfileChoice {
|
||
const fallback = profiles.find(profile => profile.path === defaultPath)
|
||
const requestedPath = temporaryPath ?? (configuredPath || fallback?.path)
|
||
const selected = profiles.find(profile => profile.path === requestedPath)?.name ?? requestedPath ?? DEFAULT_OPTION.id
|
||
const options = [...(!fallback || selected === '' ? [DEFAULT_OPTION] : []), ...profiles.map(({ name }) => ({ id: name, label: name }))]
|
||
if (!options.some(option => option.id === selected))
|
||
options.push({ id: selected, label: `不可用:${selected}` })
|
||
return { options, selected }
|
||
}
|
||
|
||
export interface ProfileSource {
|
||
list: () => Promise<ClaudeProfile[]>
|
||
defaultPath: () => string
|
||
directory: () => string
|
||
}
|
||
|
||
const PROFILE_SOURCE: ProfileSource = { list: listClaudeProfiles, defaultPath: getDefaultProfilePath, directory: getProfilesDir }
|
||
|
||
/**
|
||
* 一套 profile 选择:临时切换所有预览共用、不写进设置,重新创建服务或这一套的默认配置改了以后回到设置里的默认值。
|
||
*
|
||
* @param purpose 这一套给什么用(「问答」「重写注释」);报错时用它说是哪一套、该改设置里的哪一项
|
||
* @param configuredPath 读设置里这一套的默认 profile 路径
|
||
*/
|
||
export class AskProfiles {
|
||
private readonly listeners = new Set<(choice: ProfileChoice) => void>()
|
||
private temporary: string | undefined
|
||
private defaultKey: string | undefined
|
||
|
||
constructor(readonly purpose: string, private readonly configuredPath: () => string, private readonly source: ProfileSource = PROFILE_SOURCE) {}
|
||
|
||
async choice(): Promise<ProfileChoice> {
|
||
return (await this.read()).choice
|
||
}
|
||
|
||
/** 下一个问题使用当前显示的模型,失效时直接报错而不换成其他模型。 */
|
||
async current(): Promise<ClaudeProfile | undefined> {
|
||
const { profiles, choice } = await this.read()
|
||
this.publish(choice)
|
||
const profile = profiles.find(profile => profile.name === choice.selected)
|
||
if (choice.selected && !profile)
|
||
throw new Error(`${this.purpose}模型不可用:${choice.selected}。请在设置的「${this.purpose}默认模型」里重新选择,或在输入框旁临时切换。`)
|
||
return profile
|
||
}
|
||
|
||
/** 选择变化时通知所有打开的预览,让显示和实际发送使用同一个模型。 */
|
||
subscribe(listener: (choice: ProfileChoice) => void): Disposable {
|
||
this.listeners.add(listener)
|
||
return { dispose: () => this.listeners.delete(listener) }
|
||
}
|
||
|
||
async select(id: string): Promise<void> {
|
||
const { profiles, choice } = await this.read()
|
||
const profile = profiles.find(profile => profile.name === id)
|
||
if (id ? !profile : !choice.options.some(option => option.id === ''))
|
||
throw new Error(`${this.purpose}模型不可用:${id}。请重新选择。`)
|
||
this.temporary = profile?.path ?? ''
|
||
await this.refresh()
|
||
}
|
||
|
||
async refresh(): Promise<void> {
|
||
this.publish(await this.choice())
|
||
}
|
||
|
||
private publish(choice: ProfileChoice): void {
|
||
for (const listener of [...this.listeners])
|
||
listener(choice)
|
||
}
|
||
|
||
private async read(): Promise<{ profiles: ClaudeProfile[], choice: ProfileChoice }> {
|
||
const profiles = await this.source.list()
|
||
const configuredPath = this.configuredPath().trim()
|
||
const defaultPath = this.source.defaultPath()
|
||
const key = JSON.stringify([configuredPath, this.source.directory()])
|
||
if (key !== this.defaultKey) {
|
||
this.defaultKey = key
|
||
this.temporary = undefined
|
||
}
|
||
return { profiles, choice: profileChoice(profiles, defaultPath, this.temporary, configuredPath) }
|
||
}
|
||
}
|