fix: 修复 AI 会话链路与前端 type-check 错误

This commit is contained in:
digua committed 2026-03-25 22:42:31 +08:00
1 parent 39b06523d4
commit b6fdc3887e
18 files changed
+75 -63

No files matched your search

+2
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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)
},
+1 -1
View File
@@ -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>
+3
View File
@@ -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.
+1 -1
View File
@@ -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])
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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)
+12 -8
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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