mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
refactor(ai): migrate streaming to shared HTTP SSE
Squash Phase 2-2 streaming migration: frontend stream service, LLM SSE route, Agent SSE route, and IPC/preload cleanup.
This commit is contained in:
1 parent
769115240e
commit
1af644e4ab
21 files changed
+888
-1211
No files matched your search
@@ -1,17 +1,14 @@
|
||||
/**
|
||||
* AI 相关 API — 仅保留 IPC 必须的能力
|
||||
*
|
||||
* Conversation/message CRUD 和 debug 已迁移到 HTTP 共享路由(FetchAIAdapter),
|
||||
* Conversation/message CRUD 已迁移到 HTTP 共享路由(FetchAIAdapter)。
|
||||
* LLM/Agent streaming 已迁移到 SSE 共享路由(useAgentStreamService/useLlmStreamService)。
|
||||
* 此处只保留需要 worker、native shell、工具注册表等 IPC 才能提供的功能。
|
||||
*/
|
||||
import { ipcRenderer } from 'electron'
|
||||
import type { ExportProgress } from '../../../../src/types/base'
|
||||
|
||||
// Agent API 类型 — 从 shared/types 统一导入
|
||||
export type { TokenUsage, AgentRuntimeStatus } from '../../shared/types'
|
||||
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../shared/types'
|
||||
|
||||
export type { SerializedErrorInfo } from '../../shared/types'
|
||||
export type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../shared/types'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@@ -25,42 +22,6 @@ export interface ChatOptions {
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
export interface ChatStreamChunk {
|
||||
content: string
|
||||
isFinished: boolean
|
||||
finishReason?: 'stop' | 'length' | 'error'
|
||||
error?: string
|
||||
thinking?: string
|
||||
thinkingDone?: boolean
|
||||
}
|
||||
|
||||
export interface AgentStreamChunk {
|
||||
type: 'content' | 'think' | 'tool_start' | 'tool_result' | 'status' | 'compression_done' | 'done' | 'error'
|
||||
content?: string
|
||||
thinkTag?: string
|
||||
thinkDurationMs?: number
|
||||
toolName?: string
|
||||
toolParams?: Record<string, unknown>
|
||||
toolResult?: unknown
|
||||
status?: AgentRuntimeStatus
|
||||
error?: SerializedErrorInfo
|
||||
isFinished?: boolean
|
||||
compressionResult?: {
|
||||
summaryContent: string
|
||||
tokensBefore: number
|
||||
tokensAfter: number
|
||||
timestamp: number
|
||||
}
|
||||
usage?: TokenUsage
|
||||
}
|
||||
|
||||
export interface AgentResult {
|
||||
content: string
|
||||
toolsUsed: string[]
|
||||
toolRounds: number
|
||||
error?: SerializedErrorInfo
|
||||
}
|
||||
|
||||
export interface DesensitizeRule {
|
||||
id: string
|
||||
label: string
|
||||
@@ -98,24 +59,6 @@ export interface PreprocessConfig {
|
||||
anonymizeNames: boolean
|
||||
}
|
||||
|
||||
export interface ToolContext {
|
||||
sessionId: string
|
||||
conversationId?: string
|
||||
historyLeafMessageId?: string | null
|
||||
timeFilter?: { startTs: number; endTs: number }
|
||||
maxMessagesLimit?: number
|
||||
ownerInfo?: { platformId: string; displayName: string }
|
||||
mentionedMembers?: Array<{
|
||||
memberId: number
|
||||
platformId: string
|
||||
displayName: string
|
||||
aliases: string[]
|
||||
mentionText: string
|
||||
}>
|
||||
locale?: string
|
||||
preprocessConfig?: PreprocessConfig
|
||||
}
|
||||
|
||||
// ==================== AI API (IPC-only subset) ====================
|
||||
|
||||
export const aiApi = {
|
||||
@@ -283,8 +226,7 @@ export const aiApi = {
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== LLM API ====================
|
||||
|
||||
// LLM chat (non-streaming) still uses IPC for Electron-specific config resolution
|
||||
export const llmApi = {
|
||||
chat: (
|
||||
messages: ChatMessage[],
|
||||
@@ -292,198 +234,4 @@ export const llmApi = {
|
||||
): Promise<{ success: boolean; content?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('llm:chat', messages, options)
|
||||
},
|
||||
|
||||
chatStream: (
|
||||
messages: ChatMessage[],
|
||||
options?: ChatOptions,
|
||||
onChunk?: (chunk: ChatStreamChunk) => void
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = `llm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
console.log('[preload] chatStream 开始,requestId:', requestId)
|
||||
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { requestId: string; chunk: ChatStreamChunk; error?: string }
|
||||
) => {
|
||||
if (data.requestId === requestId) {
|
||||
if (data.error) {
|
||||
console.log('[preload] chatStream 收到错误:', data.error)
|
||||
if (onChunk) {
|
||||
onChunk({ content: '', isFinished: true, finishReason: 'error', error: data.error })
|
||||
}
|
||||
ipcRenderer.removeListener('llm:streamChunk', handler)
|
||||
resolve({ success: false, error: data.error })
|
||||
} else {
|
||||
if (onChunk) {
|
||||
onChunk(data.chunk)
|
||||
}
|
||||
|
||||
if (data.chunk.isFinished) {
|
||||
console.log('[preload] chatStream 完成,requestId:', requestId)
|
||||
ipcRenderer.removeListener('llm:streamChunk', handler)
|
||||
resolve({ success: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on('llm:streamChunk', handler)
|
||||
|
||||
ipcRenderer
|
||||
.invoke('llm:chatStream', requestId, messages, options)
|
||||
.then((result) => {
|
||||
console.log('[preload] chatStream invoke 返回:', result)
|
||||
if (!result.success) {
|
||||
ipcRenderer.removeListener('llm:streamChunk', handler)
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[preload] chatStream invoke 错误:', error)
|
||||
ipcRenderer.removeListener('llm:streamChunk', handler)
|
||||
resolve({ success: false, error: String(error) })
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== Agent API ====================
|
||||
|
||||
export const agentApi = {
|
||||
runStream: (
|
||||
userMessage: string,
|
||||
context: ToolContext,
|
||||
onChunk?: (chunk: AgentStreamChunk) => void,
|
||||
chatType?: 'group' | 'private',
|
||||
locale?: string,
|
||||
assistantId?: string,
|
||||
skillId?: string | null,
|
||||
enableAutoSkill?: boolean,
|
||||
compressionConfig?: {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
maxToolResultPercent?: number
|
||||
},
|
||||
thinkingLevel?: string
|
||||
): {
|
||||
requestId: string
|
||||
promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }>
|
||||
} => {
|
||||
const sanitizedContext: ToolContext = {
|
||||
sessionId: context.sessionId,
|
||||
conversationId: context.conversationId,
|
||||
historyLeafMessageId: context.historyLeafMessageId,
|
||||
timeFilter: context.timeFilter
|
||||
? {
|
||||
startTs: context.timeFilter.startTs,
|
||||
endTs: context.timeFilter.endTs,
|
||||
}
|
||||
: undefined,
|
||||
maxMessagesLimit: context.maxMessagesLimit,
|
||||
ownerInfo: context.ownerInfo
|
||||
? {
|
||||
platformId: context.ownerInfo.platformId,
|
||||
displayName: context.ownerInfo.displayName,
|
||||
}
|
||||
: undefined,
|
||||
mentionedMembers: context.mentionedMembers
|
||||
? context.mentionedMembers.map((member) => ({
|
||||
memberId: member.memberId,
|
||||
platformId: member.platformId,
|
||||
displayName: member.displayName,
|
||||
aliases: [...member.aliases],
|
||||
mentionText: member.mentionText,
|
||||
}))
|
||||
: undefined,
|
||||
locale: context.locale,
|
||||
preprocessConfig: context.preprocessConfig,
|
||||
}
|
||||
|
||||
const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
console.log(
|
||||
'[preload] Agent runStream 开始,requestId:',
|
||||
requestId,
|
||||
'conversationId:',
|
||||
sanitizedContext.conversationId ?? 'none',
|
||||
'chatType:',
|
||||
chatType ?? 'group'
|
||||
)
|
||||
|
||||
const promise = new Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }>((resolve) => {
|
||||
const chunkHandler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { requestId: string; chunk: AgentStreamChunk }
|
||||
) => {
|
||||
if (data.requestId === requestId) {
|
||||
if (onChunk) {
|
||||
onChunk(data.chunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const completeHandler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { requestId: string; result: AgentResult & { error?: SerializedErrorInfo } }
|
||||
) => {
|
||||
if (data.requestId === requestId) {
|
||||
console.log('[preload] Agent 完成,requestId:', requestId, 'hasError:', !!data.result?.error)
|
||||
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
|
||||
ipcRenderer.removeListener('agent:complete', completeHandler)
|
||||
if (data.result?.error) {
|
||||
resolve({ success: false, error: data.result.error })
|
||||
} else {
|
||||
resolve({ success: true, result: data.result })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on('agent:streamChunk', chunkHandler)
|
||||
ipcRenderer.on('agent:complete', completeHandler)
|
||||
|
||||
ipcRenderer
|
||||
.invoke(
|
||||
'agent:runStream',
|
||||
requestId,
|
||||
userMessage,
|
||||
sanitizedContext,
|
||||
chatType,
|
||||
locale,
|
||||
assistantId,
|
||||
skillId,
|
||||
enableAutoSkill,
|
||||
compressionConfig,
|
||||
thinkingLevel
|
||||
)
|
||||
.then((result) => {
|
||||
console.log('[preload] Agent invoke 返回:', result)
|
||||
if (!result.success) {
|
||||
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
|
||||
ipcRenderer.removeListener('agent:complete', completeHandler)
|
||||
resolve(result)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[preload] Agent invoke 错误:', error)
|
||||
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
|
||||
ipcRenderer.removeListener('agent:complete', completeHandler)
|
||||
resolve({
|
||||
success: false,
|
||||
error: {
|
||||
name: error instanceof Error ? error.name : null,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? (error.stack ?? null) : null,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return { requestId, promise }
|
||||
},
|
||||
|
||||
abort: (requestId: string): Promise<{ success: boolean; error?: string }> => {
|
||||
console.log('[preload] Agent abort 请求,requestId:', requestId)
|
||||
return ipcRenderer.invoke('agent:abort', requestId)
|
||||
},
|
||||
}
|
||||
Vendored
+3
-94
@@ -254,63 +254,16 @@ interface LLMChatOptions {
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
interface LLMChatStreamChunk {
|
||||
content: string
|
||||
isFinished: boolean
|
||||
finishReason?: 'stop' | 'length' | 'error'
|
||||
error?: string
|
||||
thinking?: string
|
||||
thinkingDone?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* HTTP service layer via FetchLLMAdapter. Streaming chat uses shared SSE route.
|
||||
* Only non-streaming chat remains on IPC.
|
||||
*/
|
||||
interface LlmApi {
|
||||
chat: (
|
||||
messages: LLMChatMessage[],
|
||||
options?: LLMChatOptions
|
||||
) => Promise<{ success: boolean; content?: string; error?: string }>
|
||||
chatStream: (
|
||||
messages: LLMChatMessage[],
|
||||
options?: LLMChatOptions,
|
||||
onChunk?: (chunk: LLMChatStreamChunk) => void
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
}
|
||||
|
||||
// TokenUsage & AgentRuntimeStatus — imported from electron/shared/types.ts
|
||||
|
||||
// Agent 相关类型
|
||||
interface AgentStreamChunk {
|
||||
type: 'content' | 'think' | 'tool_start' | 'tool_result' | 'status' | 'compression_done' | 'done' | 'error'
|
||||
content?: string
|
||||
thinkTag?: string
|
||||
thinkDurationMs?: number
|
||||
toolName?: string
|
||||
toolParams?: Record<string, unknown>
|
||||
toolResult?: unknown
|
||||
status?: AgentRuntimeStatus
|
||||
error?: SerializedErrorInfo
|
||||
isFinished?: boolean
|
||||
compressionResult?: {
|
||||
summaryContent: string
|
||||
tokensBefore: number
|
||||
tokensAfter: number
|
||||
timestamp: number
|
||||
}
|
||||
/** Token 使用量(type=done 时返回累计值) */
|
||||
usage?: TokenUsage
|
||||
}
|
||||
|
||||
interface AgentResult {
|
||||
content: string
|
||||
toolsUsed: string[]
|
||||
toolRounds: number
|
||||
/** 总 Token 使用量(累计所有 LLM 调用) */
|
||||
totalUsage?: TokenUsage
|
||||
error?: SerializedErrorInfo
|
||||
}
|
||||
|
||||
/** Owner 信息(当前用户在对话中的身份) */
|
||||
@@ -362,50 +315,7 @@ interface PreprocessConfig {
|
||||
anonymizeNames: boolean
|
||||
}
|
||||
|
||||
interface ToolContext {
|
||||
sessionId: string
|
||||
conversationId?: string
|
||||
historyLeafMessageId?: string | null
|
||||
timeFilter?: { startTs: number; endTs: number }
|
||||
/** 用户配置:每次发送给 AI 的最大消息条数 */
|
||||
maxMessagesLimit?: number
|
||||
/** Owner 信息(当前用户在对话中的身份) */
|
||||
ownerInfo?: OwnerInfo
|
||||
/** 本轮显式 @ 的成员 */
|
||||
mentionedMembers?: Array<{
|
||||
memberId: number
|
||||
platformId: string
|
||||
displayName: string
|
||||
aliases: string[]
|
||||
mentionText: string
|
||||
}>
|
||||
/** 语言环境 */
|
||||
locale?: string
|
||||
/** 聊天记录预处理配置 */
|
||||
preprocessConfig?: PreprocessConfig
|
||||
}
|
||||
|
||||
interface AgentApi {
|
||||
runStream: (
|
||||
userMessage: string,
|
||||
context: ToolContext,
|
||||
onChunk?: (chunk: AgentStreamChunk) => void,
|
||||
chatType?: 'group' | 'private',
|
||||
locale?: string,
|
||||
assistantId?: string,
|
||||
skillId?: string | null,
|
||||
enableAutoSkill?: boolean,
|
||||
compressionConfig?: {
|
||||
enabled: boolean
|
||||
tokenThresholdPercent: number
|
||||
bufferSizePercent: number
|
||||
maxToolResultPercent?: number
|
||||
},
|
||||
thinkingLevel?: string
|
||||
) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }> }
|
||||
abort: (requestId: string) => Promise<{ success: boolean; error?: string }>
|
||||
}
|
||||
|
||||
// Agent streaming migrated to shared SSE route (useAgentStreamService)
|
||||
// Assistant CRUD migrated to HTTP service layer (FetchAssistantAdapter)
|
||||
// Skill CRUD migrated to HTTP service layer (FetchSkillAdapter)
|
||||
|
||||
@@ -615,7 +525,6 @@ declare global {
|
||||
chatApi: ChatApi
|
||||
aiApi: AiApi
|
||||
llmApi: LlmApi
|
||||
agentApi: AgentApi
|
||||
cacheApi: CacheApi
|
||||
networkApi: NetworkApi
|
||||
sessionApi: SessionApi
|
||||
|
||||
@@ -8,7 +8,7 @@ import { electronAPI } from '@electron-toolkit/preload'
|
||||
// 从拆分的模块导入 API
|
||||
import { extendedApi } from './apis/core'
|
||||
import { chatApi } from './apis/chat'
|
||||
import { aiApi, llmApi, agentApi } from './apis/ai'
|
||||
import { aiApi } from './apis/ai'
|
||||
import { networkApi, cacheApi, sessionApi } from './apis/utils'
|
||||
import { apiServerApi } from './apis/api-server'
|
||||
import { internalApi } from './apis/internal-api'
|
||||
@@ -37,8 +37,6 @@ if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('api', extendedApi)
|
||||
contextBridge.exposeInMainWorld('chatApi', chatApi)
|
||||
contextBridge.exposeInMainWorld('aiApi', aiApi)
|
||||
contextBridge.exposeInMainWorld('llmApi', llmApi)
|
||||
contextBridge.exposeInMainWorld('agentApi', agentApi)
|
||||
contextBridge.exposeInMainWorld('cacheApi', cacheApi)
|
||||
contextBridge.exposeInMainWorld('networkApi', networkApi)
|
||||
contextBridge.exposeInMainWorld('sessionApi', sessionApi)
|
||||
@@ -57,10 +55,6 @@ if (process.contextIsolated) {
|
||||
// @ts-ignore (define in dts)
|
||||
window.aiApi = aiApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.llmApi = llmApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.agentApi = agentApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.cacheApi = cacheApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.networkApi = networkApi
|
||||
|
||||
Reference in new issue
Block a user