mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-12 15:08:36 +00:00
瞎改了一通,现在完全不能用了
This commit is contained in:
241
electron/main.ts
241
electron/main.ts
@@ -1690,7 +1690,104 @@ function registerIpcHandlers() {
|
|||||||
aiAnalysisService.cancelToolTest(payload?.taskId)
|
aiAnalysisService.cancelToolTest(payload?.taskId)
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle('agent:runStream', async (event, payload: {
|
ipcMain.handle('ai:getMessageContext', async (_, sessionId: string, messageIds: number | number[], contextSize?: number) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return []
|
||||||
|
return chatService.getMessageContextForAI(sessionId, messageIds, contextSize)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getSearchMessageContext', async (_, sessionId: string, messageIds: number[], contextBefore?: number, contextAfter?: number) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return []
|
||||||
|
return chatService.getSearchMessageContextForAI(sessionId, messageIds, contextBefore, contextAfter)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getRecentMessages', async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return { messages: [], total: 0 }
|
||||||
|
return chatService.getRecentMessagesForAI(sessionId, filter, limit)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getAllRecentMessages', async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return { messages: [], total: 0 }
|
||||||
|
return chatService.getRecentMessagesForAI(sessionId, filter, limit)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getConversationBetween', async (
|
||||||
|
_,
|
||||||
|
sessionId: string,
|
||||||
|
memberId1: number,
|
||||||
|
memberId2: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number
|
||||||
|
) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||||||
|
}
|
||||||
|
return chatService.getConversationBetweenForAI(sessionId, memberId1, memberId2, filter, limit)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getMessagesBefore', async (
|
||||||
|
_,
|
||||||
|
sessionId: string,
|
||||||
|
beforeId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return { messages: [], hasMore: false }
|
||||||
|
return chatService.getMessagesBeforeForAI(sessionId, beforeId, limit, filter, senderId, keywords)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getMessagesAfter', async (
|
||||||
|
_,
|
||||||
|
sessionId: string,
|
||||||
|
afterId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return { messages: [], hasMore: false }
|
||||||
|
return chatService.getMessagesAfterForAI(sessionId, afterId, limit, filter, senderId, keywords)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:searchSessions', async (
|
||||||
|
_,
|
||||||
|
sessionId: string,
|
||||||
|
keywords?: string[],
|
||||||
|
timeFilter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number,
|
||||||
|
previewCount?: number
|
||||||
|
) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return []
|
||||||
|
return chatService.searchSessionsForAI(sessionId, keywords, timeFilter, limit, previewCount)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getSessionMessages', async (_, sessionId: string, chatSessionId: string | number, limit?: number) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return null
|
||||||
|
return chatService.getSessionMessagesForAI(sessionId, chatSessionId, limit)
|
||||||
|
})
|
||||||
|
ipcMain.handle('ai:getSessionSummaries', async (
|
||||||
|
_,
|
||||||
|
sessionId: string,
|
||||||
|
options?: { sessionIds?: string[]; limit?: number; previewCount?: number }
|
||||||
|
) => {
|
||||||
|
const connectResult = await ensureAiSqlLabConnected()
|
||||||
|
if (!connectResult.success) return []
|
||||||
|
return chatService.getSessionSummariesForAI(sessionId, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
const agentRequestToRunId = new Map<string, string>()
|
||||||
|
const terminatedAgentRequests = new Set<string>()
|
||||||
|
const markAgentRequestTerminated = (requestId: string) => {
|
||||||
|
const normalized = String(requestId || '').trim()
|
||||||
|
if (!normalized) return
|
||||||
|
terminatedAgentRequests.add(normalized)
|
||||||
|
setTimeout(() => {
|
||||||
|
terminatedAgentRequests.delete(normalized)
|
||||||
|
}, 120_000)
|
||||||
|
}
|
||||||
|
ipcMain.handle('agent:runStream', async (event, requestId: string, payload: {
|
||||||
mode?: 'chat' | 'sql'
|
mode?: 'chat' | 'sql'
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
userInput: string
|
userInput: string
|
||||||
@@ -1699,19 +1796,106 @@ function registerIpcHandlers() {
|
|||||||
chatScope?: 'group' | 'private'
|
chatScope?: 'group' | 'private'
|
||||||
sqlContext?: { schemaText?: string; targetHint?: string }
|
sqlContext?: { schemaText?: string; targetHint?: string }
|
||||||
}) => {
|
}) => {
|
||||||
return aiAgentService.runStream(payload, {
|
const normalizedRequestId = String(requestId || '').trim() || randomUUID()
|
||||||
|
terminatedAgentRequests.delete(normalizedRequestId)
|
||||||
|
const startResult = await aiAgentService.runStream(payload, {
|
||||||
onChunk: (chunk) => {
|
onChunk: (chunk) => {
|
||||||
|
if (terminatedAgentRequests.has(normalizedRequestId)) return
|
||||||
try {
|
try {
|
||||||
event.sender.send('agent:stream', chunk)
|
event.sender.send('agent:streamChunk', { requestId: normalizedRequestId, chunk })
|
||||||
|
} catch {
|
||||||
|
// ignore sender errors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFinished: (result) => {
|
||||||
|
if (terminatedAgentRequests.has(normalizedRequestId)) {
|
||||||
|
agentRequestToRunId.delete(normalizedRequestId)
|
||||||
|
terminatedAgentRequests.delete(normalizedRequestId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!result.success) {
|
||||||
|
event.sender.send('agent:error', {
|
||||||
|
requestId: normalizedRequestId,
|
||||||
|
error: result.error || '执行失败',
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
runId: result.runId,
|
||||||
|
conversationId: result.conversationId,
|
||||||
|
error: result.error || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
event.sender.send('agent:complete', {
|
||||||
|
requestId: normalizedRequestId,
|
||||||
|
result: {
|
||||||
|
success: true,
|
||||||
|
runId: result.runId,
|
||||||
|
conversationId: result.conversationId,
|
||||||
|
error: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore sender errors
|
||||||
|
} finally {
|
||||||
|
agentRequestToRunId.delete(normalizedRequestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (startResult.success && startResult.runId) {
|
||||||
|
agentRequestToRunId.set(normalizedRequestId, startResult.runId)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: Boolean(startResult.success),
|
||||||
|
requestId: normalizedRequestId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ipcMain.handle('agent:abort', async (event, payload: string | { requestId?: string; runId?: string; conversationId?: string }) => {
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
const requestId = payload
|
||||||
|
const runId = agentRequestToRunId.get(requestId) || payload
|
||||||
|
markAgentRequestTerminated(requestId)
|
||||||
|
const result = await aiAgentService.abort({ runId })
|
||||||
|
if (result?.success) {
|
||||||
|
agentRequestToRunId.delete(requestId)
|
||||||
|
try {
|
||||||
|
event.sender.send('agent:cancel', { requestId, runId })
|
||||||
} catch {
|
} catch {
|
||||||
// ignore sender errors
|
// ignore sender errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
return result
|
||||||
|
}
|
||||||
|
const requestId = String(payload?.requestId || '').trim()
|
||||||
|
if (requestId) {
|
||||||
|
const runId = agentRequestToRunId.get(requestId)
|
||||||
|
if (runId) {
|
||||||
|
markAgentRequestTerminated(requestId)
|
||||||
|
agentRequestToRunId.delete(requestId)
|
||||||
|
const result = await aiAgentService.abort({ runId })
|
||||||
|
if (result?.success) {
|
||||||
|
try {
|
||||||
|
event.sender.send('agent:cancel', { requestId, runId })
|
||||||
|
} catch {
|
||||||
|
// ignore sender errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await aiAgentService.abort(payload || {})
|
||||||
|
if (result?.success && requestId) {
|
||||||
|
markAgentRequestTerminated(requestId)
|
||||||
|
agentRequestToRunId.delete(requestId)
|
||||||
|
try {
|
||||||
|
event.sender.send('agent:cancel', { requestId, runId: String(payload?.runId || '') })
|
||||||
|
} catch {
|
||||||
|
// ignore sender errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
ipcMain.handle('agent:abort', async (_, payload: { runId?: string; conversationId?: string }) =>
|
|
||||||
aiAgentService.abort(payload || {})
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle('assistant:getAll', async () => aiAssistantService.getAll())
|
ipcMain.handle('assistant:getAll', async () => aiAssistantService.getAll())
|
||||||
ipcMain.handle('assistant:getConfig', async (_, id: string) => aiAssistantService.getConfig(id))
|
ipcMain.handle('assistant:getConfig', async (_, id: string) => aiAssistantService.getConfig(id))
|
||||||
@@ -1778,49 +1962,6 @@ function registerIpcHandlers() {
|
|||||||
return wcdbService.sqlLabExecuteReadonly(payload)
|
return wcdbService.sqlLabExecuteReadonly(payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 兼容层:旧 aiAnalysis API 转调新实现
|
|
||||||
ipcMain.handle('aiAnalysis:listConversations', async (_, payload?: { page?: number; pageSize?: number }) =>
|
|
||||||
aiAnalysisService.listConversations(payload?.page, payload?.pageSize)
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:createConversation', async (_, payload?: { title?: string }) =>
|
|
||||||
aiAnalysisService.createConversation(payload?.title || '')
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:deleteConversation', async (_, conversationId: string) =>
|
|
||||||
aiAnalysisService.deleteConversation(conversationId)
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:listMessages', async (_, payload: { conversationId: string; limit?: number }) =>
|
|
||||||
aiAnalysisService.listMessages(payload.conversationId, payload.limit)
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:sendMessage', async (event, payload: {
|
|
||||||
conversationId: string
|
|
||||||
userInput: string
|
|
||||||
options?: { parentMessageId?: string; persistUserMessage?: boolean; assistantId?: string; activeSkillId?: string }
|
|
||||||
}) =>
|
|
||||||
aiAnalysisService.sendMessage(payload.conversationId, payload.userInput, payload.options, {
|
|
||||||
onRunEvent: (runEvent) => {
|
|
||||||
try {
|
|
||||||
event.sender.send('aiAnalysis:runEvent', runEvent)
|
|
||||||
} catch {
|
|
||||||
// ignore sender errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:retryMessage', async (event, payload: { conversationId: string; userMessageId?: string }) =>
|
|
||||||
aiAnalysisService.retryMessage(payload, {
|
|
||||||
onRunEvent: (runEvent) => {
|
|
||||||
try {
|
|
||||||
event.sender.send('aiAnalysis:runEvent', runEvent)
|
|
||||||
} catch {
|
|
||||||
// ignore sender errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
ipcMain.handle('aiAnalysis:abortRun', async (_, payload: { runId?: string; conversationId?: string }) =>
|
|
||||||
aiAnalysisService.abortRun(payload || {})
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle('config:clear', async () => {
|
ipcMain.handle('config:clear', async () => {
|
||||||
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
|
||||||
const result = setSystemLaunchAtStartup(false)
|
const result = setSystemLaunchAtStartup(false)
|
||||||
|
|||||||
@@ -562,6 +562,50 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.invoke('ai:listMessages', payload),
|
ipcRenderer.invoke('ai:listMessages', payload),
|
||||||
exportConversation: (payload: { conversationId: string }) =>
|
exportConversation: (payload: { conversationId: string }) =>
|
||||||
ipcRenderer.invoke('ai:exportConversation', payload),
|
ipcRenderer.invoke('ai:exportConversation', payload),
|
||||||
|
getMessageContext: (sessionId: string, messageIds: number | number[], contextSize?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getMessageContext', sessionId, messageIds, contextSize),
|
||||||
|
getSearchMessageContext: (sessionId: string, messageIds: number[], contextBefore?: number, contextAfter?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getSearchMessageContext', sessionId, messageIds, contextBefore, contextAfter),
|
||||||
|
getRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getRecentMessages', sessionId, filter, limit),
|
||||||
|
getAllRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getAllRecentMessages', sessionId, filter, limit),
|
||||||
|
getConversationBetween: (
|
||||||
|
sessionId: string,
|
||||||
|
memberId1: number,
|
||||||
|
memberId2: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number
|
||||||
|
) => ipcRenderer.invoke('ai:getConversationBetween', sessionId, memberId1, memberId2, filter, limit),
|
||||||
|
getMessagesBefore: (
|
||||||
|
sessionId: string,
|
||||||
|
beforeId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => ipcRenderer.invoke('ai:getMessagesBefore', sessionId, beforeId, limit, filter, senderId, keywords),
|
||||||
|
getMessagesAfter: (
|
||||||
|
sessionId: string,
|
||||||
|
afterId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords),
|
||||||
|
searchSessions: (
|
||||||
|
sessionId: string,
|
||||||
|
keywords?: string[],
|
||||||
|
timeFilter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number,
|
||||||
|
previewCount?: number
|
||||||
|
) => ipcRenderer.invoke('ai:searchSessions', sessionId, keywords, timeFilter, limit, previewCount),
|
||||||
|
getSessionMessages: (sessionId: string, chatSessionId: string | number, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getSessionMessages', sessionId, chatSessionId, limit),
|
||||||
|
getSessionSummaries: (
|
||||||
|
sessionId: string,
|
||||||
|
options?: { sessionIds?: string[]; limit?: number; previewCount?: number }
|
||||||
|
) => ipcRenderer.invoke('ai:getSessionSummaries', sessionId, options),
|
||||||
getToolCatalog: () => ipcRenderer.invoke('ai:getToolCatalog'),
|
getToolCatalog: () => ipcRenderer.invoke('ai:getToolCatalog'),
|
||||||
executeTool: (payload: { name: string; args?: Record<string, any> }) =>
|
executeTool: (payload: { name: string; args?: Record<string, any> }) =>
|
||||||
ipcRenderer.invoke('ai:executeTool', payload),
|
ipcRenderer.invoke('ai:executeTool', payload),
|
||||||
@@ -578,14 +622,72 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
activeSkillId?: string
|
activeSkillId?: string
|
||||||
chatScope?: 'group' | 'private'
|
chatScope?: 'group' | 'private'
|
||||||
sqlContext?: { schemaText?: string; targetHint?: string }
|
sqlContext?: { schemaText?: string; targetHint?: string }
|
||||||
}) => ipcRenderer.invoke('agent:runStream', payload),
|
}, onChunk?: (chunk: any) => void) => {
|
||||||
abort: (payload: { runId?: string; conversationId?: string }) =>
|
const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||||
ipcRenderer.invoke('agent:abort', payload),
|
const promise = new Promise<{ success: boolean; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean }; error?: string }>((resolve) => {
|
||||||
onStream: (callback: (payload: any) => void) => {
|
let settled = false
|
||||||
const listener = (_: unknown, payload: any) => callback(payload)
|
const cleanup = () => {
|
||||||
ipcRenderer.on('agent:stream', listener)
|
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
|
||||||
return () => ipcRenderer.removeListener('agent:stream', listener)
|
ipcRenderer.removeListener('agent:cancel', cancelHandler)
|
||||||
}
|
ipcRenderer.removeListener('agent:error', errorHandler)
|
||||||
|
ipcRenderer.removeListener('agent:complete', completeHandler)
|
||||||
|
}
|
||||||
|
const settle = (value: { success: boolean; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean }; error?: string }) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
cleanup()
|
||||||
|
resolve(value)
|
||||||
|
}
|
||||||
|
const chunkHandler = (_: unknown, data: { requestId: string; chunk: any }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
if (onChunk) onChunk(data.chunk)
|
||||||
|
}
|
||||||
|
const errorHandler = (_: unknown, data: { requestId: string; error?: string; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean } }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
settle({
|
||||||
|
success: false,
|
||||||
|
error: data?.error || data?.result?.error || '执行失败',
|
||||||
|
result: data?.result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const cancelHandler = (_: unknown, data: { requestId: string; runId?: string }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
settle({
|
||||||
|
success: false,
|
||||||
|
error: '任务已取消',
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
runId: data?.runId || '',
|
||||||
|
conversationId: '',
|
||||||
|
error: '任务已取消',
|
||||||
|
canceled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const completeHandler = (_: unknown, data: { requestId: string; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean } }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
if (data?.result?.error) {
|
||||||
|
settle({ success: false, error: data.result.error, result: data.result })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settle({ success: Boolean(data?.result?.success ?? true), result: data?.result })
|
||||||
|
}
|
||||||
|
ipcRenderer.on('agent:streamChunk', chunkHandler)
|
||||||
|
ipcRenderer.on('agent:cancel', cancelHandler)
|
||||||
|
ipcRenderer.on('agent:error', errorHandler)
|
||||||
|
ipcRenderer.on('agent:complete', completeHandler)
|
||||||
|
ipcRenderer.invoke('agent:runStream', requestId, payload).then((result: { success?: boolean; error?: string }) => {
|
||||||
|
if (result?.success === false) {
|
||||||
|
settle({ success: false, error: result.error || '启动失败' })
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
settle({ success: false, error: String(error) })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return { requestId, promise }
|
||||||
|
},
|
||||||
|
abort: (payload: string | { requestId?: string; runId?: string; conversationId?: string }) =>
|
||||||
|
ipcRenderer.invoke('agent:abort', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
assistantApi: {
|
assistantApi: {
|
||||||
@@ -617,48 +719,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
listModels: () => ipcRenderer.invoke('llm:listModels')
|
listModels: () => ipcRenderer.invoke('llm:listModels')
|
||||||
},
|
},
|
||||||
|
|
||||||
aiAnalysis: {
|
|
||||||
listConversations: (payload?: { page?: number; pageSize?: number }) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:listConversations', payload),
|
|
||||||
createConversation: (payload?: { title?: string }) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:createConversation', payload),
|
|
||||||
deleteConversation: (conversationId: string) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:deleteConversation', conversationId),
|
|
||||||
listMessages: (payload: { conversationId: string; limit?: number }) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:listMessages', payload),
|
|
||||||
sendMessage: (payload: {
|
|
||||||
conversationId: string
|
|
||||||
userInput: string
|
|
||||||
options?: {
|
|
||||||
parentMessageId?: string
|
|
||||||
persistUserMessage?: boolean
|
|
||||||
assistantId?: string
|
|
||||||
activeSkillId?: string
|
|
||||||
chatScope?: 'group' | 'private'
|
|
||||||
}
|
|
||||||
}) => ipcRenderer.invoke('aiAnalysis:sendMessage', payload),
|
|
||||||
retryMessage: (payload: { conversationId: string; userMessageId?: string }) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:retryMessage', payload),
|
|
||||||
abortRun: (payload: { runId?: string; conversationId?: string }) =>
|
|
||||||
ipcRenderer.invoke('aiAnalysis:abortRun', payload),
|
|
||||||
onRunEvent: (callback: (payload: {
|
|
||||||
runId: string
|
|
||||||
conversationId: string
|
|
||||||
stage: string
|
|
||||||
ts: number
|
|
||||||
message: string
|
|
||||||
intent?: string
|
|
||||||
round?: number
|
|
||||||
toolName?: string
|
|
||||||
status?: string
|
|
||||||
durationMs?: number
|
|
||||||
data?: Record<string, unknown>
|
|
||||||
}) => void) => {
|
|
||||||
const listener = (_: unknown, payload: any) => callback(payload)
|
|
||||||
ipcRenderer.on('aiAnalysis:runEvent', listener)
|
|
||||||
return () => ipcRenderer.removeListener('aiAnalysis:runEvent', listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('aiApi', {
|
contextBridge.exposeInMainWorld('aiApi', {
|
||||||
@@ -668,6 +729,50 @@ contextBridge.exposeInMainWorld('aiApi', {
|
|||||||
deleteConversation: (conversationId: string) => ipcRenderer.invoke('ai:deleteConversation', conversationId),
|
deleteConversation: (conversationId: string) => ipcRenderer.invoke('ai:deleteConversation', conversationId),
|
||||||
listMessages: (payload: { conversationId: string; limit?: number }) => ipcRenderer.invoke('ai:listMessages', payload),
|
listMessages: (payload: { conversationId: string; limit?: number }) => ipcRenderer.invoke('ai:listMessages', payload),
|
||||||
exportConversation: (payload: { conversationId: string }) => ipcRenderer.invoke('ai:exportConversation', payload),
|
exportConversation: (payload: { conversationId: string }) => ipcRenderer.invoke('ai:exportConversation', payload),
|
||||||
|
getMessageContext: (sessionId: string, messageIds: number | number[], contextSize?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getMessageContext', sessionId, messageIds, contextSize),
|
||||||
|
getSearchMessageContext: (sessionId: string, messageIds: number[], contextBefore?: number, contextAfter?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getSearchMessageContext', sessionId, messageIds, contextBefore, contextAfter),
|
||||||
|
getRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getRecentMessages', sessionId, filter, limit),
|
||||||
|
getAllRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getAllRecentMessages', sessionId, filter, limit),
|
||||||
|
getConversationBetween: (
|
||||||
|
sessionId: string,
|
||||||
|
memberId1: number,
|
||||||
|
memberId2: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number
|
||||||
|
) => ipcRenderer.invoke('ai:getConversationBetween', sessionId, memberId1, memberId2, filter, limit),
|
||||||
|
getMessagesBefore: (
|
||||||
|
sessionId: string,
|
||||||
|
beforeId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => ipcRenderer.invoke('ai:getMessagesBefore', sessionId, beforeId, limit, filter, senderId, keywords),
|
||||||
|
getMessagesAfter: (
|
||||||
|
sessionId: string,
|
||||||
|
afterId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords),
|
||||||
|
searchSessions: (
|
||||||
|
sessionId: string,
|
||||||
|
keywords?: string[],
|
||||||
|
timeFilter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number,
|
||||||
|
previewCount?: number
|
||||||
|
) => ipcRenderer.invoke('ai:searchSessions', sessionId, keywords, timeFilter, limit, previewCount),
|
||||||
|
getSessionMessages: (sessionId: string, chatSessionId: string | number, limit?: number) =>
|
||||||
|
ipcRenderer.invoke('ai:getSessionMessages', sessionId, chatSessionId, limit),
|
||||||
|
getSessionSummaries: (
|
||||||
|
sessionId: string,
|
||||||
|
options?: { sessionIds?: string[]; limit?: number; previewCount?: number }
|
||||||
|
) => ipcRenderer.invoke('ai:getSessionSummaries', sessionId, options),
|
||||||
getToolCatalog: () => ipcRenderer.invoke('ai:getToolCatalog'),
|
getToolCatalog: () => ipcRenderer.invoke('ai:getToolCatalog'),
|
||||||
executeTool: (payload: { name: string; args?: Record<string, any> }) => ipcRenderer.invoke('ai:executeTool', payload),
|
executeTool: (payload: { name: string; args?: Record<string, any> }) => ipcRenderer.invoke('ai:executeTool', payload),
|
||||||
cancelToolTest: (payload?: { taskId?: string }) => ipcRenderer.invoke('ai:cancelToolTest', payload)
|
cancelToolTest: (payload?: { taskId?: string }) => ipcRenderer.invoke('ai:cancelToolTest', payload)
|
||||||
@@ -682,13 +787,71 @@ contextBridge.exposeInMainWorld('agentApi', {
|
|||||||
activeSkillId?: string
|
activeSkillId?: string
|
||||||
chatScope?: 'group' | 'private'
|
chatScope?: 'group' | 'private'
|
||||||
sqlContext?: { schemaText?: string; targetHint?: string }
|
sqlContext?: { schemaText?: string; targetHint?: string }
|
||||||
}) => ipcRenderer.invoke('agent:runStream', payload),
|
}, onChunk?: (chunk: any) => void) => {
|
||||||
abort: (payload: { runId?: string; conversationId?: string }) => ipcRenderer.invoke('agent:abort', payload),
|
const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||||
onStream: (callback: (payload: any) => void) => {
|
const promise = new Promise<{ success: boolean; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean }; error?: string }>((resolve) => {
|
||||||
const listener = (_: unknown, payload: any) => callback(payload)
|
let settled = false
|
||||||
ipcRenderer.on('agent:stream', listener)
|
const cleanup = () => {
|
||||||
return () => ipcRenderer.removeListener('agent:stream', listener)
|
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
|
||||||
}
|
ipcRenderer.removeListener('agent:cancel', cancelHandler)
|
||||||
|
ipcRenderer.removeListener('agent:error', errorHandler)
|
||||||
|
ipcRenderer.removeListener('agent:complete', completeHandler)
|
||||||
|
}
|
||||||
|
const settle = (value: { success: boolean; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean }; error?: string }) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
cleanup()
|
||||||
|
resolve(value)
|
||||||
|
}
|
||||||
|
const chunkHandler = (_: unknown, data: { requestId: string; chunk: any }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
if (onChunk) onChunk(data.chunk)
|
||||||
|
}
|
||||||
|
const errorHandler = (_: unknown, data: { requestId: string; error?: string; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean } }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
settle({
|
||||||
|
success: false,
|
||||||
|
error: data?.error || data?.result?.error || '执行失败',
|
||||||
|
result: data?.result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const cancelHandler = (_: unknown, data: { requestId: string; runId?: string }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
settle({
|
||||||
|
success: false,
|
||||||
|
error: '任务已取消',
|
||||||
|
result: {
|
||||||
|
success: false,
|
||||||
|
runId: data?.runId || '',
|
||||||
|
conversationId: '',
|
||||||
|
error: '任务已取消',
|
||||||
|
canceled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const completeHandler = (_: unknown, data: { requestId: string; result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean } }) => {
|
||||||
|
if (data?.requestId !== requestId) return
|
||||||
|
if (data?.result?.error) {
|
||||||
|
settle({ success: false, error: data.result.error, result: data.result })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settle({ success: Boolean(data?.result?.success ?? true), result: data?.result })
|
||||||
|
}
|
||||||
|
ipcRenderer.on('agent:streamChunk', chunkHandler)
|
||||||
|
ipcRenderer.on('agent:cancel', cancelHandler)
|
||||||
|
ipcRenderer.on('agent:error', errorHandler)
|
||||||
|
ipcRenderer.on('agent:complete', completeHandler)
|
||||||
|
ipcRenderer.invoke('agent:runStream', requestId, payload).then((result: { success?: boolean; error?: string }) => {
|
||||||
|
if (result?.success === false) {
|
||||||
|
settle({ success: false, error: result.error || '启动失败' })
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
settle({ success: false, error: String(error) })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return { requestId, promise }
|
||||||
|
},
|
||||||
|
abort: (payload: string | { requestId?: string; runId?: string; conversationId?: string }) => ipcRenderer.invoke('agent:abort', payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('assistantApi', {
|
contextBridge.exposeInMainWorld('assistantApi', {
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ function normalizeText(value: unknown, fallback = ''): string {
|
|||||||
return text || fallback
|
return text || fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseOptionalInt(value: unknown): number | undefined {
|
||||||
|
const n = Number(value)
|
||||||
|
return Number.isFinite(n) ? Math.floor(n) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
function buildApiUrl(baseUrl: string, path: string): string {
|
function buildApiUrl(baseUrl: string, path: string): string {
|
||||||
const base = baseUrl.replace(/\/+$/, '')
|
const base = baseUrl.replace(/\/+$/, '')
|
||||||
const suffix = path.startsWith('/') ? path : `/${path}`
|
const suffix = path.startsWith('/') ? path : `/${path}`
|
||||||
@@ -382,9 +387,9 @@ class AiAgentService {
|
|||||||
const rawContent = normalizeText(res?.choices?.[0]?.message?.content)
|
const rawContent = normalizeText(res?.choices?.[0]?.message?.content)
|
||||||
const sql = extractSqlText(rawContent)
|
const sql = extractSqlText(rawContent)
|
||||||
const usage: TokenUsage = {
|
const usage: TokenUsage = {
|
||||||
promptTokens: Number(res?.usage?.prompt_tokens || 0),
|
promptTokens: parseOptionalInt(res?.usage?.prompt_tokens),
|
||||||
completionTokens: Number(res?.usage?.completion_tokens || 0),
|
completionTokens: parseOptionalInt(res?.usage?.completion_tokens),
|
||||||
totalTokens: Number(res?.usage?.total_tokens || 0)
|
totalTokens: parseOptionalInt(res?.usage?.total_tokens)
|
||||||
}
|
}
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
runtime.onChunk({
|
runtime.onChunk({
|
||||||
@@ -447,4 +452,3 @@ class AiAgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const aiAgentService = new AiAgentService()
|
export const aiAgentService = new AiAgentService()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
|||||||
- 先范围后细节:优先定位会话/时间范围,再拉取具体时间轴或消息。
|
- 先范围后细节:优先定位会话/时间范围,再拉取具体时间轴或消息。
|
||||||
- 可解释性:最终结论尽量附带来源范围与统计口径。
|
- 可解释性:最终结论尽量附带来源范围与统计口径。
|
||||||
- 语音消息不能臆测:必须先拿语音 ID,再点名转写,再总结。
|
- 语音消息不能臆测:必须先拿语音 ID,再点名转写,再总结。
|
||||||
- 联系人排行题(“谁聊得最多/最常联系”)命中 ai_query_top_contacts 后,必须直接给出“前N名+消息数”。
|
- 联系人排行题(“谁聊得最多/最常联系”)命中 get_member_stats 后,必须直接给出“前N名+消息数”。
|
||||||
- 除非用户明确要求,联系人排行默认不包含群聊和公众号。
|
- 除非用户明确要求,联系人排行默认不包含群聊和公众号。
|
||||||
- 用户提到“最近/近期/lately/recent”但未给时间窗时,默认按近30天口径统计并写明口径。
|
- 用户提到“最近/近期/lately/recent”但未给时间窗时,默认按近30天口径统计并写明口径。
|
||||||
- 用户提到联系人简称(如“lr”)时,先把它当联系人缩写处理,优先命中个人会话,不要默认落到群聊。
|
- 用户提到联系人简称(如“lr”)时,先把它当联系人缩写处理,优先命中个人会话,不要默认落到群聊。
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
Agent执行要求:
|
Agent执行要求:
|
||||||
- 用户输入直接进入推理,本地不做关键词分流,你自主决定工具计划。
|
- 用户输入直接进入推理,本地不做关键词分流,你自主决定工具计划。
|
||||||
- 当用户说“今天凌晨/昨晚/某段时间的聊天”,优先调用 ai_query_time_window_activity。
|
- 当用户说“今天凌晨/昨晚/某段时间的聊天”,优先调用 get_time_stats。
|
||||||
- 拿到活跃会话后,调用 ai_query_session_glimpse 对多个会话逐个抽样阅读,不要只读一个会话就停止。
|
- 拿到活跃会话后,调用 get_recent_messages 对多个会话逐个抽样阅读,不要只读一个会话就停止。
|
||||||
- 如果初步探索后用户目标仍模糊,主动提出 1 个关键澄清问题继续多轮对话。
|
- 如果初步探索后用户目标仍模糊,主动提出 1 个关键澄清问题继续多轮对话。
|
||||||
- 仅当你确认任务完成时,输出结束标记 `[[WF_DONE]]`,并紧跟 `<final_answer>...</final_answer>`。
|
- 仅当你确认任务完成时,输出结束标记 `[[WF_DONE]]`,并紧跟 `<final_answer>...</final_answer>`。
|
||||||
- 若还未完成,不要输出结束标记,继续调用工具。
|
- 若还未完成,不要输出结束标记,继续调用工具。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_fetch_message_briefs
|
工具:get_message_context
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 需要核对少量关键消息原文,避免全量展开。
|
- 需要核对少量关键消息原文,避免全量展开。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_session_candidates
|
工具:search_sessions
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 用户未明确具体会话,但给了关键词/关系词(如“老婆”“买车”)。
|
- 用户未明确具体会话,但给了关键词/关系词(如“老婆”“买车”)。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_session_glimpse
|
工具:get_recent_messages
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 已确定候选会话,需要“先看一点”理解上下文。
|
- 已确定候选会话,需要“先看一点”理解上下文。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_source_refs
|
工具:get_session_summaries
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 输出总结或分析后,用于来源说明与可解释卡片。
|
- 输出总结或分析后,用于来源说明与可解释卡片。
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
工具:ai_query_time_window_activity
|
工具:get_time_stats
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 用户提到“今天凌晨/昨晚/某个时间段”的聊天分析。
|
- 用户提到“今天凌晨/昨晚/某个时间段”的聊天分析。
|
||||||
|
|
||||||
Agent策略:
|
Agent策略:
|
||||||
- 第一步必须先扫时间窗活跃会话,不要直接下结论。
|
- 第一步必须先扫时间窗活跃会话,不要直接下结论。
|
||||||
- 拿到活跃会话后,再调用 ai_query_session_glimpse 逐个会话抽样阅读。
|
- 拿到活跃会话后,再调用 get_recent_messages 逐个会话抽样阅读。
|
||||||
- 若用户目标仍不清晰,先追问 1 个关键澄清问题再继续。
|
- 若用户目标仍不清晰,先追问 1 个关键澄清问题再继续。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_timeline
|
工具:search_messages
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 回忆事件经过、梳理时间线、提取关键节点。
|
- 回忆事件经过、梳理时间线、提取关键节点。
|
||||||
@@ -6,4 +6,4 @@
|
|||||||
调用建议:
|
调用建议:
|
||||||
- 默认 detailLevel=minimal。
|
- 默认 detailLevel=minimal。
|
||||||
- 先小批次 limit(40~120),不够再分页 offset。
|
- 先小批次 limit(40~120),不够再分页 offset。
|
||||||
- 需要引用原文证据时,可搭配 ai_fetch_message_briefs。
|
- 需要引用原文证据时,可搭配 get_message_context。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_top_contacts
|
工具:get_member_stats
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 用户问“谁联系最密切”“谁聊得最多”“最常联系的是谁”。
|
- 用户问“谁联系最密切”“谁聊得最多”“最常联系的是谁”。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
工具:ai_query_topic_stats
|
工具:get_chat_overview
|
||||||
|
|
||||||
何时用:
|
何时用:
|
||||||
- 用户问“多少、占比、趋势、对比”。
|
- 用户问“多少、占比、趋势、对比”。
|
||||||
|
|||||||
@@ -40,16 +40,16 @@ presetQuestions:
|
|||||||
- 帮我总结一下最近一周的重要聊天
|
- 帮我总结一下最近一周的重要聊天
|
||||||
- 帮我找一下关于“旅游”的讨论
|
- 帮我找一下关于“旅游”的讨论
|
||||||
allowedBuiltinTools:
|
allowedBuiltinTools:
|
||||||
- ai_query_time_window_activity
|
- get_time_stats
|
||||||
- ai_query_session_candidates
|
- search_sessions
|
||||||
- ai_query_session_glimpse
|
- get_recent_messages
|
||||||
- ai_query_timeline
|
- search_messages
|
||||||
- ai_fetch_message_briefs
|
- get_message_context
|
||||||
- ai_list_voice_messages
|
- ai_list_voice_messages
|
||||||
- ai_transcribe_voice_messages
|
- ai_transcribe_voice_messages
|
||||||
- ai_query_topic_stats
|
- get_chat_overview
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
- ai_query_top_contacts
|
- get_member_stats
|
||||||
---
|
---
|
||||||
|
|
||||||
你是 WeFlow 的全局聊天分析助手。请使用工具获取证据,给出简洁、准确、可执行的结论。
|
你是 WeFlow 的全局聊天分析助手。请使用工具获取证据,给出简洁、准确、可执行的结论。
|
||||||
@@ -70,16 +70,16 @@ presetQuestions:
|
|||||||
- Who are the most active contacts?
|
- Who are the most active contacts?
|
||||||
- Summarize my key chat topics this week
|
- Summarize my key chat topics this week
|
||||||
allowedBuiltinTools:
|
allowedBuiltinTools:
|
||||||
- ai_query_time_window_activity
|
- get_time_stats
|
||||||
- ai_query_session_candidates
|
- search_sessions
|
||||||
- ai_query_session_glimpse
|
- get_recent_messages
|
||||||
- ai_query_timeline
|
- search_messages
|
||||||
- ai_fetch_message_briefs
|
- get_message_context
|
||||||
- ai_list_voice_messages
|
- ai_list_voice_messages
|
||||||
- ai_transcribe_voice_messages
|
- ai_transcribe_voice_messages
|
||||||
- ai_query_topic_stats
|
- get_chat_overview
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
- ai_query_top_contacts
|
- get_member_stats
|
||||||
---
|
---
|
||||||
|
|
||||||
You are WeFlow's global chat analysis assistant.
|
You are WeFlow's global chat analysis assistant.
|
||||||
@@ -95,16 +95,16 @@ presetQuestions:
|
|||||||
- 一番アクティブな相手は誰?
|
- 一番アクティブな相手は誰?
|
||||||
- 今週の重要な会話を要約して
|
- 今週の重要な会話を要約して
|
||||||
allowedBuiltinTools:
|
allowedBuiltinTools:
|
||||||
- ai_query_time_window_activity
|
- get_time_stats
|
||||||
- ai_query_session_candidates
|
- search_sessions
|
||||||
- ai_query_session_glimpse
|
- get_recent_messages
|
||||||
- ai_query_timeline
|
- search_messages
|
||||||
- ai_fetch_message_briefs
|
- get_message_context
|
||||||
- ai_list_voice_messages
|
- ai_list_voice_messages
|
||||||
- ai_transcribe_voice_messages
|
- ai_transcribe_voice_messages
|
||||||
- ai_query_topic_stats
|
- get_chat_overview
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
- ai_query_top_contacts
|
- get_member_stats
|
||||||
---
|
---
|
||||||
|
|
||||||
あなたは WeFlow のグローバルチャット分析アシスタントです。
|
あなたは WeFlow のグローバルチャット分析アシスタントです。
|
||||||
@@ -231,16 +231,16 @@ function toMarkdown(config: AssistantConfigFull): string {
|
|||||||
|
|
||||||
function defaultBuiltinToolCatalog(): Array<{ name: string; category: AssistantToolCategory }> {
|
function defaultBuiltinToolCatalog(): Array<{ name: string; category: AssistantToolCategory }> {
|
||||||
return [
|
return [
|
||||||
{ name: 'ai_query_time_window_activity', category: 'core' },
|
{ name: 'get_time_stats', category: 'core' },
|
||||||
{ name: 'ai_query_session_candidates', category: 'core' },
|
{ name: 'search_sessions', category: 'core' },
|
||||||
{ name: 'ai_query_session_glimpse', category: 'core' },
|
{ name: 'get_recent_messages', category: 'core' },
|
||||||
{ name: 'ai_query_timeline', category: 'core' },
|
{ name: 'search_messages', category: 'core' },
|
||||||
{ name: 'ai_fetch_message_briefs', category: 'core' },
|
{ name: 'get_message_context', category: 'core' },
|
||||||
{ name: 'ai_list_voice_messages', category: 'core' },
|
{ name: 'ai_list_voice_messages', category: 'core' },
|
||||||
{ name: 'ai_transcribe_voice_messages', category: 'core' },
|
{ name: 'ai_transcribe_voice_messages', category: 'core' },
|
||||||
{ name: 'ai_query_topic_stats', category: 'analysis' },
|
{ name: 'get_chat_overview', category: 'analysis' },
|
||||||
{ name: 'ai_query_source_refs', category: 'analysis' },
|
{ name: 'get_session_summaries', category: 'analysis' },
|
||||||
{ name: 'ai_query_top_contacts', category: 'analysis' },
|
{ name: 'get_member_stats', category: 'analysis' },
|
||||||
{ name: 'activate_skill', category: 'analysis' }
|
{ name: 'activate_skill', category: 'analysis' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,18 +32,18 @@ tags:
|
|||||||
- evidence
|
- evidence
|
||||||
chatScope: all
|
chatScope: all
|
||||||
tools:
|
tools:
|
||||||
- ai_query_time_window_activity
|
- get_time_stats
|
||||||
- ai_query_session_candidates
|
- search_sessions
|
||||||
- ai_query_session_glimpse
|
- get_recent_messages
|
||||||
- ai_query_timeline
|
- search_messages
|
||||||
- ai_fetch_message_briefs
|
- get_message_context
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
---
|
---
|
||||||
你是“深度时间线追踪”技能。
|
你是“深度时间线追踪”技能。
|
||||||
执行步骤:
|
执行步骤:
|
||||||
1. 先按时间窗扫描活跃会话,必要时补关键词筛选候选会话。
|
1. 先按时间窗扫描活跃会话,必要时补关键词筛选候选会话。
|
||||||
2. 对候选会话先抽样,再拉取时间轴。
|
2. 对候选会话先抽样,再拉取时间轴。
|
||||||
3. 对关键节点用 ai_fetch_message_briefs 校对原文。
|
3. 对关键节点用 get_message_context 校对原文。
|
||||||
4. 最后输出“结论 + 关键节点 + 来源范围”。`
|
4. 最后输出“结论 + 关键节点 + 来源范围”。`
|
||||||
|
|
||||||
const SKILL_CONTACT_FOCUS_MD = `---
|
const SKILL_CONTACT_FOCUS_MD = `---
|
||||||
@@ -55,17 +55,17 @@ tags:
|
|||||||
- relation
|
- relation
|
||||||
chatScope: private
|
chatScope: private
|
||||||
tools:
|
tools:
|
||||||
- ai_query_top_contacts
|
- get_member_stats
|
||||||
- ai_query_topic_stats
|
- get_chat_overview
|
||||||
- ai_query_session_glimpse
|
- get_recent_messages
|
||||||
- ai_query_timeline
|
- search_messages
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
---
|
---
|
||||||
你是“联系人关系聚焦”技能。
|
你是“联系人关系聚焦”技能。
|
||||||
执行步骤:
|
执行步骤:
|
||||||
1. 优先调用 ai_query_top_contacts 得到候选联系人排名。
|
1. 优先调用 get_member_stats 得到候选联系人排名。
|
||||||
2. 针对 Top 联系人读取抽样消息并补充时间轴。
|
2. 针对 Top 联系人读取抽样消息并补充时间轴。
|
||||||
3. 如果用户问题涉及“变化趋势”,补 ai_query_topic_stats。
|
3. 如果用户问题涉及“变化趋势”,补 get_chat_overview。
|
||||||
4. 输出时必须给出对比口径(时间窗、样本范围、消息数量)。`
|
4. 输出时必须给出对比口径(时间窗、样本范围、消息数量)。`
|
||||||
|
|
||||||
const SKILL_VOICE_AUDIT_MD = `---
|
const SKILL_VOICE_AUDIT_MD = `---
|
||||||
@@ -79,7 +79,7 @@ chatScope: all
|
|||||||
tools:
|
tools:
|
||||||
- ai_list_voice_messages
|
- ai_list_voice_messages
|
||||||
- ai_transcribe_voice_messages
|
- ai_transcribe_voice_messages
|
||||||
- ai_query_source_refs
|
- get_session_summaries
|
||||||
---
|
---
|
||||||
你是“语音证据审计”技能。
|
你是“语音证据审计”技能。
|
||||||
硬规则:
|
硬规则:
|
||||||
|
|||||||
@@ -174,6 +174,36 @@ interface GetContactsOptions {
|
|||||||
lite?: boolean
|
lite?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AiTimeFilter {
|
||||||
|
startTs?: number
|
||||||
|
endTs?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiMessageResult {
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiSessionSearchResult {
|
||||||
|
id: string
|
||||||
|
startTs: number
|
||||||
|
endTs: number
|
||||||
|
messageCount: number
|
||||||
|
isComplete: boolean
|
||||||
|
previewMessages: AiMessageResult[]
|
||||||
|
}
|
||||||
|
|
||||||
interface ExportSessionStats {
|
interface ExportSessionStats {
|
||||||
totalMessages: number
|
totalMessages: number
|
||||||
voiceMessages: number
|
voiceMessages: number
|
||||||
@@ -8474,6 +8504,451 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeAiFilter(filter?: AiTimeFilter): { begin: number; end: number } {
|
||||||
|
const begin = this.normalizeTimestampSeconds(Number(filter?.startTs || 0))
|
||||||
|
const end = this.normalizeTimestampSeconds(Number(filter?.endTs || 0))
|
||||||
|
return { begin, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashSenderId(senderUsername: string): number {
|
||||||
|
const text = String(senderUsername || '').trim().toLowerCase()
|
||||||
|
if (!text) return 0
|
||||||
|
let hash = 5381
|
||||||
|
for (let i = 0; i < text.length; i += 1) {
|
||||||
|
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0
|
||||||
|
}
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private messageMatchesKeywords(message: Message, keywords?: string[]): boolean {
|
||||||
|
if (!Array.isArray(keywords) || keywords.length === 0) return true
|
||||||
|
const text = String(message.parsedContent || message.rawContent || '').toLowerCase()
|
||||||
|
if (!text) return false
|
||||||
|
return keywords.every((keyword) => {
|
||||||
|
const token = String(keyword || '').trim().toLowerCase()
|
||||||
|
if (!token) return true
|
||||||
|
return text.includes(token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private toAiMessage(sessionId: string, message: Message): AiMessageResult {
|
||||||
|
const senderUsername = String(message.senderUsername || '').trim()
|
||||||
|
const senderName = senderUsername || (message.isSend === 1 ? '我' : '未知成员')
|
||||||
|
const content = String(message.parsedContent || message.rawContent || '').trim()
|
||||||
|
return {
|
||||||
|
id: message.localId,
|
||||||
|
localId: message.localId,
|
||||||
|
sessionId,
|
||||||
|
senderName,
|
||||||
|
senderPlatformId: senderUsername,
|
||||||
|
senderUsername,
|
||||||
|
content,
|
||||||
|
timestamp: Number(message.createTime || 0),
|
||||||
|
type: Number(message.localType || 0),
|
||||||
|
isSend: message.isSend,
|
||||||
|
replyToMessageId: message.messageKey || null,
|
||||||
|
replyToContent: message.quotedContent || null,
|
||||||
|
replyToSenderName: message.quotedSender || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchMessagesByCursorWithKey(
|
||||||
|
sessionId: string,
|
||||||
|
key: { sortSeq?: number; createTime?: number; localId?: number },
|
||||||
|
limit: number,
|
||||||
|
ascending: boolean,
|
||||||
|
beginTimestamp = 0,
|
||||||
|
endTimestamp = 0
|
||||||
|
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
||||||
|
const batchSize = Math.max(limit + 8, Math.min(240, limit * 2))
|
||||||
|
const cursorResult = await wcdbService.openMessageCursorWithKey(
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
if (!cursorResult.success || !cursorResult.cursor) {
|
||||||
|
return { success: false, error: cursorResult.error || '创建游标失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const collected = await this.collectVisibleMessagesFromCursor(sessionId, cursorResult.cursor, limit)
|
||||||
|
if (!collected.success) {
|
||||||
|
return { success: false, error: collected.error || '读取消息失败' }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messages: collected.messages || [],
|
||||||
|
hasMore: collected.hasMore === true
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await wcdbService.closeMessageCursor(cursorResult.cursor).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentMessagesForAI(
|
||||||
|
sessionId: string,
|
||||||
|
filter?: AiTimeFilter,
|
||||||
|
limit = 100
|
||||||
|
): Promise<{ messages: AiMessageResult[]; total: number }> {
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(500, Number(limit || 100)))
|
||||||
|
const { begin, end } = this.normalizeAiFilter(filter)
|
||||||
|
const result = await this.getLatestMessages(sessionId, normalizedLimit)
|
||||||
|
if (!result.success || !Array.isArray(result.messages)) {
|
||||||
|
return { messages: [], total: 0 }
|
||||||
|
}
|
||||||
|
const bounded = result.messages.filter((message) => {
|
||||||
|
if (begin > 0 && Number(message.createTime || 0) < begin) return false
|
||||||
|
if (end > 0 && Number(message.createTime || 0) > end) return false
|
||||||
|
return String(message.parsedContent || message.rawContent || '').trim().length > 0
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
messages: bounded.slice(-normalizedLimit).map((message) => this.toAiMessage(sessionId, message)),
|
||||||
|
total: bounded.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessagesBeforeForAI(
|
||||||
|
sessionId: string,
|
||||||
|
beforeId: number,
|
||||||
|
limit = 50,
|
||||||
|
filter?: AiTimeFilter,
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
): Promise<{ messages: AiMessageResult[]; hasMore: boolean }> {
|
||||||
|
const base = await this.getMessageById(sessionId, Number(beforeId))
|
||||||
|
if (!base.success || !base.message) {
|
||||||
|
return { messages: [], hasMore: false }
|
||||||
|
}
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(300, Number(limit || 50)))
|
||||||
|
const { begin, end } = this.normalizeAiFilter(filter)
|
||||||
|
const cursor = await this.fetchMessagesByCursorWithKey(
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
sortSeq: base.message.sortSeq,
|
||||||
|
createTime: base.message.createTime,
|
||||||
|
localId: base.message.localId
|
||||||
|
},
|
||||||
|
Math.max(normalizedLimit * 2, normalizedLimit + 12),
|
||||||
|
false,
|
||||||
|
begin,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
if (!cursor.success) {
|
||||||
|
return { messages: [], hasMore: false }
|
||||||
|
}
|
||||||
|
const filtered = (cursor.messages || []).filter((message) => {
|
||||||
|
if (senderId && senderId > 0) {
|
||||||
|
const hashed = this.hashSenderId(String(message.senderUsername || ''))
|
||||||
|
if (hashed !== senderId) return false
|
||||||
|
}
|
||||||
|
return this.messageMatchesKeywords(message, keywords)
|
||||||
|
})
|
||||||
|
const sliced = filtered.slice(-normalizedLimit)
|
||||||
|
return {
|
||||||
|
messages: sliced.map((message) => this.toAiMessage(sessionId, message)),
|
||||||
|
hasMore: cursor.hasMore === true || filtered.length > normalizedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessagesAfterForAI(
|
||||||
|
sessionId: string,
|
||||||
|
afterId: number,
|
||||||
|
limit = 50,
|
||||||
|
filter?: AiTimeFilter,
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
): Promise<{ messages: AiMessageResult[]; hasMore: boolean }> {
|
||||||
|
const base = await this.getMessageById(sessionId, Number(afterId))
|
||||||
|
if (!base.success || !base.message) {
|
||||||
|
return { messages: [], hasMore: false }
|
||||||
|
}
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(300, Number(limit || 50)))
|
||||||
|
const { begin, end } = this.normalizeAiFilter(filter)
|
||||||
|
const cursor = await this.fetchMessagesByCursorWithKey(
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
sortSeq: base.message.sortSeq,
|
||||||
|
createTime: base.message.createTime,
|
||||||
|
localId: base.message.localId
|
||||||
|
},
|
||||||
|
Math.max(normalizedLimit * 2, normalizedLimit + 12),
|
||||||
|
true,
|
||||||
|
begin,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
if (!cursor.success) {
|
||||||
|
return { messages: [], hasMore: false }
|
||||||
|
}
|
||||||
|
const filtered = (cursor.messages || []).filter((message) => {
|
||||||
|
if (senderId && senderId > 0) {
|
||||||
|
const hashed = this.hashSenderId(String(message.senderUsername || ''))
|
||||||
|
if (hashed !== senderId) return false
|
||||||
|
}
|
||||||
|
return this.messageMatchesKeywords(message, keywords)
|
||||||
|
})
|
||||||
|
const sliced = filtered.slice(0, normalizedLimit)
|
||||||
|
return {
|
||||||
|
messages: sliced.map((message) => this.toAiMessage(sessionId, message)),
|
||||||
|
hasMore: cursor.hasMore === true || filtered.length > normalizedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessageContextForAI(
|
||||||
|
sessionId: string,
|
||||||
|
messageIds: number | number[],
|
||||||
|
contextSize = 20
|
||||||
|
): Promise<AiMessageResult[]> {
|
||||||
|
const ids = Array.isArray(messageIds) ? messageIds : [messageIds]
|
||||||
|
const uniqueIds = Array.from(new Set(ids.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)))
|
||||||
|
if (uniqueIds.length === 0) return []
|
||||||
|
const size = Math.max(0, Math.min(120, Number(contextSize || 20)))
|
||||||
|
const merged = new Map<number, AiMessageResult>()
|
||||||
|
|
||||||
|
for (const id of uniqueIds) {
|
||||||
|
const target = await this.getMessageById(sessionId, id)
|
||||||
|
if (target.success && target.message) {
|
||||||
|
merged.set(id, this.toAiMessage(sessionId, target.message))
|
||||||
|
}
|
||||||
|
if (size <= 0) continue
|
||||||
|
const [before, after] = await Promise.all([
|
||||||
|
this.getMessagesBeforeForAI(sessionId, id, size),
|
||||||
|
this.getMessagesAfterForAI(sessionId, id, size)
|
||||||
|
])
|
||||||
|
for (const item of before.messages) merged.set(item.id, item)
|
||||||
|
for (const item of after.messages) merged.set(item.id, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(merged.values()).sort((a, b) => {
|
||||||
|
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp
|
||||||
|
return a.id - b.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSearchMessageContextForAI(
|
||||||
|
sessionId: string,
|
||||||
|
messageIds: number[],
|
||||||
|
contextBefore = 2,
|
||||||
|
contextAfter = 2
|
||||||
|
): Promise<AiMessageResult[]> {
|
||||||
|
const uniqueIds = Array.from(new Set((messageIds || []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)))
|
||||||
|
if (uniqueIds.length === 0) return []
|
||||||
|
const beforeLimit = Math.max(0, Math.min(30, Number(contextBefore || 2)))
|
||||||
|
const afterLimit = Math.max(0, Math.min(30, Number(contextAfter || 2)))
|
||||||
|
const merged = new Map<number, AiMessageResult>()
|
||||||
|
|
||||||
|
for (const id of uniqueIds) {
|
||||||
|
const target = await this.getMessageById(sessionId, id)
|
||||||
|
if (target.success && target.message) {
|
||||||
|
merged.set(id, this.toAiMessage(sessionId, target.message))
|
||||||
|
}
|
||||||
|
const [before, after] = await Promise.all([
|
||||||
|
beforeLimit > 0 ? this.getMessagesBeforeForAI(sessionId, id, beforeLimit) : Promise.resolve({ messages: [], hasMore: false }),
|
||||||
|
afterLimit > 0 ? this.getMessagesAfterForAI(sessionId, id, afterLimit) : Promise.resolve({ messages: [], hasMore: false })
|
||||||
|
])
|
||||||
|
for (const item of before.messages) merged.set(item.id, item)
|
||||||
|
for (const item of after.messages) merged.set(item.id, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(merged.values()).sort((a, b) => {
|
||||||
|
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp
|
||||||
|
return a.id - b.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConversationBetweenForAI(
|
||||||
|
sessionId: string,
|
||||||
|
memberId1: number,
|
||||||
|
memberId2: number,
|
||||||
|
filter?: AiTimeFilter,
|
||||||
|
limit = 100
|
||||||
|
): Promise<{ messages: AiMessageResult[]; total: number; member1Name: string; member2Name: string }> {
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(500, Number(limit || 100)))
|
||||||
|
const { begin, end } = this.normalizeAiFilter(filter)
|
||||||
|
const sample = await this.getMessages(sessionId, 0, Math.max(600, normalizedLimit * 8), begin, end, false)
|
||||||
|
if (!sample.success || !Array.isArray(sample.messages) || sample.messages.length === 0) {
|
||||||
|
return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const idSet = new Set<number>([Number(memberId1), Number(memberId2)].filter((id) => Number.isFinite(id) && id > 0))
|
||||||
|
const filtered = sample.messages.filter((message) => {
|
||||||
|
const senderId = this.hashSenderId(String(message.senderUsername || ''))
|
||||||
|
return idSet.has(senderId) && String(message.parsedContent || message.rawContent || '').trim().length > 0
|
||||||
|
})
|
||||||
|
const picked = filtered.slice(-normalizedLimit)
|
||||||
|
const names = Array.from(new Set(picked.map((message) => String(message.senderUsername || '').trim()).filter(Boolean)))
|
||||||
|
return {
|
||||||
|
messages: picked.map((message) => this.toAiMessage(sessionId, message)),
|
||||||
|
total: filtered.length,
|
||||||
|
member1Name: names[0] || '',
|
||||||
|
member2Name: names[1] || names[0] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchSessionsForAI(
|
||||||
|
_sessionId: string,
|
||||||
|
keywords?: string[],
|
||||||
|
timeFilter?: AiTimeFilter,
|
||||||
|
limit = 20,
|
||||||
|
previewCount = 5
|
||||||
|
): Promise<AiSessionSearchResult[]> {
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(60, Number(limit || 20)))
|
||||||
|
const normalizedPreview = Math.max(1, Math.min(20, Number(previewCount || 5)))
|
||||||
|
const { begin, end } = this.normalizeAiFilter(timeFilter)
|
||||||
|
const tokenList = Array.from(new Set((keywords || []).map((keyword) => String(keyword || '').trim()).filter(Boolean)))
|
||||||
|
|
||||||
|
const sessionsResult = await this.getSessions()
|
||||||
|
if (!sessionsResult.success || !Array.isArray(sessionsResult.sessions)) return []
|
||||||
|
const sessionMap = new Map<string, ChatSession>()
|
||||||
|
for (const session of sessionsResult.sessions) {
|
||||||
|
const sid = String(session.username || '').trim()
|
||||||
|
if (!sid) continue
|
||||||
|
sessionMap.set(sid, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: Array<{ sessionId: string; hitCount: number }> = []
|
||||||
|
if (tokenList.length > 0) {
|
||||||
|
const native = await wcdbService.aiQuerySessionCandidates({
|
||||||
|
keyword: tokenList.join(' '),
|
||||||
|
limit: normalizedLimit * 4,
|
||||||
|
beginTimestamp: begin,
|
||||||
|
endTimestamp: end
|
||||||
|
})
|
||||||
|
if (native.success && Array.isArray(native.rows)) {
|
||||||
|
for (const row of native.rows as Record<string, any>[]) {
|
||||||
|
const sid = String(row.session_id || row._session_id || row.sessionId || '').trim()
|
||||||
|
if (!sid) continue
|
||||||
|
rows.push({
|
||||||
|
sessionId: sid,
|
||||||
|
hitCount: this.toSafeInt(row.hit_count ?? row.count ?? row.message_count, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateIds = rows.length > 0
|
||||||
|
? Array.from(new Set(rows.map((item) => item.sessionId)))
|
||||||
|
: sessionsResult.sessions
|
||||||
|
.filter((session) => {
|
||||||
|
if (begin > 0 && Number(session.lastTimestamp || session.sortTimestamp || 0) < begin) return false
|
||||||
|
if (end > 0 && Number(session.lastTimestamp || session.sortTimestamp || 0) > end) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.slice(0, normalizedLimit * 2)
|
||||||
|
.map((session) => String(session.username || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const output: AiSessionSearchResult[] = []
|
||||||
|
for (const sid of candidateIds.slice(0, normalizedLimit)) {
|
||||||
|
const latest = await this.getLatestMessages(sid, normalizedPreview)
|
||||||
|
const messages = Array.isArray(latest.messages) ? latest.messages : []
|
||||||
|
const mapped = messages.map((message) => this.toAiMessage(sid, message)).slice(-normalizedPreview)
|
||||||
|
const hitRow = rows.find((item) => item.sessionId === sid)
|
||||||
|
const session = sessionMap.get(sid)
|
||||||
|
const tsList = mapped.map((item) => item.timestamp).filter((value) => Number.isFinite(value) && value > 0)
|
||||||
|
const startTs = tsList.length > 0 ? Math.min(...tsList) : 0
|
||||||
|
const endTs = tsList.length > 0 ? Math.max(...tsList) : Number(session?.lastTimestamp || session?.sortTimestamp || 0)
|
||||||
|
output.push({
|
||||||
|
id: sid,
|
||||||
|
startTs,
|
||||||
|
endTs,
|
||||||
|
messageCount: hitRow?.hitCount || mapped.length,
|
||||||
|
isComplete: mapped.length <= normalizedPreview,
|
||||||
|
previewMessages: mapped
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionMessagesForAI(
|
||||||
|
_sessionId: string,
|
||||||
|
chatSessionId: string | number,
|
||||||
|
limit = 500
|
||||||
|
): Promise<{
|
||||||
|
sessionId: string
|
||||||
|
startTs: number
|
||||||
|
endTs: number
|
||||||
|
messageCount: number
|
||||||
|
returnedCount: number
|
||||||
|
participants: string[]
|
||||||
|
messages: AiMessageResult[]
|
||||||
|
} | null> {
|
||||||
|
const sid = String(chatSessionId || '').trim()
|
||||||
|
if (!sid) return null
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(1000, Number(limit || 500)))
|
||||||
|
const latest = await this.getLatestMessages(sid, normalizedLimit)
|
||||||
|
if (!latest.success || !Array.isArray(latest.messages)) return null
|
||||||
|
const mapped = latest.messages.map((message) => this.toAiMessage(sid, message))
|
||||||
|
const tsList = mapped.map((item) => item.timestamp).filter((value) => Number.isFinite(value) && value > 0)
|
||||||
|
const count = await this.getMessageCount(sid)
|
||||||
|
return {
|
||||||
|
sessionId: sid,
|
||||||
|
startTs: tsList.length > 0 ? Math.min(...tsList) : 0,
|
||||||
|
endTs: tsList.length > 0 ? Math.max(...tsList) : 0,
|
||||||
|
messageCount: count.success ? Number(count.count || mapped.length) : mapped.length,
|
||||||
|
returnedCount: mapped.length,
|
||||||
|
participants: Array.from(new Set(mapped.map((item) => item.senderName).filter(Boolean))),
|
||||||
|
messages: mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSessionSummariesForAI(
|
||||||
|
_sessionId: string,
|
||||||
|
options?: {
|
||||||
|
sessionIds?: string[]
|
||||||
|
limit?: number
|
||||||
|
previewCount?: number
|
||||||
|
}
|
||||||
|
): Promise<Array<{
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
messageCount: number
|
||||||
|
latestTs: number
|
||||||
|
previewMessages: AiMessageResult[]
|
||||||
|
}>> {
|
||||||
|
const normalizedLimit = Math.max(1, Math.min(60, Number(options?.limit || 20)))
|
||||||
|
const previewCount = Math.max(1, Math.min(20, Number(options?.previewCount || 3)))
|
||||||
|
const sessionsResult = await this.getSessions()
|
||||||
|
if (!sessionsResult.success || !Array.isArray(sessionsResult.sessions)) return []
|
||||||
|
const explicitIds = Array.isArray(options?.sessionIds)
|
||||||
|
? options?.sessionIds.map((value) => String(value || '').trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
const candidates = explicitIds.length > 0
|
||||||
|
? sessionsResult.sessions.filter((session) => explicitIds.includes(String(session.username || '').trim()))
|
||||||
|
: sessionsResult.sessions.slice(0, normalizedLimit)
|
||||||
|
|
||||||
|
const summaries: Array<{
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
messageCount: number
|
||||||
|
latestTs: number
|
||||||
|
previewMessages: AiMessageResult[]
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
for (const session of candidates.slice(0, normalizedLimit)) {
|
||||||
|
const sid = String(session.username || '').trim()
|
||||||
|
if (!sid) continue
|
||||||
|
const [countResult, latestResult] = await Promise.all([
|
||||||
|
this.getMessageCount(sid),
|
||||||
|
this.getLatestMessages(sid, previewCount)
|
||||||
|
])
|
||||||
|
const previewMessages = Array.isArray(latestResult.messages)
|
||||||
|
? latestResult.messages.map((message) => this.toAiMessage(sid, message)).slice(-previewCount)
|
||||||
|
: []
|
||||||
|
summaries.push({
|
||||||
|
sessionId: sid,
|
||||||
|
sessionName: String(session.displayName || sid),
|
||||||
|
messageCount: countResult.success ? Number(countResult.count || previewMessages.length) : previewMessages.length,
|
||||||
|
latestTs: Number(session.lastTimestamp || session.sortTimestamp || 0),
|
||||||
|
previewMessages
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return summaries
|
||||||
|
}
|
||||||
|
|
||||||
async getMessageById(sessionId: string, localId: number): Promise<{ success: boolean; message?: Message; error?: string }> {
|
async getMessageById(sessionId: string, localId: number): Promise<{ success: boolean; message?: Message; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const nativeResult = await wcdbService.getMessageById(sessionId, localId)
|
const nativeResult = await wcdbService.getMessageById(sessionId, localId)
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export class WcdbCore {
|
|||||||
private wcdbGetMessageDates: any = null
|
private wcdbGetMessageDates: any = null
|
||||||
private wcdbOpenMessageCursor: any = null
|
private wcdbOpenMessageCursor: any = null
|
||||||
private wcdbOpenMessageCursorLite: any = null
|
private wcdbOpenMessageCursorLite: any = null
|
||||||
|
private wcdbOpenMessageCursorWithKey: any = null
|
||||||
|
private wcdbOpenMessageCursorLiteWithKey: any = null
|
||||||
private wcdbFetchMessageBatch: any = null
|
private wcdbFetchMessageBatch: any = null
|
||||||
private wcdbCloseMessageCursor: any = null
|
private wcdbCloseMessageCursor: any = null
|
||||||
private wcdbGetLogs: any = null
|
private wcdbGetLogs: any = null
|
||||||
@@ -89,6 +91,15 @@ export class WcdbCore {
|
|||||||
private wcdbAiQueryTimeline: any = null
|
private wcdbAiQueryTimeline: any = null
|
||||||
private wcdbAiQueryTopicStats: any = null
|
private wcdbAiQueryTopicStats: any = null
|
||||||
private wcdbAiQuerySourceRefs: any = null
|
private wcdbAiQuerySourceRefs: any = null
|
||||||
|
private wcdbAiGetRecentMessages: any = null
|
||||||
|
private wcdbAiGetMessagesBefore: any = null
|
||||||
|
private wcdbAiGetMessagesAfter: any = null
|
||||||
|
private wcdbAiGetMessageContext: any = null
|
||||||
|
private wcdbAiGetSearchMessageContext: any = null
|
||||||
|
private wcdbAiGetConversationBetween: any = null
|
||||||
|
private wcdbAiSearchSessions: any = null
|
||||||
|
private wcdbAiGetSessionMessages: any = null
|
||||||
|
private wcdbAiGetSessionSummaries: any = null
|
||||||
private wcdbGetSnsTimeline: any = null
|
private wcdbGetSnsTimeline: any = null
|
||||||
private wcdbGetSnsAnnualStats: any = null
|
private wcdbGetSnsAnnualStats: any = null
|
||||||
private wcdbGetSnsUsernames: any = null
|
private wcdbGetSnsUsernames: any = null
|
||||||
@@ -947,6 +958,15 @@ export class WcdbCore {
|
|||||||
// wcdb_status wcdb_open_message_cursor(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
// wcdb_status wcdb_open_message_cursor(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
||||||
this.wcdbOpenMessageCursor = this.lib.func('int32 wcdb_open_message_cursor(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
this.wcdbOpenMessageCursor = this.lib.func('int32 wcdb_open_message_cursor(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
||||||
|
|
||||||
|
// wcdb_status wcdb_open_message_cursor_with_key(...)
|
||||||
|
try {
|
||||||
|
this.wcdbOpenMessageCursorWithKey = this.lib.func(
|
||||||
|
'int32 wcdb_open_message_cursor_with_key(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, int32 keyValid, int64 keySortSeq, int64 keyCreateTime, int64 keyLocalId, _Out_ int64* outCursor)'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
this.wcdbOpenMessageCursorWithKey = null
|
||||||
|
}
|
||||||
|
|
||||||
// wcdb_status wcdb_open_message_cursor_lite(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
// wcdb_status wcdb_open_message_cursor_lite(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
||||||
try {
|
try {
|
||||||
this.wcdbOpenMessageCursorLite = this.lib.func('int32 wcdb_open_message_cursor_lite(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
this.wcdbOpenMessageCursorLite = this.lib.func('int32 wcdb_open_message_cursor_lite(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
||||||
@@ -954,6 +974,15 @@ export class WcdbCore {
|
|||||||
this.wcdbOpenMessageCursorLite = null
|
this.wcdbOpenMessageCursorLite = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wcdb_status wcdb_open_message_cursor_lite_with_key(...)
|
||||||
|
try {
|
||||||
|
this.wcdbOpenMessageCursorLiteWithKey = this.lib.func(
|
||||||
|
'int32 wcdb_open_message_cursor_lite_with_key(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, int32 keyValid, int64 keySortSeq, int64 keyCreateTime, int64 keyLocalId, _Out_ int64* outCursor)'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
this.wcdbOpenMessageCursorLiteWithKey = null
|
||||||
|
}
|
||||||
|
|
||||||
// wcdb_status wcdb_fetch_message_batch(wcdb_handle handle, wcdb_cursor cursor, char** out_json, int32_t* out_has_more)
|
// wcdb_status wcdb_fetch_message_batch(wcdb_handle handle, wcdb_cursor cursor, char** out_json, int32_t* out_has_more)
|
||||||
this.wcdbFetchMessageBatch = this.lib.func('int32 wcdb_fetch_message_batch(int64 handle, int64 cursor, _Out_ void** outJson, _Out_ int32* outHasMore)')
|
this.wcdbFetchMessageBatch = this.lib.func('int32 wcdb_fetch_message_batch(int64 handle, int64 cursor, _Out_ void** outJson, _Out_ int32* outHasMore)')
|
||||||
|
|
||||||
@@ -1084,6 +1113,51 @@ export class WcdbCore {
|
|||||||
} catch {
|
} catch {
|
||||||
this.wcdbAiQuerySourceRefs = null
|
this.wcdbAiQuerySourceRefs = null
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetRecentMessages = this.lib.func('int32 wcdb_ai_get_recent_messages(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetRecentMessages = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetMessagesBefore = this.lib.func('int32 wcdb_ai_get_messages_before(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetMessagesBefore = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetMessagesAfter = this.lib.func('int32 wcdb_ai_get_messages_after(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetMessagesAfter = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetMessageContext = this.lib.func('int32 wcdb_ai_get_message_context(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetMessageContext = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetSearchMessageContext = this.lib.func('int32 wcdb_ai_get_search_message_context(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetSearchMessageContext = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetConversationBetween = this.lib.func('int32 wcdb_ai_get_conversation_between(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetConversationBetween = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiSearchSessions = this.lib.func('int32 wcdb_ai_search_sessions(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiSearchSessions = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetSessionMessages = this.lib.func('int32 wcdb_ai_get_session_messages(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetSessionMessages = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.wcdbAiGetSessionSummaries = this.lib.func('int32 wcdb_ai_get_session_summaries(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||||
|
} catch {
|
||||||
|
this.wcdbAiGetSessionSummaries = null
|
||||||
|
}
|
||||||
|
|
||||||
// wcdb_status wcdb_get_sns_timeline(wcdb_handle handle, int32_t limit, int32_t offset, const char* username, const char* keyword, int32_t start_time, int32_t end_time, char** out_json)
|
// wcdb_status wcdb_get_sns_timeline(wcdb_handle handle, int32_t limit, int32_t offset, const char* username, const char* keyword, int32_t start_time, int32_t end_time, char** out_json)
|
||||||
try {
|
try {
|
||||||
@@ -3280,6 +3354,80 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openMessageCursorWithKey(
|
||||||
|
sessionId: string,
|
||||||
|
batchSize: number,
|
||||||
|
ascending: boolean,
|
||||||
|
beginTimestamp: number,
|
||||||
|
endTimestamp: number,
|
||||||
|
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||||
|
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||||
|
if (!this.ensureReady()) {
|
||||||
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
|
}
|
||||||
|
const keySortSeq = Number.isFinite(Number(key?.sortSeq)) ? Math.floor(Number(key?.sortSeq)) : 0
|
||||||
|
const keyCreateTime = Number.isFinite(Number(key?.createTime)) ? Math.floor(Number(key?.createTime)) : 0
|
||||||
|
const keyLocalId = Number.isFinite(Number(key?.localId)) ? Math.floor(Number(key?.localId)) : 0
|
||||||
|
const keyValid = keySortSeq > 0 || keyCreateTime > 0 || keyLocalId > 0
|
||||||
|
|
||||||
|
if (!keyValid || !this.wcdbOpenMessageCursorWithKey) {
|
||||||
|
return this.openMessageCursor(sessionId, batchSize, ascending, beginTimestamp, endTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outCursor = [0]
|
||||||
|
let result = this.wcdbOpenMessageCursorWithKey(
|
||||||
|
this.handle,
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending ? 1 : 0,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
1,
|
||||||
|
keySortSeq,
|
||||||
|
keyCreateTime,
|
||||||
|
keyLocalId,
|
||||||
|
outCursor
|
||||||
|
)
|
||||||
|
if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
|
||||||
|
this.writeLog('openMessageCursorWithKey: result=-3 (no message db), attempting forceReopen...', true)
|
||||||
|
const reopened = await this.forceReopen()
|
||||||
|
if (reopened && this.handle !== null) {
|
||||||
|
outCursor[0] = 0
|
||||||
|
result = this.wcdbOpenMessageCursorWithKey(
|
||||||
|
this.handle,
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending ? 1 : 0,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
1,
|
||||||
|
keySortSeq,
|
||||||
|
keyCreateTime,
|
||||||
|
keyLocalId,
|
||||||
|
outCursor
|
||||||
|
)
|
||||||
|
this.writeLog(`openMessageCursorWithKey retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result !== 0 || outCursor[0] <= 0) {
|
||||||
|
if (result !== -3) {
|
||||||
|
await this.printLogs(true)
|
||||||
|
this.writeLog(
|
||||||
|
`openMessageCursorWithKey failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return { success: false, error: `创建游标失败: ${result}` }
|
||||||
|
}
|
||||||
|
return { success: true, cursor: outCursor[0] }
|
||||||
|
} catch (e) {
|
||||||
|
await this.printLogs(true)
|
||||||
|
this.writeLog(`openMessageCursorWithKey exception: ${String(e)}`, true)
|
||||||
|
return { success: false, error: '创建游标异常,请查看日志' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async openMessageCursorLite(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
async openMessageCursorLite(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||||
if (!this.ensureReady()) {
|
if (!this.ensureReady()) {
|
||||||
return { success: false, error: 'WCDB 未连接' }
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
@@ -3342,6 +3490,83 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openMessageCursorLiteWithKey(
|
||||||
|
sessionId: string,
|
||||||
|
batchSize: number,
|
||||||
|
ascending: boolean,
|
||||||
|
beginTimestamp: number,
|
||||||
|
endTimestamp: number,
|
||||||
|
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||||
|
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||||
|
if (!this.ensureReady()) {
|
||||||
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
|
}
|
||||||
|
const keySortSeq = Number.isFinite(Number(key?.sortSeq)) ? Math.floor(Number(key?.sortSeq)) : 0
|
||||||
|
const keyCreateTime = Number.isFinite(Number(key?.createTime)) ? Math.floor(Number(key?.createTime)) : 0
|
||||||
|
const keyLocalId = Number.isFinite(Number(key?.localId)) ? Math.floor(Number(key?.localId)) : 0
|
||||||
|
const keyValid = keySortSeq > 0 || keyCreateTime > 0 || keyLocalId > 0
|
||||||
|
|
||||||
|
if (!keyValid) {
|
||||||
|
return this.openMessageCursorLite(sessionId, batchSize, ascending, beginTimestamp, endTimestamp)
|
||||||
|
}
|
||||||
|
if (!this.wcdbOpenMessageCursorLiteWithKey) {
|
||||||
|
return this.openMessageCursorWithKey(sessionId, batchSize, ascending, beginTimestamp, endTimestamp, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outCursor = [0]
|
||||||
|
let result = this.wcdbOpenMessageCursorLiteWithKey(
|
||||||
|
this.handle,
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending ? 1 : 0,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
1,
|
||||||
|
keySortSeq,
|
||||||
|
keyCreateTime,
|
||||||
|
keyLocalId,
|
||||||
|
outCursor
|
||||||
|
)
|
||||||
|
if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
|
||||||
|
this.writeLog('openMessageCursorLiteWithKey: result=-3 (no message db), attempting forceReopen...', true)
|
||||||
|
const reopened = await this.forceReopen()
|
||||||
|
if (reopened && this.handle !== null) {
|
||||||
|
outCursor[0] = 0
|
||||||
|
result = this.wcdbOpenMessageCursorLiteWithKey(
|
||||||
|
this.handle,
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending ? 1 : 0,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
1,
|
||||||
|
keySortSeq,
|
||||||
|
keyCreateTime,
|
||||||
|
keyLocalId,
|
||||||
|
outCursor
|
||||||
|
)
|
||||||
|
this.writeLog(`openMessageCursorLiteWithKey retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result !== 0 || outCursor[0] <= 0) {
|
||||||
|
if (result !== -3) {
|
||||||
|
await this.printLogs(true)
|
||||||
|
this.writeLog(
|
||||||
|
`openMessageCursorLiteWithKey failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return { success: false, error: `创建游标失败: ${result}` }
|
||||||
|
}
|
||||||
|
return { success: true, cursor: outCursor[0] }
|
||||||
|
} catch (e) {
|
||||||
|
await this.printLogs(true)
|
||||||
|
this.writeLog(`openMessageCursorLiteWithKey exception: ${String(e)}`, true)
|
||||||
|
return { success: false, error: '创建游标异常,请查看日志' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchMessageBatch(cursor: number): Promise<{ success: boolean; rows?: any[]; hasMore?: boolean; error?: string }> {
|
async fetchMessageBatch(cursor: number): Promise<{ success: boolean; rows?: any[]; hasMore?: boolean; error?: string }> {
|
||||||
if (!this.ensureReady()) {
|
if (!this.ensureReady()) {
|
||||||
return { success: false, error: 'WCDB 未连接' }
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
@@ -4305,6 +4530,243 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async aiGetRecentMessages(options: {
|
||||||
|
sessionId: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetRecentMessages) return { success: false, error: '当前数据服务版本不支持 AI 最近消息查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetRecentMessages(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
limit: options.limit || 120,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 最近消息查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 最近消息查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessagesBefore(options: {
|
||||||
|
sessionId: string
|
||||||
|
beforeId?: number
|
||||||
|
beforeLocalId?: number
|
||||||
|
beforeCreateTime?: number
|
||||||
|
beforeSortSeq?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetMessagesBefore) return { success: false, error: '当前数据服务版本不支持 AI 前向消息查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetMessagesBefore(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
before_id: options.beforeId || 0,
|
||||||
|
before_local_id: options.beforeLocalId || options.beforeId || 0,
|
||||||
|
before_create_time: options.beforeCreateTime || 0,
|
||||||
|
before_sort_seq: options.beforeSortSeq || 0,
|
||||||
|
limit: options.limit || 120,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 前向消息查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 前向消息查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessagesAfter(options: {
|
||||||
|
sessionId: string
|
||||||
|
afterId?: number
|
||||||
|
afterLocalId?: number
|
||||||
|
afterCreateTime?: number
|
||||||
|
afterSortSeq?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetMessagesAfter) return { success: false, error: '当前数据服务版本不支持 AI 后向消息查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetMessagesAfter(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
after_id: options.afterId || 0,
|
||||||
|
after_local_id: options.afterLocalId || options.afterId || 0,
|
||||||
|
after_create_time: options.afterCreateTime || 0,
|
||||||
|
after_sort_seq: options.afterSortSeq || 0,
|
||||||
|
limit: options.limit || 120,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 后向消息查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 后向消息查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessageContext(options: {
|
||||||
|
sessionId: string
|
||||||
|
messageIds: number[]
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetMessageContext) return { success: false, error: '当前数据服务版本不支持 AI 消息上下文查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetMessageContext(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
message_ids: Array.isArray(options.messageIds) ? options.messageIds : []
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 消息上下文查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 消息上下文查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSearchMessageContext(options: {
|
||||||
|
sessionId: string
|
||||||
|
messageIds: number[]
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetSearchMessageContext) return { success: false, error: '当前数据服务版本不支持 AI 搜索上下文查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetSearchMessageContext(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
message_ids: Array.isArray(options.messageIds) ? options.messageIds : []
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 搜索上下文查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 搜索上下文查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetConversationBetween(options: {
|
||||||
|
sessionId: string
|
||||||
|
memberId1?: number
|
||||||
|
memberId2?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetConversationBetween) return { success: false, error: '当前数据服务版本不支持 AI 双人对话查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetConversationBetween(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
member_id1: options.memberId1 || 0,
|
||||||
|
member_id2: options.memberId2 || 0,
|
||||||
|
limit: options.limit || 120,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 双人对话查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 双人对话查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiSearchSessions(options: {
|
||||||
|
keyword?: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiSearchSessions) return { success: false, error: '当前数据服务版本不支持 AI 会话搜索' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiSearchSessions(this.handle, JSON.stringify({
|
||||||
|
keyword: options.keyword || '',
|
||||||
|
limit: options.limit || 20,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话搜索失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 会话搜索失败' }
|
||||||
|
const rows = JSON.parse(jsonStr)
|
||||||
|
return { success: true, rows: Array.isArray(rows) ? rows : [] }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSessionMessages(options: {
|
||||||
|
sessionId: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetSessionMessages) return { success: false, error: '当前数据服务版本不支持 AI 会话消息查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetSessionMessages(this.handle, JSON.stringify({
|
||||||
|
session_id: options.sessionId || '',
|
||||||
|
limit: options.limit || 500,
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话消息查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 会话消息查询失败' }
|
||||||
|
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSessionSummaries(options: {
|
||||||
|
sessionIds?: string[]
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||||
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
|
if (!this.wcdbAiGetSessionSummaries) return { success: false, error: '当前数据服务版本不支持 AI 会话摘要查询' }
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbAiGetSessionSummaries(this.handle, JSON.stringify({
|
||||||
|
session_ids_json: JSON.stringify(options.sessionIds || []),
|
||||||
|
begin_timestamp: options.beginTimestamp || 0,
|
||||||
|
end_timestamp: options.endTimestamp || 0
|
||||||
|
}), outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话摘要查询失败: ${result}` }
|
||||||
|
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (!jsonStr) return { success: false, error: '解析 AI 会话摘要查询失败' }
|
||||||
|
const data = JSON.parse(jsonStr)
|
||||||
|
return { success: true, data }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' }
|
if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' }
|
||||||
|
|||||||
@@ -468,6 +468,24 @@ export class WcdbService {
|
|||||||
return this.callWorker('openMessageCursor', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
return this.callWorker('openMessageCursor', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openMessageCursorWithKey(
|
||||||
|
sessionId: string,
|
||||||
|
batchSize: number,
|
||||||
|
ascending: boolean,
|
||||||
|
beginTimestamp: number,
|
||||||
|
endTimestamp: number,
|
||||||
|
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||||
|
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||||
|
return this.callWorker('openMessageCursorWithKey', {
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开轻量级消息游标
|
* 打开轻量级消息游标
|
||||||
*/
|
*/
|
||||||
@@ -475,6 +493,24 @@ export class WcdbService {
|
|||||||
return this.callWorker('openMessageCursorLite', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
return this.callWorker('openMessageCursorLite', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openMessageCursorLiteWithKey(
|
||||||
|
sessionId: string,
|
||||||
|
batchSize: number,
|
||||||
|
ascending: boolean,
|
||||||
|
beginTimestamp: number,
|
||||||
|
endTimestamp: number,
|
||||||
|
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||||
|
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||||
|
return this.callWorker('openMessageCursorLiteWithKey', {
|
||||||
|
sessionId,
|
||||||
|
batchSize,
|
||||||
|
ascending,
|
||||||
|
beginTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取下一批消息
|
* 获取下一批消息
|
||||||
*/
|
*/
|
||||||
@@ -616,6 +652,92 @@ export class WcdbService {
|
|||||||
return this.callWorker('aiQuerySourceRefs', { options })
|
return this.callWorker('aiQuerySourceRefs', { options })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async aiGetRecentMessages(options: {
|
||||||
|
sessionId: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetRecentMessages', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessagesBefore(options: {
|
||||||
|
sessionId: string
|
||||||
|
beforeId?: number
|
||||||
|
beforeLocalId?: number
|
||||||
|
beforeCreateTime?: number
|
||||||
|
beforeSortSeq?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetMessagesBefore', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessagesAfter(options: {
|
||||||
|
sessionId: string
|
||||||
|
afterId?: number
|
||||||
|
afterLocalId?: number
|
||||||
|
afterCreateTime?: number
|
||||||
|
afterSortSeq?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetMessagesAfter', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetMessageContext(options: {
|
||||||
|
sessionId: string
|
||||||
|
messageIds: number[]
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetMessageContext', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSearchMessageContext(options: {
|
||||||
|
sessionId: string
|
||||||
|
messageIds: number[]
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetSearchMessageContext', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetConversationBetween(options: {
|
||||||
|
sessionId: string
|
||||||
|
memberId1?: number
|
||||||
|
memberId2?: number
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetConversationBetween', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiSearchSessions(options: {
|
||||||
|
keyword?: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiSearchSessions', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSessionMessages(options: {
|
||||||
|
sessionId: string
|
||||||
|
limit?: number
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||||
|
return this.callWorker('aiGetSessionMessages', { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
async aiGetSessionSummaries(options: {
|
||||||
|
sessionIds?: string[]
|
||||||
|
beginTimestamp?: number
|
||||||
|
endTimestamp?: number
|
||||||
|
}): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||||
|
return this.callWorker('aiGetSessionSummaries', { options })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取语音数据
|
* 获取语音数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -164,9 +164,29 @@ if (parentPort) {
|
|||||||
case 'openMessageCursor':
|
case 'openMessageCursor':
|
||||||
result = await core.openMessageCursor(payload.sessionId, payload.batchSize, payload.ascending, payload.beginTimestamp, payload.endTimestamp)
|
result = await core.openMessageCursor(payload.sessionId, payload.batchSize, payload.ascending, payload.beginTimestamp, payload.endTimestamp)
|
||||||
break
|
break
|
||||||
|
case 'openMessageCursorWithKey':
|
||||||
|
result = await core.openMessageCursorWithKey(
|
||||||
|
payload.sessionId,
|
||||||
|
payload.batchSize,
|
||||||
|
payload.ascending,
|
||||||
|
payload.beginTimestamp,
|
||||||
|
payload.endTimestamp,
|
||||||
|
payload.key
|
||||||
|
)
|
||||||
|
break
|
||||||
case 'openMessageCursorLite':
|
case 'openMessageCursorLite':
|
||||||
result = await core.openMessageCursorLite(payload.sessionId, payload.batchSize, payload.ascending, payload.beginTimestamp, payload.endTimestamp)
|
result = await core.openMessageCursorLite(payload.sessionId, payload.batchSize, payload.ascending, payload.beginTimestamp, payload.endTimestamp)
|
||||||
break
|
break
|
||||||
|
case 'openMessageCursorLiteWithKey':
|
||||||
|
result = await core.openMessageCursorLiteWithKey(
|
||||||
|
payload.sessionId,
|
||||||
|
payload.batchSize,
|
||||||
|
payload.ascending,
|
||||||
|
payload.beginTimestamp,
|
||||||
|
payload.endTimestamp,
|
||||||
|
payload.key
|
||||||
|
)
|
||||||
|
break
|
||||||
case 'fetchMessageBatch':
|
case 'fetchMessageBatch':
|
||||||
result = await core.fetchMessageBatch(payload.cursor)
|
result = await core.fetchMessageBatch(payload.cursor)
|
||||||
break
|
break
|
||||||
@@ -215,6 +235,33 @@ if (parentPort) {
|
|||||||
case 'aiQuerySourceRefs':
|
case 'aiQuerySourceRefs':
|
||||||
result = await core.aiQuerySourceRefs(payload.options || {})
|
result = await core.aiQuerySourceRefs(payload.options || {})
|
||||||
break
|
break
|
||||||
|
case 'aiGetRecentMessages':
|
||||||
|
result = await core.aiGetRecentMessages(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetMessagesBefore':
|
||||||
|
result = await core.aiGetMessagesBefore(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetMessagesAfter':
|
||||||
|
result = await core.aiGetMessagesAfter(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetMessageContext':
|
||||||
|
result = await core.aiGetMessageContext(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetSearchMessageContext':
|
||||||
|
result = await core.aiGetSearchMessageContext(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetConversationBetween':
|
||||||
|
result = await core.aiGetConversationBetween(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiSearchSessions':
|
||||||
|
result = await core.aiSearchSessions(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetSessionMessages':
|
||||||
|
result = await core.aiGetSessionMessages(payload.options || {})
|
||||||
|
break
|
||||||
|
case 'aiGetSessionSummaries':
|
||||||
|
result = await core.aiGetSessionSummaries(payload.options || {})
|
||||||
|
break
|
||||||
case 'getVoiceData':
|
case 'getVoiceData':
|
||||||
result = await core.getVoiceData(payload.sessionId, payload.createTime, payload.candidates, payload.localId, payload.svrId)
|
result = await core.getVoiceData(payload.sessionId, payload.createTime, payload.candidates, payload.localId, payload.svrId)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,15 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
Download,
|
Download,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
Play,
|
Play,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
|
||||||
Send,
|
Send,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
SquareTerminal,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
Wrench
|
ChevronDown,
|
||||||
|
ChevronUp
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type {
|
import type {
|
||||||
AiConversation,
|
AiConversation,
|
||||||
@@ -25,10 +26,9 @@ import type {
|
|||||||
ToolCatalogEntry
|
ToolCatalogEntry
|
||||||
} from '../types/aiAnalysis'
|
} from '../types/aiAnalysis'
|
||||||
import { useAiRuntimeStore } from '../stores/aiRuntimeStore'
|
import { useAiRuntimeStore } from '../stores/aiRuntimeStore'
|
||||||
import type { AgentStreamChunk } from '../types/electron'
|
|
||||||
import './AiAnalysisPage.scss'
|
import './AiAnalysisPage.scss'
|
||||||
|
|
||||||
type MainTab = 'chat' | 'sql' | 'tool'
|
type MainTab = 'chat' | 'sql'
|
||||||
type ScopeMode = 'global' | 'contact' | 'session'
|
type ScopeMode = 'global' | 'contact' | 'session'
|
||||||
|
|
||||||
function formatDateTime(ts: number): string {
|
function formatDateTime(ts: number): string {
|
||||||
@@ -47,7 +47,10 @@ function normalizeText(value: unknown, fallback = ''): string {
|
|||||||
return text || fallback
|
return text || fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSqlTarget(schema: SqlSchemaPayload | null, key: string): { kind: 'message' | 'contact' | 'biz'; path: string | null } | null {
|
function extractSqlTarget(
|
||||||
|
schema: SqlSchemaPayload | null,
|
||||||
|
key: string
|
||||||
|
): { kind: 'message' | 'contact' | 'biz'; path: string | null } | null {
|
||||||
if (!schema) return null
|
if (!schema) return null
|
||||||
for (const source of schema.sources) {
|
for (const source of schema.sources) {
|
||||||
const sourceKey = `${source.kind}:${source.path || ''}`
|
const sourceKey = `${source.kind}:${source.path || ''}`
|
||||||
@@ -63,7 +66,9 @@ function toCsv(rows: Record<string, unknown>[], columns: string[]): string {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
const header = columns.map((column) => esc(column)).join(',')
|
const header = columns.map((column) => esc(column)).join(',')
|
||||||
const body = rows.map((row) => columns.map((column) => esc(row[column])).join(',')).join('\n')
|
const body = rows
|
||||||
|
.map((row) => columns.map((column) => esc(row[column])).join(','))
|
||||||
|
.join('\n')
|
||||||
return `${header}\n${body}`
|
return `${header}\n${body}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +77,9 @@ function AiAnalysisPage() {
|
|||||||
const agentApi = window.electronAPI.agentApi
|
const agentApi = window.electronAPI.agentApi
|
||||||
const assistantApi = window.electronAPI.assistantApi
|
const assistantApi = window.electronAPI.assistantApi
|
||||||
const skillApi = window.electronAPI.skillApi
|
const skillApi = window.electronAPI.skillApi
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<MainTab>('chat')
|
const [activeTab, setActiveTab] = useState<MainTab>('chat')
|
||||||
|
const [showDataPanel, setShowDataPanel] = useState(true)
|
||||||
const [scopeMode, setScopeMode] = useState<ScopeMode>('global')
|
const [scopeMode, setScopeMode] = useState<ScopeMode>('global')
|
||||||
const [scopeTarget, setScopeTarget] = useState('')
|
const [scopeTarget, setScopeTarget] = useState('')
|
||||||
const [conversations, setConversations] = useState<AiConversation[]>([])
|
const [conversations, setConversations] = useState<AiConversation[]>([])
|
||||||
@@ -102,34 +109,39 @@ function AiAnalysisPage() {
|
|||||||
const [sqlPage, setSqlPage] = useState(1)
|
const [sqlPage, setSqlPage] = useState(1)
|
||||||
const [sqlPageSize] = useState(50)
|
const [sqlPageSize] = useState(50)
|
||||||
|
|
||||||
const [toolCatalog, setToolCatalog] = useState<ToolCatalogEntry[]>([])
|
const messageContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const [toolName, setToolName] = useState('')
|
|
||||||
const [toolArgsText, setToolArgsText] = useState('{}')
|
|
||||||
const [toolRunning, setToolRunning] = useState(false)
|
|
||||||
const [toolOutput, setToolOutput] = useState('')
|
|
||||||
|
|
||||||
const sqlRunIdRef = useRef('')
|
|
||||||
const sqlGeneratedRef = useRef('')
|
const sqlGeneratedRef = useRef('')
|
||||||
const messageEndRef = useRef<HTMLDivElement | null>(null)
|
const [showScrollBottom, setShowScrollBottom] = useState(false)
|
||||||
|
|
||||||
const activeRunId = useAiRuntimeStore((state) => state.activeRunId)
|
|
||||||
const runtimeState = useAiRuntimeStore((state) => (
|
const runtimeState = useAiRuntimeStore((state) => (
|
||||||
currentConversationId ? state.states[currentConversationId] : undefined
|
currentConversationId ? state.states[currentConversationId] : undefined
|
||||||
))
|
))
|
||||||
|
const activeRequestId = useAiRuntimeStore((state) => state.activeRequestId)
|
||||||
const startRun = useAiRuntimeStore((state) => state.startRun)
|
const startRun = useAiRuntimeStore((state) => state.startRun)
|
||||||
const appendChunk = useAiRuntimeStore((state) => state.appendChunk)
|
const appendChunk = useAiRuntimeStore((state) => state.appendChunk)
|
||||||
const finishRun = useAiRuntimeStore((state) => state.finishRun)
|
const completeRun = useAiRuntimeStore((state) => state.completeRun)
|
||||||
|
|
||||||
const selectedAssistant = useMemo(
|
const selectedAssistant = useMemo(
|
||||||
() => assistants.find((assistant) => assistant.id === selectedAssistantId) || null,
|
() => assistants.find((assistant) => assistant.id === selectedAssistantId) || null,
|
||||||
[assistants, selectedAssistantId]
|
[assistants, selectedAssistantId]
|
||||||
)
|
)
|
||||||
|
const showThinkBlocks = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
if (query.get('debugThink') === '1') return true
|
||||||
|
return window.localStorage.getItem('wf_ai_debug_think') === '1'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const slashSuggestions = useMemo(() => {
|
const slashSuggestions = useMemo(() => {
|
||||||
const text = normalizeText(input)
|
const text = normalizeText(input)
|
||||||
if (!text.startsWith('/')) return []
|
if (!text.startsWith('/')) return []
|
||||||
const key = text.slice(1).toLowerCase()
|
const key = text.slice(1).toLowerCase()
|
||||||
return skills.filter((skill) => !key || skill.id.includes(key) || skill.name.toLowerCase().includes(key)).slice(0, 8)
|
return skills
|
||||||
|
.filter((skill) => !key || skill.id.includes(key) || skill.name.toLowerCase().includes(key))
|
||||||
|
.slice(0, 8)
|
||||||
}, [input, skills])
|
}, [input, skills])
|
||||||
|
|
||||||
const mentionSuggestions = useMemo(() => {
|
const mentionSuggestions = useMemo(() => {
|
||||||
@@ -137,7 +149,11 @@ function AiAnalysisPage() {
|
|||||||
if (!match) return []
|
if (!match) return []
|
||||||
const keyword = match[1].toLowerCase()
|
const keyword = match[1].toLowerCase()
|
||||||
return contacts
|
return contacts
|
||||||
.filter((contact) => !keyword || contact.displayName.toLowerCase().includes(keyword) || contact.username.toLowerCase().includes(keyword))
|
.filter((contact) =>
|
||||||
|
!keyword ||
|
||||||
|
contact.displayName.toLowerCase().includes(keyword) ||
|
||||||
|
contact.username.toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
}, [contacts, input])
|
}, [contacts, input])
|
||||||
|
|
||||||
@@ -177,7 +193,9 @@ function AiAnalysisPage() {
|
|||||||
}
|
}
|
||||||
const list = res.conversations || []
|
const list = res.conversations || []
|
||||||
setConversations(list)
|
setConversations(list)
|
||||||
if (!currentConversationId && list.length > 0) setCurrentConversationId(list[0].conversationId)
|
if (!currentConversationId && list.length > 0) {
|
||||||
|
setCurrentConversationId(list[0].conversationId)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingConversations(false)
|
setLoadingConversations(false)
|
||||||
}
|
}
|
||||||
@@ -206,7 +224,11 @@ function AiAnalysisPage() {
|
|||||||
])
|
])
|
||||||
setAssistants(assistantList || [])
|
setAssistants(assistantList || [])
|
||||||
setSkills(skillList || [])
|
setSkills(skillList || [])
|
||||||
if (assistantList && assistantList.length > 0 && !assistantList.some((item) => item.id === selectedAssistantId)) {
|
if (
|
||||||
|
assistantList &&
|
||||||
|
assistantList.length > 0 &&
|
||||||
|
!assistantList.some((item) => item.id === selectedAssistantId)
|
||||||
|
) {
|
||||||
setSelectedAssistantId(assistantList[0].id)
|
setSelectedAssistantId(assistantList[0].id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -221,7 +243,12 @@ function AiAnalysisPage() {
|
|||||||
const list = res.contacts
|
const list = res.contacts
|
||||||
.map((contact) => ({
|
.map((contact) => ({
|
||||||
username: normalizeText(contact.username),
|
username: normalizeText(contact.username),
|
||||||
displayName: normalizeText(contact.displayName || contact.remark || contact.nickname || contact.username)
|
displayName: normalizeText(
|
||||||
|
contact.displayName ||
|
||||||
|
contact.remark ||
|
||||||
|
contact.nickname ||
|
||||||
|
contact.username
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
.filter((contact) => contact.username && contact.displayName)
|
.filter((contact) => contact.username && contact.displayName)
|
||||||
.slice(0, 300)
|
.slice(0, 300)
|
||||||
@@ -231,18 +258,6 @@ function AiAnalysisPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadToolCatalog = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const catalog = await aiApi.getToolCatalog()
|
|
||||||
setToolCatalog(Array.isArray(catalog) ? catalog : [])
|
|
||||||
if (!toolName && Array.isArray(catalog) && catalog.length > 0) {
|
|
||||||
setToolName(catalog[0].name)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setErrorText(String((error as Error)?.message || error))
|
|
||||||
}
|
|
||||||
}, [aiApi, toolName])
|
|
||||||
|
|
||||||
const loadSchema = useCallback(async () => {
|
const loadSchema = useCallback(async () => {
|
||||||
const res = await window.electronAPI.chat.getSchema({})
|
const res = await window.electronAPI.chat.getSchema({})
|
||||||
if (!res.success || !res.schema) {
|
if (!res.success || !res.schema) {
|
||||||
@@ -268,52 +283,35 @@ function AiAnalysisPage() {
|
|||||||
}, [currentConversationId, loadMessages])
|
}, [currentConversationId, loadMessages])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'sql' && !sqlSchema) void loadSchema()
|
if (activeTab === 'sql' && !sqlSchema) {
|
||||||
if (activeTab === 'tool' && toolCatalog.length === 0) void loadToolCatalog()
|
void loadSchema()
|
||||||
}, [activeTab, sqlSchema, loadSchema, toolCatalog.length, loadToolCatalog])
|
}
|
||||||
|
}, [activeTab, sqlSchema, loadSchema])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const off = agentApi.onStream((chunk: AgentStreamChunk) => {
|
const panel = messageContainerRef.current
|
||||||
if (sqlRunIdRef.current && chunk.runId === sqlRunIdRef.current) {
|
if (!panel) return
|
||||||
if (chunk.type === 'content') {
|
const onScroll = () => {
|
||||||
setSqlGenerated((prev) => {
|
const distance = panel.scrollHeight - panel.scrollTop - panel.clientHeight
|
||||||
const next = `${prev}${chunk.content || ''}`
|
setShowScrollBottom(distance > 64)
|
||||||
sqlGeneratedRef.current = next
|
}
|
||||||
return next
|
panel.addEventListener('scroll', onScroll)
|
||||||
})
|
onScroll()
|
||||||
} else if (chunk.type === 'done') {
|
return () => panel.removeEventListener('scroll', onScroll)
|
||||||
setSqlGenerating(false)
|
}, [messageContainerRef.current])
|
||||||
if (normalizeText(sqlGeneratedRef.current)) {
|
|
||||||
setSqlHistory((prev) => [sqlGeneratedRef.current.trim(), ...prev].slice(0, 30))
|
|
||||||
}
|
|
||||||
sqlRunIdRef.current = ''
|
|
||||||
} else if (chunk.type === 'error') {
|
|
||||||
setSqlGenerating(false)
|
|
||||||
setSqlError(chunk.error || 'SQL 生成失败')
|
|
||||||
sqlRunIdRef.current = ''
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const conversationId = normalizeText(chunk.conversationId, currentConversationId)
|
|
||||||
if (!conversationId) return
|
|
||||||
appendChunk(conversationId, chunk)
|
|
||||||
if (chunk.type === 'done' || chunk.type === 'error' || chunk.isFinished) {
|
|
||||||
finishRun(conversationId)
|
|
||||||
void loadMessages(conversationId)
|
|
||||||
void loadConversations()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return () => off()
|
|
||||||
}, [agentApi, appendChunk, currentConversationId, finishRun, loadConversations, loadMessages])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messageEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
|
const panel = messageContainerRef.current
|
||||||
}, [messages, runtimeState?.draft, runtimeState?.chunks.length])
|
if (!panel) return
|
||||||
|
panel.scrollTo({ top: panel.scrollHeight, behavior: 'smooth' })
|
||||||
|
}, [messages, runtimeState?.blocks.length, runtimeState?.draft])
|
||||||
|
|
||||||
const ensureConversation = useCallback(async (): Promise<string> => {
|
const ensureConversation = useCallback(async (): Promise<string> => {
|
||||||
if (currentConversationId) return currentConversationId
|
if (currentConversationId) return currentConversationId
|
||||||
const created = await aiApi.createConversation({ title: '新的 AI 对话' })
|
const created = await aiApi.createConversation({ title: '新的 AI 对话' })
|
||||||
if (!created.success || !created.conversationId) throw new Error(created.error || '创建会话失败')
|
if (!created.success || !created.conversationId) {
|
||||||
|
throw new Error(created.error || '创建会话失败')
|
||||||
|
}
|
||||||
setCurrentConversationId(created.conversationId)
|
setCurrentConversationId(created.conversationId)
|
||||||
await loadConversations()
|
await loadConversations()
|
||||||
return created.conversationId
|
return created.conversationId
|
||||||
@@ -361,8 +359,10 @@ function AiAnalysisPage() {
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
const text = normalizeText(input)
|
const text = normalizeText(input)
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
setErrorText('')
|
setErrorText('')
|
||||||
const conversationId = await ensureConversation()
|
const conversationId = await ensureConversation()
|
||||||
|
|
||||||
setMessages((prev) => ([
|
setMessages((prev) => ([
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -377,25 +377,37 @@ function AiAnalysisPage() {
|
|||||||
}
|
}
|
||||||
]))
|
]))
|
||||||
setInput('')
|
setInput('')
|
||||||
const run = await agentApi.runStream({
|
|
||||||
|
const run = agentApi.runStream({
|
||||||
mode: 'chat',
|
mode: 'chat',
|
||||||
conversationId,
|
conversationId,
|
||||||
userInput: text,
|
userInput: text,
|
||||||
assistantId: selectedAssistantId,
|
assistantId: selectedAssistantId,
|
||||||
activeSkillId: selectedSkillId || undefined,
|
activeSkillId: selectedSkillId || undefined,
|
||||||
chatScope: scopeMode === 'session' ? 'private' : 'private'
|
chatScope: scopeMode === 'session' ? 'private' : 'private'
|
||||||
|
}, (chunk) => {
|
||||||
|
appendChunk(conversationId, chunk)
|
||||||
})
|
})
|
||||||
if (!run.success || !run.runId) {
|
|
||||||
setErrorText('启动失败')
|
startRun(conversationId, run.requestId)
|
||||||
return
|
const result = await run.promise
|
||||||
|
completeRun(conversationId, result.result || { error: result.error, canceled: false })
|
||||||
|
|
||||||
|
if (!result.success && !result.result?.canceled) {
|
||||||
|
setErrorText(result.error || '执行失败')
|
||||||
}
|
}
|
||||||
startRun(conversationId, run.runId)
|
|
||||||
|
await loadMessages(conversationId)
|
||||||
|
await loadConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStop = async () => {
|
const handleStop = async () => {
|
||||||
if (!currentConversationId) return
|
if (!currentConversationId) return
|
||||||
await agentApi.abort({ runId: activeRunId || undefined, conversationId: currentConversationId })
|
const requestId = runtimeState?.requestId || activeRequestId
|
||||||
finishRun(currentConversationId)
|
if (!requestId) return
|
||||||
|
setErrorText('')
|
||||||
|
await agentApi.abort(requestId)
|
||||||
|
completeRun(currentConversationId, { canceled: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportConversation = async () => {
|
const handleExportConversation = async () => {
|
||||||
@@ -409,11 +421,6 @@ function AiAnalysisPage() {
|
|||||||
window.alert('会话 Markdown 已复制到剪贴板')
|
window.alert('会话 Markdown 已复制到剪贴板')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenLog = async () => {
|
|
||||||
const logPath = await window.electronAPI.log.getPath()
|
|
||||||
await window.electronAPI.shell.openPath(logPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGenerateSql = async () => {
|
const handleGenerateSql = async () => {
|
||||||
const prompt = normalizeText(sqlPrompt)
|
const prompt = normalizeText(sqlPrompt)
|
||||||
if (!prompt) return
|
if (!prompt) return
|
||||||
@@ -421,21 +428,35 @@ function AiAnalysisPage() {
|
|||||||
setSqlGenerated('')
|
setSqlGenerated('')
|
||||||
sqlGeneratedRef.current = ''
|
sqlGeneratedRef.current = ''
|
||||||
setSqlError('')
|
setSqlError('')
|
||||||
|
|
||||||
const target = extractSqlTarget(sqlSchema, sqlTargetKey)
|
const target = extractSqlTarget(sqlSchema, sqlTargetKey)
|
||||||
const run = await agentApi.runStream({
|
const run = agentApi.runStream({
|
||||||
mode: 'sql',
|
mode: 'sql',
|
||||||
userInput: prompt,
|
userInput: prompt,
|
||||||
sqlContext: {
|
sqlContext: {
|
||||||
schemaText: sqlSchemaText,
|
schemaText: sqlSchemaText,
|
||||||
targetHint: target ? `${target.kind}:${target.path || ''}` : ''
|
targetHint: target ? `${target.kind}:${target.path || ''}` : ''
|
||||||
}
|
}
|
||||||
|
}, (chunk) => {
|
||||||
|
if (chunk.type === 'content') {
|
||||||
|
setSqlGenerated((prev) => {
|
||||||
|
const next = `${prev}${chunk.content || ''}`
|
||||||
|
sqlGeneratedRef.current = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if (!run.success || !run.runId) {
|
|
||||||
setSqlGenerating(false)
|
const result = await run.promise
|
||||||
setSqlError('SQL 生成失败')
|
setSqlGenerating(false)
|
||||||
|
if (!result.success) {
|
||||||
|
setSqlError(result.error || 'SQL 生成失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sqlRunIdRef.current = run.runId
|
|
||||||
|
if (normalizeText(sqlGeneratedRef.current)) {
|
||||||
|
setSqlHistory((prev) => [sqlGeneratedRef.current.trim(), ...prev].slice(0, 30))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExecuteSql = async () => {
|
const handleExecuteSql = async () => {
|
||||||
@@ -478,54 +499,32 @@ function AiAnalysisPage() {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRunTool = async () => {
|
|
||||||
setToolRunning(true)
|
|
||||||
try {
|
|
||||||
const args = JSON.parse(toolArgsText || '{}')
|
|
||||||
const result = await aiApi.executeTool({ name: toolName, args })
|
|
||||||
setToolOutput(JSON.stringify(result, null, 2))
|
|
||||||
} catch (error) {
|
|
||||||
setToolOutput(String((error as Error)?.message || error))
|
|
||||||
} finally {
|
|
||||||
setToolRunning(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedTools = useMemo(() => ({
|
|
||||||
core: toolCatalog.filter((item) => item.category === 'core'),
|
|
||||||
analysis: toolCatalog.filter((item) => item.category === 'analysis')
|
|
||||||
}), [toolCatalog])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ai-analysis-v2">
|
<div className="ai-analysis-chatlab">
|
||||||
<header className="ai-header">
|
<header className="ai-topbar">
|
||||||
<div className="left">
|
<div className="title-group">
|
||||||
<Sparkles size={18} />
|
<Sparkles size={18} />
|
||||||
<h1>AI Analysis</h1>
|
<h1>AI Analysis</h1>
|
||||||
<span>Chat Explorer + SQL Lab + Tool Test</span>
|
<span>ChatLab 交互同构模式</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs">
|
<div className="top-actions">
|
||||||
<button type="button" className={activeTab === 'chat' ? 'active' : ''} onClick={() => setActiveTab('chat')}>
|
<button type="button" className={activeTab === 'chat' ? 'active' : ''} onClick={() => setActiveTab('chat')}>
|
||||||
<Bot size={14} />
|
<Bot size={14} />
|
||||||
Chat Explorer
|
AI Chat
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={activeTab === 'sql' ? 'active' : ''} onClick={() => setActiveTab('sql')}>
|
<button type="button" className={activeTab === 'sql' ? 'active' : ''} onClick={() => setActiveTab('sql')}>
|
||||||
<Database size={14} />
|
<Database size={14} />
|
||||||
SQL Lab
|
SQL Lab
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={activeTab === 'tool' ? 'active' : ''} onClick={() => setActiveTab('tool')}>
|
|
||||||
<SquareTerminal size={14} />
|
|
||||||
Tool Test
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{activeTab === 'chat' && (
|
{activeTab === 'chat' && (
|
||||||
<div className="chat-layout">
|
<div className={`chat-shell ${showDataPanel ? 'with-data' : ''}`}>
|
||||||
<aside className="conversation-panel">
|
<aside className="conversation-sidebar">
|
||||||
<div className="panel-head">
|
<div className="sidebar-head">
|
||||||
<h3>会话</h3>
|
<h3>会话</h3>
|
||||||
<button type="button" onClick={() => void handleCreateConversation()} title="新建">+</button>
|
<button type="button" onClick={() => void handleCreateConversation()} title="新建会话">+</button>
|
||||||
</div>
|
</div>
|
||||||
{loadingConversations ? (
|
{loadingConversations ? (
|
||||||
<div className="empty"><Loader2 className="spin" size={14} /> 加载中...</div>
|
<div className="empty"><Loader2 className="spin" size={14} /> 加载中...</div>
|
||||||
@@ -542,9 +541,11 @@ function AiAnalysisPage() {
|
|||||||
<strong>{conversation.title || '新的 AI 对话'}</strong>
|
<strong>{conversation.title || '新的 AI 对话'}</strong>
|
||||||
<small>{formatDateTime(conversation.updatedAt)}</small>
|
<small>{formatDateTime(conversation.updatedAt)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="ops">
|
<div className="ops" onClick={(event) => event.stopPropagation()}>
|
||||||
<span onClick={(event) => { event.stopPropagation(); void handleRenameConversation(conversation.conversationId) }}>重命名</span>
|
<span onClick={() => void handleRenameConversation(conversation.conversationId)}>重命名</span>
|
||||||
<span onClick={(event) => { event.stopPropagation(); void handleDeleteConversation(conversation.conversationId) }}><Trash2 size={12} /></span>
|
<span onClick={() => void handleDeleteConversation(conversation.conversationId)}>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -553,9 +554,9 @@ function AiAnalysisPage() {
|
|||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="chat-main">
|
<section className="chat-main-panel">
|
||||||
<div className="chat-toolbar">
|
<div className="chat-toolbar">
|
||||||
<div className="row">
|
<div className="controls-row">
|
||||||
<label>助手</label>
|
<label>助手</label>
|
||||||
<select value={selectedAssistantId} onChange={(event) => setSelectedAssistantId(event.target.value)}>
|
<select value={selectedAssistantId} onChange={(event) => setSelectedAssistantId(event.target.value)}>
|
||||||
{assistants.map((assistant) => (
|
{assistants.map((assistant) => (
|
||||||
@@ -583,7 +584,17 @@ function AiAnalysisPage() {
|
|||||||
placeholder={scopeMode === 'contact' ? '联系人昵称/账号' : '会话ID'}
|
placeholder={scopeMode === 'contact' ? '联系人昵称/账号' : '会话ID'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost"
|
||||||
|
onClick={() => setShowDataPanel((prev) => !prev)}
|
||||||
|
title={showDataPanel ? '隐藏数据面板' : '显示数据面板'}
|
||||||
|
>
|
||||||
|
{showDataPanel ? <PanelLeftClose size={14} /> : <PanelLeftOpen size={14} />}
|
||||||
|
数据源
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedAssistant?.presetQuestions?.length ? (
|
{selectedAssistant?.presetQuestions?.length ? (
|
||||||
<div className="preset-row">
|
<div className="preset-row">
|
||||||
{selectedAssistant.presetQuestions.slice(0, 8).map((question) => (
|
{selectedAssistant.presetQuestions.slice(0, 8).map((question) => (
|
||||||
@@ -593,69 +604,121 @@ function AiAnalysisPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="message-panel">
|
<div className="messages-wrap" ref={messageContainerRef}>
|
||||||
{loadingMessages ? (
|
{loadingMessages ? (
|
||||||
<div className="empty"><Loader2 className="spin" size={14} /> 加载消息...</div>
|
<div className="empty"><Loader2 className="spin" size={14} /> 加载消息...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="message-list">
|
<>
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<div key={message.messageId} className={`msg ${message.role === 'user' ? 'user' : message.role}`}>
|
<article key={message.messageId} className={`message-card ${message.role === 'user' ? 'user' : 'assistant'}`}>
|
||||||
<div className="head">{message.role === 'user' ? '你' : message.role === 'assistant' ? '助手' : message.role}</div>
|
<header>
|
||||||
<div className="body">{message.content || '(空)'}</div>
|
<span>{message.role === 'user' ? '你' : '助手'}</span>
|
||||||
</div>
|
<time>{formatDateTime(message.createdAt)}</time>
|
||||||
|
</header>
|
||||||
|
<div className="message-body">{message.content || '(空)'}</div>
|
||||||
|
{message.role === 'assistant' && Array.isArray(message.toolTrace) && message.toolTrace.length > 0 ? (
|
||||||
|
<details className="tool-trace">
|
||||||
|
<summary>工具调用轨迹({message.toolTrace.length})</summary>
|
||||||
|
<ul>
|
||||||
|
{message.toolTrace.map((trace, index) => (
|
||||||
|
<li key={`${message.messageId}-trace-${index}`}>
|
||||||
|
{String(trace?.toolName || 'unknown')} · {String(trace?.status || 'unknown')} · {Number(trace?.durationMs || 0)}ms
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{runtimeState?.running && runtimeState?.chunks?.length ? (
|
{runtimeState?.running ? (
|
||||||
<div className="runtime-cards">
|
<article className="message-card assistant streaming">
|
||||||
{runtimeState.chunks
|
<header>
|
||||||
.filter((chunk) => chunk.type === 'tool_start' || chunk.type === 'tool_result' || chunk.type === 'error')
|
<span>助手(实时)</span>
|
||||||
.slice(-16)
|
<time>{runtimeState?.status?.phase || 'thinking'}</time>
|
||||||
.map((chunk, index) => (
|
</header>
|
||||||
<div key={`${chunk.runId}-${index}`} className={`chunk ${chunk.type}`}>
|
<div className="message-body blocks">
|
||||||
<strong>{chunk.type}</strong>
|
{(runtimeState?.blocks || []).map((block, index) => {
|
||||||
{chunk.toolName ? <span>{chunk.toolName}</span> : null}
|
if (block.type === 'text') {
|
||||||
{chunk.content ? <pre>{chunk.content}</pre> : null}
|
return <div key={`text-${index}`} className="text-block">{block.text}</div>
|
||||||
{chunk.type === 'tool_result' && chunk.toolResult !== undefined ? (
|
}
|
||||||
<pre>{JSON.stringify(chunk.toolResult, null, 2)}</pre>
|
if (block.type === 'think') {
|
||||||
) : null}
|
if (!showThinkBlocks) return null
|
||||||
{chunk.error ? <span className="err">{chunk.error}</span> : null}
|
return (
|
||||||
</div>
|
<details key={`think-${index}`} className="think-block">
|
||||||
))}
|
<summary>
|
||||||
</div>
|
思考过程
|
||||||
|
{block.durationMs ? <small>{Math.max(0, block.durationMs)}ms</small> : null}
|
||||||
|
</summary>
|
||||||
|
<pre>{block.text}</pre>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={`tool-${index}`} className={`tool-block ${block.tool.status}`}>
|
||||||
|
<div className="line">
|
||||||
|
<strong>{block.tool.name}</strong>
|
||||||
|
<span>{block.tool.status}</span>
|
||||||
|
</div>
|
||||||
|
{block.tool.params ? (
|
||||||
|
<pre>{JSON.stringify(block.tool.params, null, 2)}</pre>
|
||||||
|
) : null}
|
||||||
|
{block.tool.result ? (
|
||||||
|
<pre>{JSON.stringify(block.tool.result, null, 2)}</pre>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{runtimeState?.running ? (
|
||||||
|
<span className="typing-cursor">|</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
{runtimeState?.draft ? (
|
|
||||||
<div className="msg assistant draft">
|
|
||||||
<div className="head">助手(流式)</div>
|
|
||||||
<div className="body">{runtimeState.draft}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div ref={messageEndRef} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showScrollBottom ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scroll-bottom"
|
||||||
|
onClick={() => messageContainerRef.current?.scrollTo({ top: messageContainerRef.current.scrollHeight, behavior: 'smooth' })}
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="footer-actions">
|
<div className="status-row">
|
||||||
<button type="button" className="ghost" onClick={() => void loadConversations()}>
|
<div className="left">
|
||||||
<RefreshCw size={14} />
|
<span>状态:{runtimeState?.status?.phase || 'idle'}</span>
|
||||||
刷新
|
{typeof runtimeState?.usage?.totalTokens === 'number' ? (
|
||||||
</button>
|
<span>Tokens: {runtimeState?.usage?.totalTokens}</span>
|
||||||
<button type="button" className="ghost" onClick={() => void handleExportConversation()}>
|
) : null}
|
||||||
<Download size={14} />
|
</div>
|
||||||
导出会话
|
<div className="right">
|
||||||
</button>
|
<button type="button" className="ghost" onClick={() => void loadConversations()}>
|
||||||
<button type="button" className="ghost" onClick={() => void handleOpenLog()}>
|
<RefreshCw size={13} /> 刷新
|
||||||
<Search size={14} />
|
</button>
|
||||||
打开日志
|
<button type="button" className="ghost" onClick={() => void handleExportConversation()}>
|
||||||
</button>
|
<Download size={13} /> 导出
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="danger"
|
||||||
|
disabled={!runtimeState?.running}
|
||||||
|
onClick={() => void handleStop()}
|
||||||
|
>
|
||||||
|
<CircleStop size={13} /> 停止
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-panel">
|
<div className="input-panel">
|
||||||
<textarea
|
<textarea
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(event) => setInput(event.target.value)}
|
onChange={(event) => setInput(event.target.value)}
|
||||||
placeholder="输入问题,支持 /技能 和 @成员"
|
placeholder="输入问题,支持 /技能 和 @成员,Ctrl/Cmd + Enter 发送"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -691,22 +754,63 @@ function AiAnalysisPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="input-actions">
|
<div className="input-actions">
|
||||||
<button type="button" className="primary" onClick={() => void handleSend()} disabled={runtimeState?.running}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="primary"
|
||||||
|
onClick={() => void handleSend()}
|
||||||
|
disabled={runtimeState?.running}
|
||||||
|
>
|
||||||
{runtimeState?.running ? <Loader2 className="spin" size={14} /> : <Send size={14} />}
|
{runtimeState?.running ? <Loader2 className="spin" size={14} /> : <Send size={14} />}
|
||||||
发送
|
发送
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="danger" onClick={() => void handleStop()} disabled={!runtimeState?.running}>
|
|
||||||
<CircleStop size={14} />
|
|
||||||
停止
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{showDataPanel ? (
|
||||||
|
<aside className="data-panel">
|
||||||
|
<header>
|
||||||
|
<h3>数据源面板</h3>
|
||||||
|
<span>{runtimeState?.sourceMessages?.length || 0} 条</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="keywords">
|
||||||
|
<h4>关键词</h4>
|
||||||
|
<div className="chips">
|
||||||
|
{(runtimeState?.currentKeywords || []).length ? (
|
||||||
|
runtimeState?.currentKeywords.map((keyword) => (
|
||||||
|
<span key={keyword}>{keyword}</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<small>暂无</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="sources">
|
||||||
|
<h4>引用消息</h4>
|
||||||
|
<div className="source-list">
|
||||||
|
{(runtimeState?.sourceMessages || []).map((message) => (
|
||||||
|
<article key={`${message.sessionId}-${message.localId}-${message.timestamp}`}>
|
||||||
|
<header>
|
||||||
|
<strong>{message.senderName || '未知成员'}</strong>
|
||||||
|
<time>{formatDateTime((message.timestamp || 0) * 1000)}</time>
|
||||||
|
</header>
|
||||||
|
<p>{message.content}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
{(runtimeState?.sourceMessages || []).length === 0 ? (
|
||||||
|
<div className="empty">暂无检索来源</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'sql' && (
|
{activeTab === 'sql' && (
|
||||||
<div className="sql-layout">
|
<div className="sql-shell">
|
||||||
<aside className="schema-panel">
|
<aside className="schema-panel">
|
||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<h3>Schema</h3>
|
<h3>Schema</h3>
|
||||||
@@ -783,6 +887,7 @@ function AiAnalysisPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{column}
|
{column}
|
||||||
|
{sqlSortBy === column ? (sqlSortOrder === 'asc' ? <ChevronUp size={12} /> : <ChevronDown size={12} />) : null}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -814,7 +919,7 @@ function AiAnalysisPage() {
|
|||||||
<div className="history-list">
|
<div className="history-list">
|
||||||
{sqlHistory.map((sql, index) => (
|
{sqlHistory.map((sql, index) => (
|
||||||
<button key={`sql-${index}`} type="button" onClick={() => setSqlGenerated(sql)}>
|
<button key={`sql-${index}`} type="button" onClick={() => setSqlGenerated(sql)}>
|
||||||
{sql.slice(0, 120)}
|
{sql.slice(0, 160)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -823,56 +928,6 @@ function AiAnalysisPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'tool' && (
|
|
||||||
<div className="tool-layout">
|
|
||||||
<aside className="tool-catalog">
|
|
||||||
<h3>工具目录</h3>
|
|
||||||
<h4>Core</h4>
|
|
||||||
<div className="tool-list">
|
|
||||||
{groupedTools.core.map((tool) => (
|
|
||||||
<button key={tool.name} type="button" className={toolName === tool.name ? 'active' : ''} onClick={() => setToolName(tool.name)}>
|
|
||||||
{tool.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<h4>Analysis</h4>
|
|
||||||
<div className="tool-list">
|
|
||||||
{groupedTools.analysis.map((tool) => (
|
|
||||||
<button key={tool.name} type="button" className={toolName === tool.name ? 'active' : ''} onClick={() => setToolName(tool.name)}>
|
|
||||||
{tool.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section className="tool-main">
|
|
||||||
<div className="tool-top">
|
|
||||||
<div>
|
|
||||||
<h3>{toolName || '请选择工具'}</h3>
|
|
||||||
<p>{toolCatalog.find((tool) => tool.name === toolName)?.description || '暂无描述'}</p>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
|
||||||
<button type="button" onClick={() => void handleRunTool()} disabled={!toolName || toolRunning}>
|
|
||||||
{toolRunning ? <Loader2 className="spin" size={14} /> : <Wrench size={14} />}
|
|
||||||
执行
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => void aiApi.cancelToolTest({})}>
|
|
||||||
<CircleStop size={14} />
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="tool-args"
|
|
||||||
value={toolArgsText}
|
|
||||||
onChange={(event) => setToolArgsText(event.target.value)}
|
|
||||||
placeholder='{"keyword":"买车","limit":10}'
|
|
||||||
/>
|
|
||||||
<pre className="tool-output">{toolOutput || '执行结果会显示在这里(超长内容可滚动查看)'}</pre>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errorText ? <div className="global-error">{errorText}</div> : null}
|
{errorText ? <div className="global-error">{errorText}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,45 +1,212 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { AgentStreamChunk } from '../types/electron'
|
import type { AgentRuntimeStatus, AgentStreamChunk, TokenUsage } from '../types/electron'
|
||||||
|
|
||||||
|
export type RuntimeContentBlock =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| { type: 'think'; tag: string; text: string; durationMs?: number }
|
||||||
|
| {
|
||||||
|
type: 'tool'
|
||||||
|
tool: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: 'running' | 'done' | 'error'
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
result?: unknown
|
||||||
|
durationMs?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeSourceMessage {
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
}
|
||||||
|
|
||||||
interface ConversationRuntimeState {
|
interface ConversationRuntimeState {
|
||||||
|
requestId: string
|
||||||
|
runId: string
|
||||||
|
running: boolean
|
||||||
draft: string
|
draft: string
|
||||||
chunks: AgentStreamChunk[]
|
chunks: AgentStreamChunk[]
|
||||||
running: boolean
|
blocks: RuntimeContentBlock[]
|
||||||
|
sourceMessages: RuntimeSourceMessage[]
|
||||||
|
currentKeywords: string[]
|
||||||
|
usage?: TokenUsage
|
||||||
|
status?: AgentRuntimeStatus
|
||||||
|
error?: string
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AiRuntimeStoreState {
|
interface AiRuntimeStoreState {
|
||||||
activeRunId: string
|
activeRequestId: string
|
||||||
states: Record<string, ConversationRuntimeState>
|
states: Record<string, ConversationRuntimeState>
|
||||||
startRun: (conversationId: string, runId: string) => void
|
startRun: (conversationId: string, requestId: string) => void
|
||||||
appendChunk: (conversationId: string, chunk: AgentStreamChunk) => void
|
appendChunk: (conversationId: string, chunk: AgentStreamChunk) => void
|
||||||
finishRun: (conversationId: string) => void
|
completeRun: (
|
||||||
|
conversationId: string,
|
||||||
|
payload?: { runId?: string; conversationId?: string; error?: string; canceled?: boolean }
|
||||||
|
) => void
|
||||||
clearConversation: (conversationId: string) => void
|
clearConversation: (conversationId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextConversationState(previous?: ConversationRuntimeState): ConversationRuntimeState {
|
function nextConversationState(previous?: ConversationRuntimeState): ConversationRuntimeState {
|
||||||
return previous || {
|
return previous || {
|
||||||
|
requestId: '',
|
||||||
|
runId: '',
|
||||||
|
running: false,
|
||||||
draft: '',
|
draft: '',
|
||||||
chunks: [],
|
chunks: [],
|
||||||
running: false,
|
blocks: [],
|
||||||
|
sourceMessages: [],
|
||||||
|
currentKeywords: [],
|
||||||
|
usage: undefined,
|
||||||
|
status: undefined,
|
||||||
|
error: '',
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeText(value: unknown): string {
|
||||||
|
return String(value ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRuntimeSourceMessage(row: any): RuntimeSourceMessage | null {
|
||||||
|
if (!row || typeof row !== 'object') return null
|
||||||
|
const id = Number(row.id ?? row.localId ?? row.local_id ?? 0)
|
||||||
|
const timestamp = Number(row.timestamp ?? row.createTime ?? row.create_time ?? 0)
|
||||||
|
const senderName = normalizeText(row.senderName ?? row.sender ?? row.sender_username)
|
||||||
|
const content = normalizeText(row.content ?? row.snippet ?? row.message_content)
|
||||||
|
const sessionId = normalizeText(row.sessionId ?? row._session_id ?? row.session_id)
|
||||||
|
if (id <= 0 || !content) return null
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
localId: Number(row.localId ?? row.local_id ?? id),
|
||||||
|
sessionId,
|
||||||
|
senderName: senderName || '未知成员',
|
||||||
|
senderPlatformId: normalizeText(row.senderPlatformId ?? row.sender_platform_id ?? row.sender_username),
|
||||||
|
senderUsername: normalizeText(row.senderUsername ?? row.sender_username),
|
||||||
|
content,
|
||||||
|
timestamp,
|
||||||
|
type: Number(row.type ?? row.localType ?? row.local_type ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRuntimeSourceMessages(payload: unknown): RuntimeSourceMessage[] {
|
||||||
|
if (!payload || typeof payload !== 'object') return []
|
||||||
|
const bag = payload as Record<string, unknown>
|
||||||
|
const candidates: unknown[] = []
|
||||||
|
|
||||||
|
if (Array.isArray(bag.rawMessages)) candidates.push(...bag.rawMessages)
|
||||||
|
if (Array.isArray(bag.messages)) candidates.push(...bag.messages)
|
||||||
|
if (Array.isArray(bag.rows)) candidates.push(...bag.rows)
|
||||||
|
|
||||||
|
const nestedResult = bag.result
|
||||||
|
if (nestedResult && typeof nestedResult === 'object') {
|
||||||
|
const nested = nestedResult as Record<string, unknown>
|
||||||
|
if (Array.isArray(nested.rawMessages)) candidates.push(...nested.rawMessages)
|
||||||
|
if (Array.isArray(nested.messages)) candidates.push(...nested.messages)
|
||||||
|
if (Array.isArray(nested.rows)) candidates.push(...nested.rows)
|
||||||
|
if (Array.isArray(nested.items)) candidates.push(...nested.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: RuntimeSourceMessage[] = []
|
||||||
|
const dedup = new Set<string>()
|
||||||
|
for (const row of candidates) {
|
||||||
|
const normalized = toRuntimeSourceMessage(row)
|
||||||
|
if (!normalized) continue
|
||||||
|
const key = `${normalized.sessionId}:${normalized.localId}:${normalized.timestamp}`
|
||||||
|
if (dedup.has(key)) continue
|
||||||
|
dedup.add(key)
|
||||||
|
output.push(normalized)
|
||||||
|
if (output.length >= 120) break
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertToolBlock(blocks: RuntimeContentBlock[], chunk: AgentStreamChunk): RuntimeContentBlock[] {
|
||||||
|
const toolName = normalizeText(chunk.toolName)
|
||||||
|
if (!toolName) return blocks
|
||||||
|
|
||||||
|
if (chunk.type === 'tool_start') {
|
||||||
|
return [
|
||||||
|
...blocks,
|
||||||
|
{
|
||||||
|
type: 'tool',
|
||||||
|
tool: {
|
||||||
|
id: `${toolName}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||||
|
name: toolName,
|
||||||
|
status: 'running',
|
||||||
|
params: chunk.toolParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type !== 'tool_result') return blocks
|
||||||
|
|
||||||
|
const next = [...blocks]
|
||||||
|
for (let i = next.length - 1; i >= 0; i -= 1) {
|
||||||
|
const block = next[i]
|
||||||
|
if (block.type !== 'tool') continue
|
||||||
|
if (block.tool.name !== toolName) continue
|
||||||
|
if (block.tool.status !== 'running') continue
|
||||||
|
next[i] = {
|
||||||
|
type: 'tool',
|
||||||
|
tool: {
|
||||||
|
...block.tool,
|
||||||
|
status: chunk.error ? 'error' : 'done',
|
||||||
|
result: chunk.toolResult,
|
||||||
|
durationMs: Number((chunk.toolResult as any)?.durationMs || 0) || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
next.push({
|
||||||
|
type: 'tool',
|
||||||
|
tool: {
|
||||||
|
id: `${toolName}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||||
|
name: toolName,
|
||||||
|
status: chunk.error ? 'error' : 'done',
|
||||||
|
params: chunk.toolParams,
|
||||||
|
result: chunk.toolResult,
|
||||||
|
durationMs: Number((chunk.toolResult as any)?.durationMs || 0) || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
export const useAiRuntimeStore = create<AiRuntimeStoreState>((set) => ({
|
export const useAiRuntimeStore = create<AiRuntimeStoreState>((set) => ({
|
||||||
activeRunId: '',
|
activeRequestId: '',
|
||||||
states: {},
|
states: {},
|
||||||
startRun: (conversationId, runId) => set((state) => {
|
startRun: (conversationId, requestId) => set((state) => {
|
||||||
const prev = nextConversationState(state.states[conversationId])
|
const prev = nextConversationState(state.states[conversationId])
|
||||||
return {
|
return {
|
||||||
activeRunId: runId,
|
activeRequestId: requestId,
|
||||||
states: {
|
states: {
|
||||||
...state.states,
|
...state.states,
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
...prev,
|
...prev,
|
||||||
|
requestId,
|
||||||
|
runId: '',
|
||||||
|
running: true,
|
||||||
draft: '',
|
draft: '',
|
||||||
chunks: [],
|
chunks: [],
|
||||||
running: true,
|
blocks: [],
|
||||||
|
error: '',
|
||||||
|
sourceMessages: [],
|
||||||
|
currentKeywords: [],
|
||||||
|
usage: undefined,
|
||||||
|
status: {
|
||||||
|
phase: 'thinking',
|
||||||
|
updatedAt: Date.now()
|
||||||
|
},
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,33 +214,98 @@ export const useAiRuntimeStore = create<AiRuntimeStoreState>((set) => ({
|
|||||||
}),
|
}),
|
||||||
appendChunk: (conversationId, chunk) => set((state) => {
|
appendChunk: (conversationId, chunk) => set((state) => {
|
||||||
const prev = nextConversationState(state.states[conversationId])
|
const prev = nextConversationState(state.states[conversationId])
|
||||||
const nextDraft = chunk.type === 'content'
|
const nextBlocks = [...prev.blocks]
|
||||||
? `${prev.draft}${chunk.content || ''}`
|
let nextDraft = prev.draft
|
||||||
: prev.draft
|
const nextKeywords = [...prev.currentKeywords]
|
||||||
|
|
||||||
|
if (chunk.type === 'content') {
|
||||||
|
const text = normalizeText(chunk.content)
|
||||||
|
if (text) {
|
||||||
|
nextDraft = `${prev.draft}${text}`
|
||||||
|
const last = nextBlocks[nextBlocks.length - 1]
|
||||||
|
if (last && last.type === 'text') {
|
||||||
|
last.text = `${last.text}${text}`
|
||||||
|
} else {
|
||||||
|
nextBlocks.push({ type: 'text', text })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type === 'think') {
|
||||||
|
const text = normalizeText(chunk.content)
|
||||||
|
if (text) {
|
||||||
|
nextBlocks.push({
|
||||||
|
type: 'think',
|
||||||
|
tag: normalizeText(chunk.thinkTag) || 'thinking',
|
||||||
|
text,
|
||||||
|
durationMs: chunk.thinkDurationMs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedBlocks = upsertToolBlock(nextBlocks, chunk)
|
||||||
|
const extractedSource = chunk.type === 'tool_result'
|
||||||
|
? extractRuntimeSourceMessages(chunk.toolResult)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const sourceDedup = new Map<string, RuntimeSourceMessage>()
|
||||||
|
for (const item of [...prev.sourceMessages, ...extractedSource]) {
|
||||||
|
const key = `${item.sessionId}:${item.localId}:${item.timestamp}`
|
||||||
|
sourceDedup.set(key, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.toolParams) {
|
||||||
|
const keywordRaw = chunk.toolParams.keyword ?? chunk.toolParams.keywords
|
||||||
|
if (Array.isArray(keywordRaw)) {
|
||||||
|
for (const item of keywordRaw) {
|
||||||
|
const keyword = normalizeText(item)
|
||||||
|
if (keyword && !nextKeywords.includes(keyword)) nextKeywords.push(keyword)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keyword = normalizeText(keywordRaw)
|
||||||
|
if (keyword && !nextKeywords.includes(keyword)) nextKeywords.push(keyword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
states: {
|
states: {
|
||||||
...state.states,
|
...state.states,
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
...prev,
|
...prev,
|
||||||
|
runId: normalizeText(chunk.runId) || prev.runId,
|
||||||
draft: nextDraft,
|
draft: nextDraft,
|
||||||
chunks: [...prev.chunks, chunk].slice(-300),
|
blocks: mergedBlocks,
|
||||||
updatedAt: Date.now(),
|
chunks: [...prev.chunks, chunk].slice(-500),
|
||||||
running: chunk.type === 'done' || chunk.isFinished ? false : prev.running
|
sourceMessages: Array.from(sourceDedup.values()).slice(-120),
|
||||||
|
currentKeywords: nextKeywords.slice(-12),
|
||||||
|
usage: chunk.usage || prev.usage,
|
||||||
|
status: chunk.status || prev.status,
|
||||||
|
error: chunk.error || prev.error,
|
||||||
|
running: chunk.type === 'done' || chunk.type === 'error' || chunk.isFinished ? false : prev.running,
|
||||||
|
updatedAt: Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
finishRun: (conversationId) => set((state) => {
|
completeRun: (conversationId, payload) => set((state) => {
|
||||||
const prev = state.states[conversationId]
|
const prev = state.states[conversationId]
|
||||||
if (!prev) return state
|
if (!prev) return state
|
||||||
|
const failed = normalizeText(payload?.error)
|
||||||
|
const canceled = payload?.canceled === true || failed === '任务已取消' || failed === '任务已停止'
|
||||||
return {
|
return {
|
||||||
activeRunId: '',
|
activeRequestId: '',
|
||||||
states: {
|
states: {
|
||||||
...state.states,
|
...state.states,
|
||||||
[conversationId]: {
|
[conversationId]: {
|
||||||
...prev,
|
...prev,
|
||||||
draft: '',
|
runId: normalizeText(payload?.runId) || prev.runId,
|
||||||
running: false,
|
running: false,
|
||||||
|
error: canceled ? '' : (failed || prev.error),
|
||||||
|
status: canceled
|
||||||
|
? { phase: 'aborted', updatedAt: Date.now(), totalUsage: prev.usage }
|
||||||
|
: failed
|
||||||
|
? { phase: 'error', updatedAt: Date.now(), totalUsage: prev.usage }
|
||||||
|
: { phase: 'completed', updatedAt: Date.now(), totalUsage: prev.usage },
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
325
src/types/electron.d.ts
vendored
325
src/types/electron.d.ts
vendored
@@ -1192,6 +1192,223 @@ export interface ElectronAPI {
|
|||||||
markdown?: string
|
markdown?: string
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getMessageContext: (sessionId: string, messageIds: number | number[], contextSize?: number) => Promise<Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>>
|
||||||
|
getSearchMessageContext: (sessionId: string, messageIds: number[], contextBefore?: number, contextAfter?: number) => Promise<Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>>
|
||||||
|
getRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => Promise<{
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
total: number
|
||||||
|
}>
|
||||||
|
getAllRecentMessages: (sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => Promise<{
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
total: number
|
||||||
|
}>
|
||||||
|
getConversationBetween: (
|
||||||
|
sessionId: string,
|
||||||
|
memberId1: number,
|
||||||
|
memberId2: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number
|
||||||
|
) => Promise<{
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
total: number
|
||||||
|
member1Name: string
|
||||||
|
member2Name: string
|
||||||
|
}>
|
||||||
|
getMessagesBefore: (
|
||||||
|
sessionId: string,
|
||||||
|
beforeId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => Promise<{
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
hasMore: boolean
|
||||||
|
}>
|
||||||
|
getMessagesAfter: (
|
||||||
|
sessionId: string,
|
||||||
|
afterId: number,
|
||||||
|
limit?: number,
|
||||||
|
filter?: { startTs?: number; endTs?: number },
|
||||||
|
senderId?: number,
|
||||||
|
keywords?: string[]
|
||||||
|
) => Promise<{
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
hasMore: boolean
|
||||||
|
}>
|
||||||
|
searchSessions: (
|
||||||
|
sessionId: string,
|
||||||
|
keywords?: string[],
|
||||||
|
timeFilter?: { startTs?: number; endTs?: number },
|
||||||
|
limit?: number,
|
||||||
|
previewCount?: number
|
||||||
|
) => Promise<Array<{
|
||||||
|
id: string
|
||||||
|
startTs: number
|
||||||
|
endTs: number
|
||||||
|
messageCount: number
|
||||||
|
isComplete: boolean
|
||||||
|
previewMessages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
}>>
|
||||||
|
getSessionMessages: (sessionId: string, chatSessionId: string | number, limit?: number) => Promise<{
|
||||||
|
sessionId: string
|
||||||
|
startTs: number
|
||||||
|
endTs: number
|
||||||
|
messageCount: number
|
||||||
|
returnedCount: number
|
||||||
|
participants: string[]
|
||||||
|
messages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
} | null>
|
||||||
|
getSessionSummaries: (
|
||||||
|
sessionId: string,
|
||||||
|
options?: { sessionIds?: string[]; limit?: number; previewCount?: number }
|
||||||
|
) => Promise<Array<{
|
||||||
|
sessionId: string
|
||||||
|
sessionName: string
|
||||||
|
messageCount: number
|
||||||
|
latestTs: number
|
||||||
|
previewMessages: Array<{
|
||||||
|
id: number
|
||||||
|
localId: number
|
||||||
|
sessionId: string
|
||||||
|
senderName: string
|
||||||
|
senderPlatformId: string
|
||||||
|
senderUsername: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
type: number
|
||||||
|
isSend: number | null
|
||||||
|
replyToMessageId: string | null
|
||||||
|
replyToContent: string | null
|
||||||
|
replyToSenderName: string | null
|
||||||
|
}>
|
||||||
|
}>>
|
||||||
getToolCatalog: () => Promise<Array<{
|
getToolCatalog: () => Promise<Array<{
|
||||||
name: string
|
name: string
|
||||||
category: 'core' | 'analysis'
|
category: 'core' | 'analysis'
|
||||||
@@ -1214,9 +1431,15 @@ export interface ElectronAPI {
|
|||||||
activeSkillId?: string
|
activeSkillId?: string
|
||||||
chatScope?: 'group' | 'private'
|
chatScope?: 'group' | 'private'
|
||||||
sqlContext?: { schemaText?: string; targetHint?: string }
|
sqlContext?: { schemaText?: string; targetHint?: string }
|
||||||
}) => Promise<{ success: boolean; runId: string }>
|
}, onChunk?: (payload: AgentStreamChunk) => void) => {
|
||||||
abort: (payload: { runId?: string; conversationId?: string }) => Promise<{ success: boolean }>
|
requestId: string
|
||||||
onStream: (callback: (payload: AgentStreamChunk) => void) => () => void
|
promise: Promise<{
|
||||||
|
success: boolean
|
||||||
|
result?: { success: boolean; runId?: string; conversationId?: string; error?: string; canceled?: boolean }
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
abort: (payload: string | { requestId?: string; runId?: string; conversationId?: string }) => Promise<{ success: boolean }>
|
||||||
}
|
}
|
||||||
assistantApi: {
|
assistantApi: {
|
||||||
getAll: () => Promise<Array<{
|
getAll: () => Promise<Array<{
|
||||||
@@ -1293,102 +1516,6 @@ export interface ElectronAPI {
|
|||||||
setConfig: (payload: { apiBaseUrl?: string; apiKey?: string; model?: string }) => Promise<{ success: boolean }>
|
setConfig: (payload: { apiBaseUrl?: string; apiKey?: string; model?: string }) => Promise<{ success: boolean }>
|
||||||
listModels: () => Promise<{ success: boolean; models: Array<{ id: string; label: string }> }>
|
listModels: () => Promise<{ success: boolean; models: Array<{ id: string; label: string }> }>
|
||||||
}
|
}
|
||||||
aiAnalysis: {
|
|
||||||
listConversations: (payload?: { page?: number; pageSize?: number }) => Promise<{
|
|
||||||
success: boolean
|
|
||||||
conversations?: Array<{
|
|
||||||
conversationId: string
|
|
||||||
title: string
|
|
||||||
createdAt: number
|
|
||||||
updatedAt: number
|
|
||||||
lastMessageAt: number
|
|
||||||
}>
|
|
||||||
error?: string
|
|
||||||
}>
|
|
||||||
createConversation: (payload?: { title?: string }) => Promise<{
|
|
||||||
success: boolean
|
|
||||||
conversationId?: string
|
|
||||||
error?: string
|
|
||||||
}>
|
|
||||||
deleteConversation: (conversationId: string) => Promise<{ success: boolean; error?: string }>
|
|
||||||
listMessages: (payload: { conversationId: string; limit?: number }) => Promise<{
|
|
||||||
success: boolean
|
|
||||||
messages?: Array<{
|
|
||||||
messageId: string
|
|
||||||
conversationId: string
|
|
||||||
role: 'user' | 'assistant' | 'system' | 'tool' | string
|
|
||||||
content: string
|
|
||||||
intentType: string
|
|
||||||
components: any[]
|
|
||||||
toolTrace: any[]
|
|
||||||
usage: Record<string, unknown>
|
|
||||||
error: string
|
|
||||||
parentMessageId: string
|
|
||||||
createdAt: number
|
|
||||||
}>
|
|
||||||
error?: string
|
|
||||||
}>
|
|
||||||
sendMessage: (payload: {
|
|
||||||
conversationId: string
|
|
||||||
userInput: string
|
|
||||||
options?: {
|
|
||||||
parentMessageId?: string
|
|
||||||
persistUserMessage?: boolean
|
|
||||||
assistantId?: string
|
|
||||||
activeSkillId?: string
|
|
||||||
chatScope?: 'group' | 'private'
|
|
||||||
}
|
|
||||||
}) => Promise<{
|
|
||||||
success: boolean
|
|
||||||
result?: {
|
|
||||||
conversationId: string
|
|
||||||
messageId: string
|
|
||||||
assistantText: string
|
|
||||||
components: any[]
|
|
||||||
toolTrace: any[]
|
|
||||||
usage?: {
|
|
||||||
promptTokens?: number
|
|
||||||
completionTokens?: number
|
|
||||||
totalTokens?: number
|
|
||||||
}
|
|
||||||
error?: string
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
error?: string
|
|
||||||
}>
|
|
||||||
retryMessage: (payload: { conversationId: string; userMessageId?: string }) => Promise<{
|
|
||||||
success: boolean
|
|
||||||
result?: {
|
|
||||||
conversationId: string
|
|
||||||
messageId: string
|
|
||||||
assistantText: string
|
|
||||||
components: any[]
|
|
||||||
toolTrace: any[]
|
|
||||||
usage?: {
|
|
||||||
promptTokens?: number
|
|
||||||
completionTokens?: number
|
|
||||||
totalTokens?: number
|
|
||||||
}
|
|
||||||
error?: string
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
error?: string
|
|
||||||
}>
|
|
||||||
abortRun: (payload: { runId?: string; conversationId?: string }) => Promise<{ success: boolean }>
|
|
||||||
onRunEvent: (callback: (payload: {
|
|
||||||
runId: string
|
|
||||||
conversationId: string
|
|
||||||
stage: string
|
|
||||||
ts: number
|
|
||||||
message: string
|
|
||||||
intent?: string
|
|
||||||
round?: number
|
|
||||||
toolName?: string
|
|
||||||
status?: string
|
|
||||||
durationMs?: number
|
|
||||||
data?: Record<string, unknown>
|
|
||||||
}) => void) => () => void
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user