feat: 逻辑优化

This commit is contained in:
digua
2026-01-25 21:37:34 +08:00
parent ec2f91965e
commit 99315e56be
11 changed files with 234 additions and 89 deletions
+3 -12
View File
@@ -219,23 +219,14 @@ export function setActiveEmbeddingConfig(id: string): { success: boolean; error?
return { success: true }
}
/**
* 设置语义搜索启用状态
*/
export function setEmbeddingEnabled(enabled: boolean): { success: boolean } {
const store = loadEmbeddingConfigStore()
store.enabled = enabled
saveEmbeddingConfigStore(store)
logger.info('RAG', `语义搜索 ${enabled ? '已启用' : '已禁用'}`)
return { success: true }
}
/**
* 检查语义搜索是否启用
* 简化逻辑:只要有激活的配置就启用
*/
export function isEmbeddingEnabled(): boolean {
const store = loadEmbeddingConfigStore()
return store.enabled && store.activeConfigId !== null
// 只要有激活的配置就启用,无需额外开关
return store.activeConfigId !== null && store.configs.some((c) => c.id === store.activeConfigId)
}
/**
-1
View File
@@ -21,7 +21,6 @@ export {
updateEmbeddingConfig,
deleteEmbeddingConfig,
setActiveEmbeddingConfig,
setEmbeddingEnabled,
isEmbeddingEnabled,
getActiveEmbeddingConfigId,
// 旧版兼容
+122
View File
@@ -918,6 +918,127 @@ async function getSessionMessagesExecutor(
}
}
// ==================== 摘要查询工具 ====================
/**
* 获取会话摘要列表
*/
const getSessionSummariesTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_session_summaries',
description: `获取会话摘要列表,快速了解群聊历史讨论的主题。
适用场景:
1. 了解群里最近在聊什么话题
2. 按关键词搜索讨论过的话题
3. 概览性问题如"群里有没有讨论过旅游"
返回的摘要是对每个会话的简短总结,可以帮助快速定位感兴趣的会话,然后用 get_session_messages 获取详情。`,
parameters: {
type: 'object',
properties: {
keywords: {
type: 'array',
items: { type: 'string' },
description: '在摘要中搜索的关键词列表(OR 逻辑匹配)',
},
limit: {
type: 'number',
description: '返回会话数量限制,默认 20',
},
year: {
type: 'number',
description: '筛选指定年份的会话',
},
month: {
type: 'number',
description: '筛选指定月份的会话(1-12',
},
day: {
type: 'number',
description: '筛选指定日期的会话(1-31',
},
start_time: {
type: 'string',
description: '开始时间,格式 "YYYY-MM-DD HH:mm"',
},
end_time: {
type: 'string',
description: '结束时间,格式 "YYYY-MM-DD HH:mm"',
},
},
},
},
}
async function getSessionSummariesExecutor(
params: {
keywords?: string[]
limit?: number
year?: number
month?: number
day?: number
start_time?: string
end_time?: string
},
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, locale } = context
const limit = params.limit || 20
// 解析时间参数
const effectiveTimeFilter = parseExtendedTimeParams(params, contextTimeFilter)
// 获取会话列表(带摘要)
const sessions = await workerManager.getSessionSummaries(sessionId, {
limit: limit * 2, // 多查询一些以便过滤
timeFilter: effectiveTimeFilter,
})
if (!sessions || sessions.length === 0) {
return {
message: isChineseLocale(locale)
? '未找到带摘要的会话。可能还没有生成摘要,请在会话时间线中点击"批量生成"按钮。'
: 'No sessions with summaries found. Summaries may not have been generated yet.',
}
}
// 按关键词过滤
let filteredSessions = sessions
if (params.keywords && params.keywords.length > 0) {
const keywords = params.keywords.map((k) => k.toLowerCase())
filteredSessions = sessions.filter((s) =>
keywords.some((keyword) => s.summary?.toLowerCase().includes(keyword))
)
}
// 只返回有摘要的
filteredSessions = filteredSessions.filter((s) => s.summary)
// 限制数量
const limitedSessions = filteredSessions.slice(0, limit)
const localeStr = isChineseLocale(locale) ? 'zh-CN' : 'en-US'
return {
total: filteredSessions.length,
returned: limitedSessions.length,
timeRange: formatTimeRange(effectiveTimeFilter, locale),
sessions: limitedSessions.map((s) => {
const startTime = new Date(s.startTs * 1000).toLocaleString(localeStr)
const endTime = new Date(s.endTs * 1000).toLocaleString(localeStr)
return {
sessionId: s.id,
time: `${startTime} ~ ${endTime}`,
messageCount: s.messageCount,
participants: s.participants,
summary: s.summary,
}
}),
}
}
// ==================== 语义搜索工具 ====================
/**
@@ -1062,4 +1183,5 @@ registerTool(getConversationBetweenTool, getConversationBetweenExecutor)
registerTool(getMessageContextTool, getMessageContextExecutor)
registerTool(searchSessionsTool, searchSessionsExecutor)
registerTool(getSessionMessagesTool, getSessionMessagesExecutor)
registerTool(getSessionSummariesTool, getSessionSummariesExecutor)
registerTool(semanticSearchMessagesTool, semanticSearchMessagesExecutor)
-16
View File
@@ -709,22 +709,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
}
})
/**
* 设置语义搜索启用状态
*/
ipcMain.handle('embedding:setEnabled', async (_, enabled: boolean) => {
try {
rag.setEmbeddingEnabled(enabled)
if (!enabled) {
await rag.resetEmbeddingService()
}
return { success: true }
} catch (error) {
aiLogger.error('IPC', '设置语义搜索状态失败', error)
return { success: false, error: String(error) }
}
})
/**
* 添加 Embedding 配置
*/
+90
View File
@@ -631,6 +631,96 @@ export async function getSessionMessages(
return sendToWorker('getSessionMessages', { sessionId, chatSessionId, limit })
}
/**
* 会话摘要结果类型(用于 AI 工具)
*/
export interface SessionSummaryItem {
id: number
startTs: number
endTs: number
messageCount: number
participants: string[]
summary: string | null
}
/**
* 获取带摘要的会话列表(用于 AI 工具)
* 直接在主进程中查询,不通过 Worker
*/
export async function getSessionSummaries(
sessionId: string,
options: {
limit?: number
timeFilter?: { startTs: number; endTs: number }
}
): Promise<SessionSummaryItem[]> {
const { openDatabase } = await import('../database/core')
const db = openDatabase(sessionId, true)
if (!db) {
return []
}
const { limit = 50, timeFilter } = options
let sql = `
SELECT
cs.id,
cs.start_ts as startTs,
cs.end_ts as endTs,
cs.message_count as messageCount,
cs.summary
FROM chat_session cs
WHERE cs.summary IS NOT NULL AND cs.summary != ''
`
const params: unknown[] = []
if (timeFilter) {
sql += ' AND cs.start_ts >= ? AND cs.start_ts <= ?'
params.push(timeFilter.startTs, timeFilter.endTs)
}
sql += ' ORDER BY cs.start_ts DESC LIMIT ?'
params.push(limit)
try {
const sessions = db.prepare(sql).all(...params) as Array<{
id: number
startTs: number
endTs: number
messageCount: number
summary: string | null
}>
// 为每个会话获取参与者
const results: SessionSummaryItem[] = []
for (const session of sessions) {
const participantsSql = `
SELECT DISTINCT COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as name
FROM message_context mc
JOIN message m ON m.id = mc.message_id
JOIN member mb ON mb.id = m.sender_id
WHERE mc.session_id = ?
LIMIT 10
`
const participants = db.prepare(participantsSql).all(session.id) as Array<{ name: string }>
results.push({
id: session.id,
startTs: session.startTs,
endTs: session.endTs,
messageCount: session.messageCount,
participants: participants.map((p) => p.name),
summary: session.summary,
})
}
return results
} catch (error) {
console.error('获取会话摘要失败:', error)
return []
}
}
// ==================== 自定义筛选 API ====================
/**
-1
View File
@@ -466,7 +466,6 @@ interface EmbeddingApi {
getConfig: (id: string) => Promise<EmbeddingServiceConfig | null>
getActiveConfigId: () => Promise<string | null>
isEnabled: () => Promise<boolean>
setEnabled: (enabled: boolean) => Promise<{ success: boolean; error?: string }>
addConfig: (
config: Omit<EmbeddingServiceConfig, 'id' | 'createdAt' | 'updatedAt'>
) => Promise<{ success: boolean; config?: EmbeddingServiceConfig; error?: string }>
-7
View File
@@ -1484,13 +1484,6 @@ const embeddingApi = {
return ipcRenderer.invoke('embedding:isEnabled')
},
/**
* 设置语义搜索启用状态
*/
setEnabled: (enabled: boolean): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('embedding:setEnabled', enabled)
},
/**
* 添加 Embedding 配置
*/
@@ -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 enabled = computed(() => embeddingStore.enabled)
const hasConfig = computed(() => embeddingStore.hasConfig)
const isMaxConfigs = computed(() => embeddingStore.isMaxConfigs)
const vectorStoreStats = computed(() => embeddingStore.vectorStoreStats)
@@ -51,12 +50,6 @@ async function handleSaved() {
emit('config-changed')
}
async function handleToggleEnabled() {
const newValue = !enabled.value
await embeddingStore.setEnabled(newValue)
emit('config-changed')
}
async function handleSetActive(id: string) {
await embeddingStore.setActiveConfig(id)
emit('config-changed')
@@ -90,14 +83,11 @@ onMounted(() => {
<!-- 配置内容 -->
<div v-else class="space-y-6">
<!-- 标题和启用开关 -->
<div class="flex items-center justify-between">
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-magnifying-glass-circle" class="h-4 w-4 text-emerald-500" />
{{ t('settings.embedding.title') }}
</h4>
<USwitch :model-value="enabled" @update:model-value="handleToggleEnabled" />
</div>
<!-- 标题 -->
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
<UIcon name="i-heroicons-magnifying-glass-circle" class="h-4 w-4 text-emerald-500" />
{{ t('settings.embedding.title') }}
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.embedding.description') }}
+7 -7
View File
@@ -3,8 +3,8 @@
"tabs": {
"basic": "General",
"ai": "AI Settings",
"aiConfig": "AI Models",
"aiRAG": "Semantic Search",
"aiConfig": "Chat Model",
"aiRAG": "Vector Model",
"aiPrompt": "Chat Config",
"aiPreset": "Prompts",
"storage": "Data & Storage",
@@ -47,7 +47,7 @@
}
},
"aiConfig": {
"title": "Model Configuration",
"title": "Chat Model",
"loading": "Loading...",
"inUse": "Active",
"defaultModel": "Default Model",
@@ -211,8 +211,8 @@
}
},
"embedding": {
"title": "Semantic Search",
"description": "Uses embedding vectors to understand question meaning, suitable for analyzing relationships, emotions, and abstract topics",
"title": "Vector Model",
"description": "Uses embedding vectors to understand question meaning, enables AI to perform semantic search",
"configList": "Embedding Configs",
"addConfig": "Add Config",
"editConfig": "Edit Config",
@@ -223,8 +223,8 @@
"configName": "Config Name",
"configNamePlaceholder": "e.g. Ollama Embedding",
"apiSource": "API Source",
"apiSourceHint": "\"Reuse AI Config\" will use the endpoint and key from the active AI model config",
"reuseLLM": "Reuse AI Config",
"apiSourceHint": "\"Reuse Chat Model\" will use the endpoint and key from the active chat model",
"reuseLLM": "Reuse Chat Model",
"customAPI": "Custom API",
"model": "Model Name",
"modelPlaceholder": "e.g. nomic-embed-text",
+7 -7
View File
@@ -3,8 +3,8 @@
"tabs": {
"basic": "基础设置",
"ai": "AI 设置",
"aiConfig": "模型配置",
"aiRAG": "语义搜索",
"aiConfig": "对话模型",
"aiRAG": "向量模型",
"aiPrompt": "对话配置",
"aiPreset": "提示词配置",
"storage": "数据和存储",
@@ -47,7 +47,7 @@
}
},
"aiConfig": {
"title": "模型配置",
"title": "对话模型",
"loading": "加载中...",
"inUse": "使用中",
"defaultModel": "默认模型",
@@ -211,8 +211,8 @@
}
},
"embedding": {
"title": "语义搜索",
"description": "通过 Embedding 向量相似度理解问题含义,适合分析关系、情感、抽象话题等场景",
"title": "向量模型",
"description": "通过 Embedding 向量相似度理解问题含义,启用后 AI 可进行语义搜索",
"configList": "Embedding 配置",
"addConfig": "添加配置",
"editConfig": "编辑配置",
@@ -223,8 +223,8 @@
"configName": "配置名称",
"configNamePlaceholder": "如:Ollama Embedding",
"apiSource": "API 来源",
"apiSourceHint": "「复用 AI 配置」将使用当前激活的 AI 模型配置的端点和密钥",
"reuseLLM": "复用 AI 配置",
"apiSourceHint": "「复用对话模型」将使用当前激活的对话模型的端点和密钥",
"reuseLLM": "复用对话模型",
"customAPI": "自定义 API",
"model": "模型名称",
"modelPlaceholder": "如 nomic-embed-text",
-23
View File
@@ -15,9 +15,6 @@ export const useEmbeddingStore = defineStore('embedding', () => {
/** 当前激活配置 ID */
const activeConfigId = ref<string | null>(null)
/** 是否启用语义搜索 */
const enabled = ref(false)
/** 是否正在加载 */
const isLoading = ref(false)
@@ -77,7 +74,6 @@ export const useEmbeddingStore = defineStore('embedding', () => {
])
configs.value = configsData
activeConfigId.value = activeId
enabled.value = isEnabled
vectorStoreStats.value = stats
} catch (error) {
console.error('[Embedding Store] 加载配置失败:', error)
@@ -86,23 +82,6 @@ export const useEmbeddingStore = defineStore('embedding', () => {
}
}
/**
* 设置语义搜索启用状态
*/
async function setEnabled(value: boolean): Promise<boolean> {
try {
const result = await window.embeddingApi.setEnabled(value)
if (result.success) {
enabled.value = value
return true
}
console.error('[Embedding Store] 设置启用状态失败:', result.error)
return false
} catch (error) {
console.error('[Embedding Store] 设置启用状态失败:', error)
return false
}
}
/**
* 切换激活配置
@@ -170,7 +149,6 @@ export const useEmbeddingStore = defineStore('embedding', () => {
// 状态
configs,
activeConfigId,
enabled,
isLoading,
isInitialized,
vectorStoreStats,
@@ -182,7 +160,6 @@ export const useEmbeddingStore = defineStore('embedding', () => {
// 方法
init,
loadConfigs,
setEnabled,
setActiveConfig,
deleteConfig,
clearVectorStore,