v0.3.0: 模型与设置(目录/API key/模型切换/系统提示词)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user