mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): migrate AI CRUD routes to shared package and establish frontend service layer
Extract LLM config, assistant, skill, conversation and summary CRUD
routes from CLI-specific ai.ts into @openchatlab/http-routes, enabling
Electron Internal Server to serve AI endpoints via shared routes.
Frontend service layer:
- Create src/services/{llm,assistant,skill}/ with typed adapters
- Migrate all window.llmApi/assistantApi/skillApi calls to service layer
- Clean up Electron preload: remove CRUD bridges, keep only streaming IPC
Shared AI routes (packages/http-routes):
- ai-llm: config CRUD, provider registry, model catalog, custom store
- ai-assistants: CRUD, import/reimport, tool catalog
- ai-skills: CRUD, import/reimport, builtin catalog
- ai-conversations: conversation/message/debug CRUD
- ai-summaries: summary generation with LLM config injection
- Add requireAi fail-fast option to registerSharedRoutes
Backend improvements:
- Extract CustomProviderStore/CustomModelStore to node-runtime
- Fix authProfile persistence (onApiKeyCreated returns profileName)
- Reuse AI manager singletons in Electron (avoid dual instances)
- Prevent plaintext API key leakage in addConfig response
- Add authProfile regression tests
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
09a7d7c5e2
commit
3ab162162b
42 files changed
+1599
-1305
No files matched your search
+11
-324
@@ -54,15 +54,6 @@ export interface AIMessage {
|
||||
tokenUsage?: TokenUsageData
|
||||
}
|
||||
|
||||
// LLM API 类型
|
||||
export interface LLMProvider {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
defaultBaseUrl: string
|
||||
models: Array<{ id: string; name: string; description?: string }>
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant'
|
||||
content: string
|
||||
@@ -175,22 +166,6 @@ export interface ToolContext {
|
||||
preprocessConfig?: PreprocessConfig
|
||||
}
|
||||
|
||||
// AI 服务配置类型(前端用)
|
||||
export interface AIServiceConfigDisplay {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
apiKey: string // 脱敏后的 API Key
|
||||
apiKeySet: boolean
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
maxTokens?: number
|
||||
apiFormat?: string
|
||||
customModels?: Array<{ id: string; name: string }>
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
// ==================== AI API ====================
|
||||
|
||||
export const aiApi = {
|
||||
@@ -530,162 +505,12 @@ export const aiApi = {
|
||||
// ==================== LLM API ====================
|
||||
|
||||
export const llmApi = {
|
||||
// ==================== Provider Registry / Model Catalog ====================
|
||||
|
||||
getProviderRegistry: () => {
|
||||
return ipcRenderer.invoke('llm:getProviderRegistry')
|
||||
},
|
||||
|
||||
getModelCatalog: () => {
|
||||
return ipcRenderer.invoke('llm:getModelCatalog')
|
||||
},
|
||||
|
||||
addCustomProvider: (input: Record<string, unknown>) => {
|
||||
return ipcRenderer.invoke('llm:addCustomProvider', input)
|
||||
},
|
||||
|
||||
updateCustomProvider: (id: string, updates: Record<string, unknown>) => {
|
||||
return ipcRenderer.invoke('llm:updateCustomProvider', id, updates)
|
||||
},
|
||||
|
||||
deleteCustomProvider: (id: string) => {
|
||||
return ipcRenderer.invoke('llm:deleteCustomProvider', id)
|
||||
},
|
||||
|
||||
addCustomModel: (input: Record<string, unknown>) => {
|
||||
return ipcRenderer.invoke('llm:addCustomModel', input)
|
||||
},
|
||||
|
||||
updateCustomModel: (providerId: string, modelId: string, updates: Record<string, unknown>) => {
|
||||
return ipcRenderer.invoke('llm:updateCustomModel', providerId, modelId, updates)
|
||||
},
|
||||
|
||||
deleteCustomModel: (providerId: string, modelId: string) => {
|
||||
return ipcRenderer.invoke('llm:deleteCustomModel', providerId, modelId)
|
||||
},
|
||||
|
||||
/** @deprecated 使用 getProviderRegistry 代替 */
|
||||
getProviders: (): Promise<LLMProvider[]> => {
|
||||
return ipcRenderer.invoke('llm:getProviders')
|
||||
},
|
||||
|
||||
// ==================== 多配置管理 API ====================
|
||||
|
||||
/**
|
||||
* 获取所有配置列表
|
||||
* LLM CRUD (config, provider, model, validate, etc.) has been migrated to
|
||||
* HTTP service layer via FetchLLMAdapter. Only streaming / non-streaming
|
||||
* chat APIs remain on IPC because they require real-time event forwarding.
|
||||
*/
|
||||
getAllConfigs: (): Promise<AIServiceConfigDisplay[]> => {
|
||||
return ipcRenderer.invoke('llm:getAllConfigs')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取默认助手 slot(configId + modelId)
|
||||
*/
|
||||
getDefaultAssistantSlot: (): Promise<{ configId: string; modelId: string } | null> => {
|
||||
return ipcRenderer.invoke('llm:getDefaultAssistantSlot')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取快速模型 slot
|
||||
*/
|
||||
getFastModelSlot: (): Promise<{ configId: string; modelId: string } | null> => {
|
||||
return ipcRenderer.invoke('llm:getFastModelSlot')
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加新配置
|
||||
*/
|
||||
addConfig: (config: {
|
||||
name: string
|
||||
provider: string
|
||||
apiKey: string
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
maxTokens?: number
|
||||
apiFormat?: string
|
||||
customModels?: Array<{ id: string; name: string }>
|
||||
}): Promise<{ success: boolean; config?: AIServiceConfigDisplay; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:addConfig', config)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig: (
|
||||
id: string,
|
||||
updates: {
|
||||
name?: string
|
||||
provider?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
maxTokens?: number
|
||||
apiFormat?: string
|
||||
customModels?: Array<{ id: string; name: string }>
|
||||
}
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:updateConfig', id, updates)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
deleteConfig: (id?: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:deleteConfig', id)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置默认助手模型(configId + modelId)
|
||||
*/
|
||||
setDefaultAssistantModel: (configId: string, modelId: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:setDefaultAssistantModel', configId, modelId)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置快速模型
|
||||
*/
|
||||
setFastModel: (slot: { configId: string; modelId: string } | null): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:setFastModel', slot)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证 API Key(支持自定义 baseUrl 和 model)
|
||||
*/
|
||||
validateApiKey: (
|
||||
provider: string,
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
model?: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:validateApiKey', provider, apiKey, baseUrl, model)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取远程模型列表
|
||||
*/
|
||||
fetchRemoteModels: (
|
||||
provider: string,
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
apiFormat?: string
|
||||
): Promise<{
|
||||
success: boolean
|
||||
models?: Array<{ id: string; name: string; ownedBy?: string; contextWindow?: number }>
|
||||
error?: string
|
||||
}> => {
|
||||
return ipcRenderer.invoke('llm:fetchRemoteModels', provider, apiKey, baseUrl, apiFormat)
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否已配置 LLM(是否有激活的配置)
|
||||
*/
|
||||
hasConfig: (): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('llm:hasConfig')
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送 LLM 聊天请求(非流式)
|
||||
*/
|
||||
chat: (
|
||||
messages: ChatMessage[],
|
||||
options?: ChatOptions
|
||||
@@ -693,10 +518,6 @@ export const llmApi = {
|
||||
return ipcRenderer.invoke('llm:chat', messages, options)
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送 LLM 聊天请求(流式)
|
||||
* 返回一个 Promise,该 Promise 在流完成后才 resolve
|
||||
*/
|
||||
chatStream: (
|
||||
messages: ChatMessage[],
|
||||
options?: ChatOptions,
|
||||
@@ -755,149 +576,15 @@ export const llmApi = {
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== Assistant API ====================
|
||||
/**
|
||||
* Assistant CRUD has been migrated to HTTP service layer via FetchAssistantAdapter.
|
||||
* The preload bridge is no longer needed.
|
||||
*/
|
||||
|
||||
export interface AssistantSummary {
|
||||
id: string
|
||||
name: string
|
||||
systemPrompt: string
|
||||
presetQuestions: string[]
|
||||
builtinId?: string
|
||||
applicableChatTypes?: ('group' | 'private')[]
|
||||
supportedLocales?: string[]
|
||||
}
|
||||
|
||||
export interface AssistantConfigFull {
|
||||
id: string
|
||||
name: string
|
||||
systemPrompt: string
|
||||
presetQuestions: string[]
|
||||
allowedBuiltinTools?: string[]
|
||||
builtinId?: string
|
||||
applicableChatTypes?: ('group' | 'private')[]
|
||||
supportedLocales?: string[]
|
||||
}
|
||||
|
||||
export interface BuiltinAssistantInfo {
|
||||
id: string
|
||||
name: string
|
||||
systemPrompt: string
|
||||
applicableChatTypes?: ('group' | 'private')[]
|
||||
supportedLocales?: string[]
|
||||
imported: boolean
|
||||
}
|
||||
|
||||
export const assistantApi = {
|
||||
getAll: (): Promise<AssistantSummary[]> => {
|
||||
return ipcRenderer.invoke('assistant:getAll')
|
||||
},
|
||||
|
||||
getConfig: (id: string): Promise<AssistantConfigFull | null> => {
|
||||
return ipcRenderer.invoke('assistant:getConfig', id)
|
||||
},
|
||||
|
||||
update: (id: string, updates: Partial<AssistantConfigFull>): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:update', id, updates)
|
||||
},
|
||||
|
||||
create: (config: Omit<AssistantConfigFull, 'id'>): Promise<{ success: boolean; id?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:create', config)
|
||||
},
|
||||
|
||||
delete: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:delete', id)
|
||||
},
|
||||
|
||||
reset: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:reset', id)
|
||||
},
|
||||
|
||||
getBuiltinCatalog: (): Promise<BuiltinAssistantInfo[]> => {
|
||||
return ipcRenderer.invoke('assistant:getBuiltinCatalog')
|
||||
},
|
||||
|
||||
getBuiltinToolCatalog: (): Promise<Array<{ name: string; category: 'core' | 'analysis' }>> => {
|
||||
return ipcRenderer.invoke('assistant:getBuiltinToolCatalog')
|
||||
},
|
||||
|
||||
importAssistant: (builtinId: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:import', builtinId)
|
||||
},
|
||||
|
||||
reimportAssistant: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:reimport', id)
|
||||
},
|
||||
|
||||
importFromMd: (rawMd: string): Promise<{ success: boolean; id?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:importFromMd', rawMd)
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== Skill API ====================
|
||||
|
||||
export interface SkillSummary {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
tags: string[]
|
||||
chatScope: 'all' | 'group' | 'private'
|
||||
tools: string[]
|
||||
builtinId?: string
|
||||
}
|
||||
|
||||
export interface SkillConfigFull {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
tags: string[]
|
||||
chatScope: 'all' | 'group' | 'private'
|
||||
prompt: string
|
||||
tools: string[]
|
||||
builtinId?: string
|
||||
}
|
||||
|
||||
export interface BuiltinSkillInfo extends SkillSummary {
|
||||
imported: boolean
|
||||
hasUpdate: boolean
|
||||
}
|
||||
|
||||
export const skillApi = {
|
||||
getAll: (): Promise<SkillSummary[]> => {
|
||||
return ipcRenderer.invoke('skill:getAll')
|
||||
},
|
||||
|
||||
getConfig: (id: string): Promise<SkillConfigFull | null> => {
|
||||
return ipcRenderer.invoke('skill:getConfig', id)
|
||||
},
|
||||
|
||||
update: (id: string, rawMd: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:update', id, rawMd)
|
||||
},
|
||||
|
||||
create: (rawMd: string): Promise<{ success: boolean; id?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:create', rawMd)
|
||||
},
|
||||
|
||||
delete: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:delete', id)
|
||||
},
|
||||
|
||||
getBuiltinCatalog: (): Promise<BuiltinSkillInfo[]> => {
|
||||
return ipcRenderer.invoke('skill:getBuiltinCatalog')
|
||||
},
|
||||
|
||||
importSkill: (builtinId: string): Promise<{ success: boolean; id?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:import', builtinId)
|
||||
},
|
||||
|
||||
reimportSkill: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:reimport', id)
|
||||
},
|
||||
|
||||
importFromMd: (rawMd: string): Promise<{ success: boolean; id?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('skill:importFromMd', rawMd)
|
||||
},
|
||||
}
|
||||
/**
|
||||
* Skill CRUD has been migrated to HTTP service layer via FetchSkillAdapter.
|
||||
* The preload bridge is no longer needed.
|
||||
*/
|
||||
|
||||
// ==================== Agent API ====================
|
||||
|
||||
|
||||
Reference in new issue
Block a user