mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix: 修复 AI 会话链路与前端 type-check 错误
This commit is contained in:
1 parent
39b06523d4
commit
b6fdc3887e
18 files changed
+75
-63
No files matched your search
@@ -7,6 +7,8 @@ import Database from 'better-sqlite3'
|
||||
import * as path from 'path'
|
||||
import { getAiDataDir, ensureDir } from '../paths'
|
||||
|
||||
const DEFAULT_GENERAL_ID = 'general_cn'
|
||||
|
||||
// AI 数据库实例
|
||||
let AI_DB: Database.Database | null = null
|
||||
|
||||
|
||||
+28
-22
@@ -1,5 +1,5 @@
|
||||
// electron/main/ipc/ai.ts
|
||||
import { ipcMain, BrowserWindow, shell } from 'electron'
|
||||
import { ipcMain, shell } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as aiConversations from '../ai/conversations'
|
||||
@@ -13,12 +13,26 @@ import { getActiveConfig, buildPiModel } from '../ai/llm'
|
||||
import * as assistantManager from '../ai/assistant'
|
||||
import type { AssistantConfig } from '../ai/assistant/types'
|
||||
import * as skillManager from '../ai/skills'
|
||||
import { completeSimple, streamSimple, type TextContent as PiTextContent } from '@mariozechner/pi-ai'
|
||||
import {
|
||||
completeSimple,
|
||||
streamSimple,
|
||||
type Message as PiMessage,
|
||||
type TextContent as PiTextContent,
|
||||
} from '@mariozechner/pi-ai'
|
||||
import { t } from '../i18n'
|
||||
import type { ToolContext } from '../ai/tools/types'
|
||||
import { getDefaultRulesForLocale, mergeRulesForLocale } from '../ai/preprocessor/builtin-rules'
|
||||
import type { IpcContext } from './types'
|
||||
|
||||
function toPiSimpleMessages(messages: Array<{ role: string; content: string }>, timestamp: number): PiMessage[] {
|
||||
// pi-ai 的 simple API 在类型上要求完整 Message 联合,这里沿用现有轻量消息格式并集中做兼容转换。
|
||||
return messages.map((message) => ({
|
||||
role: message.role as 'user' | 'assistant',
|
||||
content: message.content,
|
||||
timestamp,
|
||||
})) as unknown as PiMessage[]
|
||||
}
|
||||
|
||||
// ==================== AI Agent 请求追踪 ====================
|
||||
// 用于跟踪活跃的 Agent 请求,支持中止操作
|
||||
const activeAgentRequests = new Map<string, AbortController>()
|
||||
@@ -147,22 +161,22 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
* 创建新的 AI 对话
|
||||
* 参数契约与 preload / 数据层保持一致:(sessionId, title?)
|
||||
*/
|
||||
ipcMain.handle('ai:createConversation', async (_, sessionId: string, title?: string, assistantId?: string) => {
|
||||
try {
|
||||
if (!assistantId) {
|
||||
throw new Error('assistantId is required when creating a conversation')
|
||||
ipcMain.handle(
|
||||
'ai:createConversation',
|
||||
async (_, sessionId: string, title: string | undefined, assistantId: string) => {
|
||||
try {
|
||||
return aiConversations.createConversation(sessionId, title, assistantId)
|
||||
} catch (error) {
|
||||
console.error('Failed to create AI conversation:', error)
|
||||
throw error
|
||||
}
|
||||
return aiConversations.createConversation(sessionId, title, assistantId)
|
||||
} catch (error) {
|
||||
console.error('Failed to create AI conversation:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取所有 AI 对话列表
|
||||
*/
|
||||
ipcMain.handle('ai:getConversations', async (_, sessionId?: string) => {
|
||||
ipcMain.handle('ai:getConversations', async (_, sessionId: string) => {
|
||||
try {
|
||||
return aiConversations.getConversations(sessionId)
|
||||
} catch (error) {
|
||||
@@ -485,11 +499,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
piModel,
|
||||
{
|
||||
systemPrompt: systemMsg?.content,
|
||||
messages: nonSystemMsgs.map((m) => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
timestamp: now,
|
||||
})),
|
||||
messages: toPiSimpleMessages(nonSystemMsgs, now),
|
||||
},
|
||||
{
|
||||
apiKey: activeConfig.apiKey,
|
||||
@@ -536,11 +546,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
piModel,
|
||||
{
|
||||
systemPrompt: systemMsg?.content,
|
||||
messages: nonSystemMsgs.map((m) => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
timestamp: now,
|
||||
})),
|
||||
messages: toPiSimpleMessages(nonSystemMsgs, now),
|
||||
},
|
||||
{
|
||||
apiKey: activeConfig.apiKey,
|
||||
|
||||
@@ -419,7 +419,7 @@ export const aiApi = {
|
||||
/**
|
||||
* 创建 AI 对话
|
||||
*/
|
||||
createConversation: (sessionId: string, title?: string, assistantId: string): Promise<AIConversation> => {
|
||||
createConversation: (sessionId: string, title: string | undefined, assistantId: string): Promise<AIConversation> => {
|
||||
return ipcRenderer.invoke('ai:createConversation', sessionId, title, assistantId)
|
||||
},
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -358,7 +358,7 @@ interface AiApi {
|
||||
senderId?: number,
|
||||
keywords?: string[]
|
||||
) => Promise<{ messages: SearchMessageResult[]; hasMore: boolean }>
|
||||
createConversation: (sessionId: string, title?: string, assistantId: string) => Promise<AIConversation>
|
||||
createConversation: (sessionId: string, title: string | undefined, assistantId: string) => Promise<AIConversation>
|
||||
getConversations: (sessionId: string) => Promise<AIConversation[]>
|
||||
getConversation: (conversationId: string) => Promise<AIConversation | null>
|
||||
updateConversationTitle: (conversationId: string, title: string) => Promise<boolean>
|
||||
|
||||
@@ -11,6 +11,9 @@ import { chatApi, mergeApi } from './apis/chat'
|
||||
import { aiApi, llmApi, agentApi, embeddingApi, assistantApi, skillApi } from './apis/ai'
|
||||
import { nlpApi, networkApi, cacheApi, sessionApi } from './apis/utils'
|
||||
|
||||
// 为渲染进程提供统一的类型入口,避免 type-only import 指向无导出的运行时代码。
|
||||
export type { PreprocessConfig, EmbeddingServiceConfig, EmbeddingServiceConfigDisplay } from './apis/ai'
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TooltipComponent, GridComponent, VisualMapComponent } from 'echarts/com
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { loadClusterGraph } from './queries'
|
||||
import type { ClusterGraphData, ClusterGraphOptions, ClusterGraphNode } from './types'
|
||||
import type { ClusterGraphData, ClusterGraphOptions } from './types'
|
||||
|
||||
echarts.use([HeatmapChart, TooltipComponent, GridComponent, VisualMapComponent, CanvasRenderer])
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ async function handleOpenTask() {
|
||||
<!-- 默认态直接作为返回入口,不再保留额外展开层。 -->
|
||||
<button
|
||||
class="group flex h-9 items-center gap-1.5 overflow-hidden rounded-full bg-primary-500 px-2 shadow-[0_10px_24px_rgba(15,23,42,0.12)] transition-all hover:-translate-y-0.5 hover:bg-primary-600 hover:shadow-[0_16px_32px_rgba(15,23,42,0.16)] dark:bg-primary-500 dark:hover:bg-primary-400"
|
||||
:title="`${t('ai.chat.backgroundTask.return')} · ${activeTask.sessionName}`"
|
||||
:title="`${t('ai.chat.backgroundTask.return')} · ${activeTask?.sessionName ?? ''}`"
|
||||
@click="handleOpenTask"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -387,7 +387,7 @@ function closeModal() {
|
||||
{{ t('ai.assistant.config.presetQuestions') }}
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(q, index) in form.presetQuestions" :key="index" class="flex items-center gap-2">
|
||||
<div v-for="(_, index) in form.presetQuestions" :key="index" class="flex items-center gap-2">
|
||||
<UInput
|
||||
v-model="form.presetQuestions[index]"
|
||||
class="min-w-0 flex-1"
|
||||
|
||||
@@ -394,7 +394,7 @@ function fillInput(content: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function handleSelectMention(member: MentionCandidate) {
|
||||
function handleSelectMention(member: Pick<MentionCandidate, 'memberId' | 'platformId' | 'displayName' | 'insertName' | 'aliases'>) {
|
||||
if (props.disabled || !mentionRange.value) return
|
||||
|
||||
const prefix = inputValue.value.slice(0, mentionRange.value.start)
|
||||
|
||||
@@ -33,10 +33,10 @@ export interface SQLResult {
|
||||
limited: boolean
|
||||
}
|
||||
|
||||
// 表/列的多语言标签映射
|
||||
type LocaleType = 'zh-CN' | 'en-US'
|
||||
// 表/列标签目前只维护中英文;其他 locale 先归一化后复用。
|
||||
type LabelLocale = 'zh-CN' | 'en-US'
|
||||
|
||||
export const TABLE_LABELS: Record<LocaleType, Record<string, string>> = {
|
||||
export const TABLE_LABELS: Record<LabelLocale, Record<string, string>> = {
|
||||
'zh-CN': {
|
||||
message: '消息记录',
|
||||
member: '成员',
|
||||
@@ -51,7 +51,7 @@ export const TABLE_LABELS: Record<LocaleType, Record<string, string>> = {
|
||||
},
|
||||
}
|
||||
|
||||
export const COLUMN_LABELS: Record<LocaleType, Record<string, Record<string, string>>> = {
|
||||
export const COLUMN_LABELS: Record<LabelLocale, Record<string, Record<string, string>>> = {
|
||||
'zh-CN': {
|
||||
message: {
|
||||
id: '消息ID',
|
||||
@@ -119,11 +119,15 @@ export const COLUMN_LABELS: Record<LocaleType, Record<string, Record<string, str
|
||||
}
|
||||
|
||||
// 获取表的标签
|
||||
export function getTableLabel(tableName: string, locale: LocaleType = 'zh-CN'): string {
|
||||
return TABLE_LABELS[locale]?.[tableName] || tableName
|
||||
export function getTableLabel(tableName: string, locale: string = 'zh-CN'): string {
|
||||
return TABLE_LABELS[normalizeLabelLocale(locale)]?.[tableName] || tableName
|
||||
}
|
||||
|
||||
// 获取列的标签
|
||||
export function getColumnLabel(tableName: string, columnName: string, locale: LocaleType = 'zh-CN'): string {
|
||||
return COLUMN_LABELS[locale]?.[tableName]?.[columnName] || columnName
|
||||
export function getColumnLabel(tableName: string, columnName: string, locale: string = 'zh-CN'): string {
|
||||
return COLUMN_LABELS[normalizeLabelLocale(locale)]?.[tableName]?.[columnName] || columnName
|
||||
}
|
||||
|
||||
function normalizeLabelLocale(locale: string): LabelLocale {
|
||||
return locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDateFormat, useDebounceFn } from '@vueuse/core'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -34,8 +34,8 @@ type TimeRangePreset = 'today' | 'yesterday' | 'week' | 'month' | 'custom'
|
||||
const selectedPreset = ref<TimeRangePreset>('today')
|
||||
|
||||
// 自定义时间范围
|
||||
const customStartDate = ref<Date | null>(null)
|
||||
const customEndDate = ref<Date | null>(null)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
// 会话列表
|
||||
interface SessionItem {
|
||||
@@ -43,7 +43,8 @@ interface SessionItem {
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
summary: string | null
|
||||
// 与 sessionApi 返回值保持一致,历史数据里 summary 可能不存在。
|
||||
summary?: string | null
|
||||
}
|
||||
const sessions = ref<SessionItem[]>([])
|
||||
const isLoading = ref(false)
|
||||
@@ -102,9 +103,11 @@ const timeRange = computed(() => {
|
||||
}
|
||||
case 'custom':
|
||||
if (customStartDate.value && customEndDate.value) {
|
||||
const start = new Date(customStartDate.value)
|
||||
const end = new Date(customEndDate.value)
|
||||
return {
|
||||
start: customStartDate.value.getTime(),
|
||||
end: new Date(customEndDate.value.getTime() + 24 * 60 * 60 * 1000 - 1).getTime(), // 当天结束
|
||||
start: start.getTime(),
|
||||
end: new Date(end.getTime() + 24 * 60 * 60 * 1000 - 1).getTime(), // 当天结束
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -331,11 +334,6 @@ function close() {
|
||||
}
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
// 格式化时间戳
|
||||
function formatTs(ts: number) {
|
||||
return useDateFormat(new Date(ts * 1000), 'MM-DD HH:mm').value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -134,7 +134,7 @@ export function getDefaultRoleDefinition(locale: LocaleType = 'zh-CN'): string {
|
||||
}
|
||||
|
||||
/** @deprecated responseRules 已合并到 systemPrompt */
|
||||
export function getDefaultResponseRules(locale: LocaleType = 'zh-CN'): string {
|
||||
export function getDefaultResponseRules(_locale: LocaleType = 'zh-CN'): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ interface AIServiceConfig {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
apiKey: string
|
||||
apiKey?: string
|
||||
apiKeySet: boolean
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
|
||||
@@ -26,7 +26,6 @@ const editingConfig = ref<EmbeddingServiceConfigDisplay | null>(null)
|
||||
const isLoading = computed(() => embeddingStore.isLoading)
|
||||
const configs = computed(() => embeddingStore.configs)
|
||||
const activeConfigId = computed(() => embeddingStore.activeConfigId)
|
||||
const hasConfig = computed(() => embeddingStore.hasConfig)
|
||||
const isMaxConfigs = computed(() => embeddingStore.isMaxConfigs)
|
||||
const vectorStoreStats = computed(() => embeddingStore.vectorStoreStats)
|
||||
const vectorStoreSizeFormatted = computed(() => embeddingStore.vectorStoreSizeFormatted)
|
||||
|
||||
@@ -51,13 +51,6 @@ const canMerge = computed(() => {
|
||||
return platforms.size === 1
|
||||
})
|
||||
|
||||
// 选中会话的平台
|
||||
const selectedPlatform = computed(() => {
|
||||
if (selectedIds.value.size === 0) return null
|
||||
const selectedSessions = sessions.value.filter((s) => selectedIds.value.has(s.id))
|
||||
return selectedSessions[0]?.platform || null
|
||||
})
|
||||
|
||||
// 全选状态(基于过滤后的列表)
|
||||
const isAllSelected = computed(() => {
|
||||
return filteredSessions.value.length > 0 && filteredSessions.value.every((s) => selectedIds.value.has(s.id))
|
||||
|
||||
+11
-3
@@ -381,6 +381,16 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
activeTask.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话创建成功后,把后台任务绑定到真实 conversationId。
|
||||
* 单独抽成 helper,避免在长 async 流程里触发异常的类型缩窄。
|
||||
*/
|
||||
function updateActiveTaskConversationId(chatKey: string, conversationId: string): void {
|
||||
if (!activeTask.value) return
|
||||
if (activeTask.value.chatKey !== chatKey) return
|
||||
activeTask.value.conversationId = conversationId
|
||||
}
|
||||
|
||||
function buildFallbackAgentStatus(state: AIChatSessionState): AgentRuntimeStatus {
|
||||
return {
|
||||
phase: 'preparing',
|
||||
@@ -742,9 +752,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
resolvedConversationId = conversation.id
|
||||
renameBufferKey(state, DRAFT_CONVERSATION_KEY, conversation.id)
|
||||
targetBuffer.assistantId = currentAssistantId
|
||||
if (activeTask.value?.chatKey === chatKey) {
|
||||
activeTask.value.conversationId = conversation.id
|
||||
}
|
||||
updateActiveTaskConversationId(chatKey, conversation.id)
|
||||
}
|
||||
|
||||
const maxHistoryRounds = aiGlobalSettings.value.maxHistoryRounds ?? 5
|
||||
|
||||
@@ -66,10 +66,9 @@ export const useEmbeddingStore = defineStore('embedding', () => {
|
||||
async function loadConfigs() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const [configsData, activeId, isEnabled, stats] = await Promise.all([
|
||||
const [configsData, activeId, stats] = await Promise.all([
|
||||
window.embeddingApi.getAllConfigs(),
|
||||
window.embeddingApi.getActiveConfigId(),
|
||||
window.embeddingApi.isEnabled(),
|
||||
window.embeddingApi.getVectorStoreStats(),
|
||||
])
|
||||
configs.value = configsData
|
||||
|
||||
Reference in new issue
Block a user