import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; import { loadConfig } from './src/config'; import { fetchModelEntries } from './src/fetcher'; import { createModelLoadCoordinator, type LoadAttemptResult } from './src/loader'; import { buildProviderConfig } from './src/provider'; import type { ProviderModelConfig } from './src/types'; /** * CLIProxyAPI Pi Provider 扩展(1.1)。 * * 启动流程: * 1. 读取 env;缺失则不注册 * 2. async factory 内 boot 预拉(默认 5s);失败/空列表注册空壳 * 3. session_start:若仍无模型,生命周期内最多自动补拉一次(默认 5s) * 4. /cliproxy-reload:手动刷新(默认 30s);失败或空列表不覆盖已有非空目录 */ export default async function (pi: ExtensionAPI) { const loaded = loadConfig(); if (!loaded.ok) { console.error(`[CLIProxyAPI] ${loaded.error}`); return; } const { config } = loaded; const providerConfig = buildProviderConfig(config.baseUrl, config.apiKey); const coordinator = createModelLoadCoordinator((timeoutMs) => fetchModelEntries(config.modelsUrl, config.apiKey, timeoutMs), ); function registerModels(models: ProviderModelConfig[]) { pi.registerProvider('cliproxy', { ...providerConfig, models, }); } function notifyWarning( ctx: { ui: { notify: (message: string, type?: string) => void } }, result: LoadAttemptResult, ) { let message = result.message; if (result.ui === 'failed_empty' || result.ui === 'empty') { message = `${message}。可在代理就绪后执行 /cliproxy-reload`; } ctx.ui.notify(`[CLIProxyAPI] ${message}`, 'warning'); } // Boot preload const boot = await coordinator.load({ timeoutMs: config.bootTimeoutMs, reason: 'boot', }); registerModels(boot.acceptedModels); if (boot.ui === 'failed_empty' || boot.ui === 'empty') { console.error(`[CLIProxyAPI] ${boot.message}`); } pi.on('session_start', async (_event, ctx) => { // TUI is ready: surface boot success once if we already have models. if (coordinator.hasNonEmptyCatalog()) { const n = coordinator.consumePendingSuccessNotify(); if (n !== null) { ctx.ui.notify(`[CLIProxyAPI] 已加载 ${n} 个模型`, 'info'); } return; } if (!coordinator.shouldSessionAutoRetry()) { return; } const result = await coordinator.load({ timeoutMs: config.retryTimeoutMs, reason: 'session-retry', }); if (result.shouldRegister) { registerModels(result.acceptedModels); } if (result.ui === 'loaded') { const n = coordinator.consumePendingSuccessNotify(); if (n !== null) { ctx.ui.notify(`[CLIProxyAPI] 已加载 ${n} 个模型`, 'info'); } return; } if (result.ui !== 'session_retry_skipped') { notifyWarning(ctx, result); } }); pi.registerCommand('cliproxy-reload', { description: '重新从 CLIProxyAPI 拉取模型列表', handler: async (_args, ctx) => { const result = await coordinator.load({ timeoutMs: config.reloadTimeoutMs, reason: 'manual-reload', }); if (result.shouldRegister) { registerModels(result.acceptedModels); } if (result.ui === 'loaded') { coordinator.markSuccessNotified(); ctx.ui.notify( `[CLIProxyAPI] 已加载 ${result.acceptedModels.length} 个模型`, 'info', ); return; } notifyWarning(ctx, result); }, }); }