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 {
+9 -1
View File
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron'
import type { AgentUiEvent, SessionMeta, SessionState } from '@shared/types'
import type { AgentUiEvent, AppSettings, ModelCatalog, SessionMeta, SessionState } from '@shared/types'
const api = {
send: (text: string): Promise<void> => ipcRenderer.invoke('agent:send', text),
@@ -13,6 +13,14 @@ const api = {
openSession: (path: string): Promise<void> => ipcRenderer.invoke('sessions:open', path),
renameSession: (name: string): Promise<void> => ipcRenderer.invoke('sessions:rename', name),
deleteSession: (path: string): Promise<void> => ipcRenderer.invoke('sessions:delete', path),
getModelCatalog: (): Promise<ModelCatalog> => ipcRenderer.invoke('models:catalog'),
setModel: (providerId: string, modelId: string): Promise<void> =>
ipcRenderer.invoke('models:set', providerId, modelId),
setApiKey: (providerId: string, apiKey: string): Promise<void> =>
ipcRenderer.invoke('auth:setKey', providerId, apiKey),
logoutProvider: (providerId: string): Promise<void> => ipcRenderer.invoke('auth:logout', providerId),
getSettings: (): Promise<AppSettings> => ipcRenderer.invoke('settings:get'),
setSystemPrompt: (text: string): Promise<void> => ipcRenderer.invoke('settings:setPrompt', text),
onEvent: (callback: (event: AgentUiEvent) => void): (() => void) => {
const listener = (_e: Electron.IpcRendererEvent, event: AgentUiEvent): void => callback(event)
ipcRenderer.on('agent:event', listener)
+5 -1
View File
@@ -1,13 +1,15 @@
import { useRef } from 'react'
import { useRef, useState } from 'react'
import type { JSX } from 'react'
import { useAgent } from './hooks/useAgent'
import { Sidebar } from './components/Sidebar'
import { ChatView } from './components/ChatView'
import { Composer } from './components/Composer'
import { SettingsModal } from './components/SettingsModal'
export default function App(): JSX.Element {
const { state, sessions, send, stop, clearError, newSession, openSession, renameSession, deleteSession } = useAgent()
const prefillRef = useRef<string | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
const handlePickSuggestion = (text: string): void => {
prefillRef.current = text
@@ -26,6 +28,7 @@ export default function App(): JSX.Element {
onOpenSession={openSession}
onRenameSession={renameSession}
onDeleteSession={deleteSession}
onOpenSettings={() => setSettingsOpen(true)}
/>
<main className="flex min-w-0 flex-1 flex-col">
<ChatView
@@ -47,6 +50,7 @@ export default function App(): JSX.Element {
prefillRef={prefillRef}
/>
</main>
<SettingsModal open={settingsOpen} sessionState={state.sessionState} onClose={() => setSettingsOpen(false)} />
</div>
)
}
@@ -0,0 +1,309 @@
import { useEffect, useMemo, useState } from 'react'
import type { JSX } from 'react'
import { Check, Cpu, KeyRound, LogOut, Search, Sparkles, X } from 'lucide-react'
import type { ModelCatalog, ProviderCatalogEntry, SessionState } from '@shared/types'
type Tab = 'model' | 'prompt'
function ProviderBadge({ provider }: { provider: ProviderCatalogEntry }): JSX.Element {
if (provider.hasApiKey) {
return <span className="rounded-full bg-emerald-950/70 px-1.5 py-0.5 text-[10px] text-emerald-300">API Key</span>
}
if (provider.isOAuth) {
return <span className="rounded-full bg-sky-950/70 px-1.5 py-0.5 text-[10px] text-sky-300">OAuth</span>
}
return <span className="rounded-full bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500">未配置</span>
}
function formatContext(n: number): string {
if (n >= 1000000) return `${Math.round(n / 1000000)}M`
if (n >= 1000) return `${Math.round(n / 1000)}K`
return String(n)
}
export function SettingsModal({
open,
sessionState,
onClose
}: {
open: boolean
sessionState: SessionState | null
onClose: () => void
}): JSX.Element | null {
const [tab, setTab] = useState<Tab>('model')
const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const [keyProvider, setKeyProvider] = useState<string | null>(null)
const [keyValue, setKeyValue] = useState('')
const [prompt, setPrompt] = useState('')
const [promptDirty, setPromptDirty] = useState(false)
const [busy, setBusy] = useState(false)
const [notice, setNotice] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null)
const refreshCatalog = (): void => {
setLoading(true)
window.api
.getModelCatalog()
.then(setCatalog)
.catch((err: unknown) =>
setNotice({ kind: 'err', text: err instanceof Error ? err.message : String(err) })
)
.finally(() => setLoading(false))
}
useEffect(() => {
if (!open) return
refreshCatalog()
void window.api
.getSettings()
.then((s) => {
setPrompt(s.systemPrompt)
setPromptDirty(false)
})
.catch(() => undefined)
setNotice(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
const currentProvider = sessionState?.model?.provider
const currentModel = sessionState?.model?.id
const filtered = useMemo(() => {
if (!catalog) return []
const q = search.trim().toLowerCase()
return catalog.providers
.map((p) => ({
...p,
models: q
? p.models.filter(
(m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
)
: p.models
}))
.filter((p) => p.models.length > 0 || (!q && p.models.length === 0))
}, [catalog, search])
const run = (label: string, fn: () => Promise<void>, after?: () => void): void => {
setBusy(true)
setNotice(null)
fn()
.then(() => {
setNotice({ kind: 'ok', text: label })
after?.()
})
.catch((err: unknown) => setNotice({ kind: 'err', text: err instanceof Error ? err.message : String(err) }))
.finally(() => setBusy(false))
}
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div
className="flex h-[600px] w-[720px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-zinc-800 bg-zinc-950 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* 头部 */}
<div className="flex items-center gap-1 border-b border-zinc-800/80 px-5 py-3">
<div className="mr-3 text-[14px] font-medium text-zinc-100">设置</div>
{(
[
['model', '模型与密钥', Cpu],
['prompt', '系统提示词', Sparkles]
] as const
).map(([id, label, Icon]) => (
<button
key={id}
type="button"
onClick={() => setTab(id)}
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12.5px] transition-colors ${
tab === id ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-500 hover:bg-zinc-800/50 hover:text-zinc-300'
}`}
>
<Icon size={13} />
{label}
</button>
))}
<button
type="button"
onClick={onClose}
className="ml-auto rounded-lg p-1.5 text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<X size={16} />
</button>
</div>
{/* 内容 */}
{tab === 'model' ? (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex items-center gap-2 px-5 pb-2 pt-3">
<div className="relative flex-1">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索模型 / 服务商…"
className="w-full rounded-lg border border-zinc-800 bg-zinc-900/60 py-1.5 pl-8 pr-3 text-[12.5px] text-zinc-200 outline-none placeholder:text-zinc-600 focus:border-zinc-700"
/>
</div>
{loading && <span className="text-[11px] text-zinc-600">加载中…</span>}
</div>
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-5 pb-4">
{filtered.map((provider) => (
<div key={provider.id}>
<div className="mb-1.5 flex items-center gap-2">
<span className="text-[12px] font-medium text-zinc-300">{provider.name}</span>
<ProviderBadge provider={provider} />
<button
type="button"
onClick={() => {
setKeyProvider(provider.id)
setKeyValue('')
}}
className="ml-auto flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-300"
>
<KeyRound size={11} />
{provider.hasApiKey ? '更换密钥' : '设置密钥'}
</button>
{(provider.hasApiKey || provider.isOAuth) && (
<button
type="button"
disabled={busy}
onClick={() =>
run('已移除凭据', () => window.api.logoutProvider(provider.id), refreshCatalog)
}
className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-red-950/60 hover:text-red-300"
>
<LogOut size={11} />
移除
</button>
)}
</div>
{keyProvider === provider.id && (
<div className="mb-2 flex items-center gap-2 rounded-lg border border-zinc-800 bg-zinc-900/50 p-2">
<input
type="password"
value={keyValue}
onChange={(e) => setKeyValue(e.target.value)}
placeholder="输入 API Key(保存到 ~/.pi/agent/auth.json,与 pi CLI 共享)"
className="flex-1 rounded-md border border-zinc-800 bg-zinc-950 px-2.5 py-1.5 font-mono text-[12px] text-zinc-200 outline-none focus:border-emerald-800"
/>
<button
type="button"
disabled={busy || !keyValue.trim()}
onClick={() =>
run('密钥已保存', () => window.api.setApiKey(provider.id, keyValue.trim()), () => {
setKeyProvider(null)
setKeyValue('')
refreshCatalog()
})
}
className="flex items-center gap-1 rounded-md bg-emerald-600 px-2.5 py-1.5 text-[12px] text-white transition-colors hover:bg-emerald-500 disabled:opacity-50"
>
<Check size={12} />
保存
</button>
<button
type="button"
onClick={() => {
setKeyProvider(null)
setKeyValue('')
}}
className="rounded-md px-2 py-1.5 text-[12px] text-zinc-500 hover:bg-zinc-800"
>
取消
</button>
</div>
)}
<div className="grid grid-cols-1 gap-1">
{provider.models.map((m) => {
const selected = provider.id === currentProvider && m.id === currentModel
return (
<button
key={m.id}
type="button"
disabled={busy}
onClick={() => run(`已切换到 ${m.name}`, () => window.api.setModel(provider.id, m.id))}
className={`flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors ${
selected ? 'bg-emerald-950/50 ring-1 ring-emerald-900/60' : 'hover:bg-zinc-800/50'
}`}
>
{selected ? (
<Check size={13} className="shrink-0 text-emerald-400" />
) : (
<span className="h-[13px] w-[13px] shrink-0 rounded-full border border-zinc-700" />
)}
<span className={`text-[12.5px] ${selected ? 'text-emerald-100' : 'text-zinc-300'}`}>
{m.name}
</span>
<span className="font-mono text-[10.5px] text-zinc-600">{m.id}</span>
<span className="ml-auto flex items-center gap-2 text-[10.5px] text-zinc-600">
{m.reasoning && <span>推理</span>}
<span>{formatContext(m.contextWindow)} ctx</span>
</span>
</button>
)
})}
{provider.models.length === 0 && (
<div className="px-2.5 py-2 text-[11.5px] text-zinc-600">配置密钥后显示可用模型</div>
)}
</div>
</div>
))}
{!loading && filtered.length === 0 && (
<div className="py-10 text-center text-[12.5px] text-zinc-600">没有匹配的模型</div>
)}
</div>
</div>
) : (
<div className="flex min-h-0 flex-1 flex-col px-5 py-4">
<div className="mb-2 text-[12px] leading-5 text-zinc-500">
覆盖 pi 默认系统提示词。留空使用默认(面向编码的提示词)。保存后运行时重建,当前最近会话的历史会保留。
</div>
<textarea
value={prompt}
onChange={(e) => {
setPrompt(e.target.value)
setPromptDirty(true)
}}
spellCheck={false}
placeholder={'例如:你是店铺运营助手,负责热点词分析、商品文案生成与上架流程编排…'}
className="min-h-0 flex-1 resize-none rounded-xl border border-zinc-800 bg-zinc-900/60 p-3.5 font-mono text-[12.5px] leading-6 text-zinc-200 outline-none placeholder:text-zinc-700 focus:border-zinc-700"
/>
<div className="mt-3 flex items-center justify-end">
<button
type="button"
disabled={busy || !promptDirty}
onClick={() =>
run('系统提示词已保存,运行时已重建', () => window.api.setSystemPrompt(prompt), () =>
setPromptDirty(false)
)
}
className="flex items-center gap-1.5 rounded-lg bg-emerald-600 px-3.5 py-1.5 text-[12.5px] text-white transition-colors hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-50"
>
<Check size={13} />
保存并应用
</button>
</div>
</div>
)}
{/* 通知条 */}
{notice && (
<div
className={`border-t px-5 py-2 text-[12px] ${
notice.kind === 'ok'
? 'border-emerald-900/50 bg-emerald-950/40 text-emerald-300'
: 'border-red-900/50 bg-red-950/40 text-red-300'
}`}
>
{notice.text}
</div>
)}
</div>
</div>
)
}
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import type { JSX } from 'react'
import { Check, Cpu, MessageSquare, Pencil, Store, Trash2, X } from 'lucide-react'
import { Check, Cpu, MessageSquare, Pencil, Settings, Store, Trash2, X } from 'lucide-react'
import type { SessionMeta, SessionState, SessionStatsInfo } from '@shared/types'
interface VersionInfo {
@@ -105,7 +105,8 @@ export function Sidebar({
onNewSession,
onOpenSession,
onRenameSession,
onDeleteSession
onDeleteSession,
onOpenSettings
}: {
sessionState: SessionState | null
stats: SessionStatsInfo | null
@@ -115,6 +116,7 @@ export function Sidebar({
onOpenSession: (path: string) => void
onRenameSession: (name: string) => void
onDeleteSession: (path: string) => void
onOpenSettings: () => void
}): JSX.Element {
const [versions, setVersions] = useState<VersionInfo | null>(null)
const [renaming, setRenaming] = useState(false)
@@ -293,6 +295,14 @@ export function Sidebar({
<div className="text-[10.5px] text-zinc-600">{sessionState?.model ? `${sessionState.model.provider}` : '—'}</div>
</div>
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${connected ? 'bg-emerald-400' : 'bg-zinc-600'}`} />
<button
type="button"
onClick={onOpenSettings}
title="设置(模型 / 密钥 / 系统提示词)"
className="shrink-0 rounded-md p-1 text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<Settings size={13} />
</button>
</div>
{stats && (
<div className="mt-2.5 flex items-center justify-between text-[10.5px] text-zinc-600">
@@ -123,6 +123,8 @@ function reducer(state: AgentUiState, action: Action): AgentUiState {
switch (event.type) {
case 'session_ready':
return { ...state, ready: true, sessionState: event.state }
case 'state':
return { ...state, ready: true, sessionState: event.state }
case 'session_error':
return { ...state, sessionError: event.message }
case 'session_switched':
+16 -2
View File
@@ -1,6 +1,14 @@
import type { AgentUiEvent, ModelInfo, SessionMeta, SessionState, SessionStatsInfo } from '@shared/types'
import type {
AgentUiEvent,
AppSettings,
ModelCatalog,
ModelInfo,
SessionMeta,
SessionState,
SessionStatsInfo
} from '@shared/types'
export type { AgentUiEvent, ModelInfo, SessionMeta, SessionState, SessionStatsInfo }
export type { AgentUiEvent, AppSettings, ModelCatalog, ModelInfo, SessionMeta, SessionState, SessionStatsInfo }
declare global {
interface Window {
@@ -15,6 +23,12 @@ declare global {
openSession: (path: string) => Promise<void>
renameSession: (name: string) => Promise<void>
deleteSession: (path: string) => Promise<void>
getModelCatalog: () => Promise<ModelCatalog>
setModel: (providerId: string, modelId: string) => Promise<void>
setApiKey: (providerId: string, apiKey: string) => Promise<void>
logoutProvider: (providerId: string) => Promise<void>
getSettings: () => Promise<AppSettings>
setSystemPrompt: (text: string) => Promise<void>
onEvent: (callback: (event: AgentUiEvent) => void) => () => void
}
}
+29
View File
@@ -35,6 +35,34 @@ export interface SessionMeta {
messageCount: number
}
/** 模型目录条目 */
export interface ModelMeta {
id: string
name: string
contextWindow: number
reasoning: boolean
}
export interface ProviderCatalogEntry {
id: string
name: string
/** 已配置 api_key 凭据 */
hasApiKey: boolean
/** 已配置 oauth 凭据 */
isOAuth: boolean
models: ModelMeta[]
}
export interface ModelCatalog {
providers: ProviderCatalogEntry[]
}
/** 应用级设置(存 userData/aso-settings.json) */
export interface AppSettings {
/** 覆盖 pi 默认系统提示词(空串 = 不覆盖) */
systemPrompt: string
}
/**
* 转发到渲染进程的 agent 事件。
* 已归一化为可结构化克隆的普通 JSON(message 原样透传,渲染层防御性读取)。
@@ -43,6 +71,7 @@ export type AgentUiEvent =
| { type: 'session_ready'; state: SessionState }
| { type: 'session_error'; message: string }
| { type: 'session_switched'; state: SessionState; messages: unknown[]; activeTools: Record<string, unknown> }
| { type: 'state'; state: SessionState }
| { type: 'agent_start' }
| { type: 'agent_end' }
| { type: 'agent_settled' }