mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-16 03:20:19 +08:00
feat: 逻辑优化
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ export {
|
||||
updateEmbeddingConfig,
|
||||
deleteEmbeddingConfig,
|
||||
setActiveEmbeddingConfig,
|
||||
setEmbeddingEnabled,
|
||||
isEmbeddingEnabled,
|
||||
getActiveEmbeddingConfigId,
|
||||
// 旧版兼容
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 配置
|
||||
*/
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
/**
|
||||
|
||||
Vendored
-1
@@ -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 }>
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user