v0.3.0: 模型与设置(目录/API key/模型切换/系统提示词)

This commit is contained in:
Rick Ye
2026-09-05 09:41:57 +08:00
parent 9984e2a427
commit fe70cb0f71
11 changed files with 631 additions and 8 deletions
+33
View File
@@ -72,6 +72,39 @@ export function registerIpc(): AgentService {
})
})
// ---- 模型与凭据 ----
const pass = (fn: () => Promise<unknown>): Promise<unknown> =>
fn().catch((err: unknown) => {
throw new Error(err instanceof Error ? err.message : String(err))
})
ipcMain.handle('models:catalog', () => pass(() => service.getModelCatalog()))
ipcMain.handle('models:set', (_event, providerId: unknown, modelId: unknown) => {
if (typeof providerId !== 'string' || typeof modelId !== 'string') throw new Error('invalid model')
return pass(() => service.setModel(providerId, modelId))
})
ipcMain.handle('auth:setKey', (_event, providerId: unknown, apiKey: unknown) => {
if (typeof providerId !== 'string' || typeof apiKey !== 'string' || !apiKey.trim()) {
throw new Error('invalid api key')
}
return pass(() => service.setApiKey(providerId, apiKey.trim()))
})
ipcMain.handle('auth:logout', (_event, providerId: unknown) => {
if (typeof providerId !== 'string') throw new Error('invalid provider')
return pass(() => service.logoutProvider(providerId))
})
// ---- 应用设置 ----
ipcMain.handle('settings:get', () => service.getSettings())
ipcMain.handle('settings:setPrompt', (_event, text: unknown) => {
if (typeof text !== 'string') throw new Error('invalid prompt')
return pass(() => service.setSystemPrompt(text))
})
ipcMain.handle('app:versions', () => ({
app: process.env.npm_package_version ?? '0.0.0',
electron: process.versions.electron,
+134 -2
View File
@@ -1,4 +1,6 @@
import { app, shell } from 'electron'
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
createAgentSessionRuntime,
createAgentSessionServices,
@@ -11,7 +13,15 @@ import {
type CreateAgentSessionRuntimeFactory,
type SessionInfo
} from '@earendil-works/pi-coding-agent'
import type { AgentUiEvent, ModelInfo, SessionMeta, SessionState, SessionStatsInfo } from '@shared/types'
import type {
AgentUiEvent,
AppSettings,
ModelCatalog,
ModelInfo,
SessionMeta,
SessionState,
SessionStatsInfo
} from '@shared/types'
/** 增量事件合批的刷新间隔(ms),参考 pi-web 的 60ms 快照节流经验取略低值 */
const DELTA_FLUSH_MS = 40
@@ -30,11 +40,35 @@ export class AgentService {
private flushTimer: NodeJS.Timeout | null = null
private attached: AgentSession | null = null
private unsubscribeAttached: (() => void) | null = null
private settings: AppSettings = { systemPrompt: '' }
private get cwd(): string {
return app.getPath('home')
}
private get settingsFile(): string {
return join(app.getPath('userData'), 'aso-settings.json')
}
private loadSettings(): AppSettings {
try {
const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as Partial<AppSettings>
return {
systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : ''
}
} catch {
return { systemPrompt: '' }
}
}
private saveSettings(): void {
try {
writeFileSync(this.settingsFile, JSON.stringify(this.settings, null, 2), 'utf8')
} catch (err) {
console.error('[agent-service] save settings failed:', err)
}
}
onEvent(fn: (event: AgentUiEvent) => void): () => void {
this.listeners.add(fn)
return () => this.listeners.delete(fn)
@@ -75,7 +109,14 @@ export class AgentService {
private async create(): Promise<AgentSessionRuntime> {
const cwd = this.cwd
const agentDir = getAgentDir()
const initialServices = await createAgentSessionServices({ cwd, agentDir })
this.settings = this.loadSettings()
const initialServices = await createAgentSessionServices({
cwd,
agentDir,
resourceLoaderOptions: this.settings.systemPrompt.trim()
? { systemPrompt: this.settings.systemPrompt }
: undefined
})
// 每次切换/新建时由 runtime 调用;cwd 变化时重建服务,否则复用
const factory: CreateAgentSessionRuntimeFactory = async ({
@@ -330,6 +371,97 @@ export class AgentService {
await runtime.newSession()
}
}
// ---- 模型与凭据 ----
async getModelCatalog(): Promise<ModelCatalog> {
const runtime = await this.getRuntime()
const mr = runtime.session.modelRuntime
const credentials = await mr.listCredentials()
const credSet = new Set(credentials.map((c) => `${c.providerId}:${c.type}`))
const providers = mr
.getProviders()
.map((p) => {
const pid = String(p.id ?? '')
return {
id: pid,
name: String(p.name ?? pid),
hasApiKey: credSet.has(`${pid}:api_key`),
isOAuth: credSet.has(`${pid}:oauth`),
models: mr
.getModels(pid)
.map((m) => ({
id: String(m.id ?? ''),
name: String(m.name ?? m.id ?? ''),
contextWindow: Number(m.contextWindow ?? 0),
reasoning: Boolean(m.reasoning)
}))
}
})
// 已配置凭据的排前面,其余按名称排序
.sort((a, b) => {
const av = a.hasApiKey || a.isOAuth ? 0 : 1
const bv = b.hasApiKey || b.isOAuth ? 0 : 1
return av - bv || a.name.localeCompare(b.name)
})
return { providers }
}
async setApiKey(providerId: string, apiKey: string): Promise<void> {
const runtime = await this.getRuntime()
const mr = runtime.session.modelRuntime
// login 走凭据存储的 modify(持久化到 ~/.pi/agent/auth.json,与 pi CLI 共享)
await mr.login(providerId, 'api_key', {
prompt: async () => apiKey,
notify: async () => undefined
})
this.emit({ type: 'state', state: this.buildState(runtime.session) })
}
async logoutProvider(providerId: string): Promise<void> {
const runtime = await this.getRuntime()
await runtime.session.modelRuntime.logout(providerId)
this.emit({ type: 'state', state: this.buildState(runtime.session) })
}
async setModel(providerId: string, modelId: string): Promise<void> {
const runtime = await this.getRuntime()
const mr = runtime.session.modelRuntime
const model = mr.getModel(providerId, modelId)
if (!model) {
throw new Error(`未知模型: ${providerId}/${modelId}`)
}
await runtime.session.setModel(model, { persist: true })
this.emit({ type: 'state', state: this.buildState(runtime.session) })
}
// ---- 应用设置 ----
getSettings(): AppSettings {
return { ...this.settings }
}
/** 更新系统提示词后重建运行时(continueRecent 重新挂载当前最近会话,历史保留) */
async setSystemPrompt(text: string): Promise<void> {
this.settings = { systemPrompt: text }
this.saveSettings()
await this.rebuildRuntime()
}
private async rebuildRuntime(): Promise<void> {
const old = this.runtime
this.runtime = null
this.initPromise = null
this.unsubscribeAttached?.()
this.unsubscribeAttached = null
this.attached = null
if (old) {
await old.dispose().catch((err: unknown) => {
console.error('[agent-service] runtime dispose error:', err)
})
}
await this.ensureReady()
}
}
function toSessionMeta(info: SessionInfo): SessionMeta {