mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
refactor: unify AI tool session queries via core
Align searchSessions and getSessionMessages in @openchatlab/core with Electron's richer return shape and FTS support: - Add senderId, senderPlatformId to SessionPreviewMessage type and SQL - Add optional ftsMatchExpression param to searchSessions for FTS path - Electron aiTools.ts: delegates to core with FTS tokenization at edge - Electron types.ts: re-export SessionSearchResultItem/SessionMessagesResult from core - workerManager.ts: remove duplicate type definitions, re-export from session module Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
5d31184400
commit
6a44547c67
6 files changed
+82
-306
No files matched your search
@@ -1,134 +1,40 @@
|
||||
/**
|
||||
* AI 工具专用查询模块
|
||||
* 提供搜索会话和获取会话消息等功能,供 AI 工具使用
|
||||
* AI tool session queries — Electron worker wrappers.
|
||||
* Core search/messages logic lives in @openchatlab/core;
|
||||
* Electron adds FTS tokenization and DB lifecycle.
|
||||
*/
|
||||
|
||||
import { searchSessions as coreSearchSessions, getSessionMessages as coreGetSessionMessages } from '@openchatlab/core'
|
||||
import type { SessionSearchItem, SessionMessagesData } from '@openchatlab/core'
|
||||
import { openReadonlyDatabase } from './core'
|
||||
import type { SessionSearchResultItem, SessionMessagesResult } from './types'
|
||||
import { wrapAsDatabaseAdapter } from '../../core'
|
||||
import { hasFtsIndex } from '../fts'
|
||||
import { tokenizeQueryForFts } from '../../../nlp/ftsTokenizer'
|
||||
|
||||
/**
|
||||
* 搜索会话(用于 AI 工具)
|
||||
* 支持按关键词和时间范围筛选会话
|
||||
*
|
||||
* @param sessionId 数据库会话ID
|
||||
* @param keywords 关键词列表(可选,或逻辑匹配)
|
||||
* @param timeFilter 时间过滤器(可选)
|
||||
* @param limit 返回数量限制,默认 20
|
||||
* @param previewCount 预览消息数量,默认 5
|
||||
* @returns 匹配的会话列表
|
||||
*/
|
||||
// Re-export core types under Electron-local aliases
|
||||
export type { SessionSearchItem as SessionSearchResultItem }
|
||||
export type { SessionMessagesData as SessionMessagesResult }
|
||||
|
||||
export function searchSessions(
|
||||
sessionId: string,
|
||||
keywords?: string[],
|
||||
timeFilter?: { startTs: number; endTs: number },
|
||||
limit: number = 20,
|
||||
previewCount: number = 5
|
||||
): SessionSearchResultItem[] {
|
||||
): SessionSearchItem[] {
|
||||
const db = openReadonlyDatabase(sessionId)
|
||||
if (!db) {
|
||||
return []
|
||||
}
|
||||
if (!db) return []
|
||||
|
||||
try {
|
||||
// 1. 构建会话查询 SQL
|
||||
let sessionSql = `
|
||||
SELECT
|
||||
cs.id,
|
||||
cs.start_ts as startTs,
|
||||
cs.end_ts as endTs,
|
||||
cs.message_count as messageCount
|
||||
FROM chat_session cs
|
||||
WHERE 1=1
|
||||
`
|
||||
const params: unknown[] = []
|
||||
const adapter = wrapAsDatabaseAdapter(db)
|
||||
|
||||
// 时间范围过滤
|
||||
if (timeFilter) {
|
||||
sessionSql += ` AND cs.start_ts >= ? AND cs.end_ts <= ?`
|
||||
params.push(timeFilter.startTs, timeFilter.endTs)
|
||||
let ftsMatchExpression: string | undefined
|
||||
if (keywords && keywords.length > 0 && hasFtsIndex(sessionId)) {
|
||||
const match = tokenizeQueryForFts(keywords)
|
||||
if (match) ftsMatchExpression = match
|
||||
}
|
||||
|
||||
// 关键词过滤:只返回包含关键词的会话
|
||||
if (keywords && keywords.length > 0) {
|
||||
const useFts = hasFtsIndex(sessionId)
|
||||
const matchQuery = useFts ? tokenizeQueryForFts(keywords) : ''
|
||||
|
||||
if (useFts && matchQuery) {
|
||||
sessionSql += `
|
||||
AND cs.id IN (
|
||||
SELECT DISTINCT mc.session_id
|
||||
FROM message_context mc
|
||||
WHERE mc.message_id IN (SELECT rowid FROM message_fts WHERE content MATCH ?)
|
||||
)
|
||||
`
|
||||
params.push(matchQuery)
|
||||
} else {
|
||||
const keywordConditions = keywords.map(() => `m.content LIKE ?`).join(' OR ')
|
||||
sessionSql += `
|
||||
AND cs.id IN (
|
||||
SELECT DISTINCT mc.session_id
|
||||
FROM message_context mc
|
||||
JOIN message m ON m.id = mc.message_id
|
||||
WHERE (${keywordConditions})
|
||||
)
|
||||
`
|
||||
for (const kw of keywords) {
|
||||
params.push(`%${kw}%`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionSql += ` ORDER BY cs.start_ts DESC LIMIT ?`
|
||||
params.push(limit)
|
||||
|
||||
const sessions = db.prepare(sessionSql).all(...params) as Array<{
|
||||
id: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
}>
|
||||
|
||||
// 2. 为每个会话获取预览消息
|
||||
const previewSql = `
|
||||
SELECT
|
||||
m.id,
|
||||
mb.id as senderId,
|
||||
COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
mb.platform_id as senderPlatformId,
|
||||
m.content,
|
||||
m.ts as timestamp
|
||||
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 = ?
|
||||
ORDER BY m.ts ASC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
const results: SessionSearchResultItem[] = []
|
||||
for (const session of sessions) {
|
||||
const previewMessages = db.prepare(previewSql).all(session.id, previewCount) as Array<{
|
||||
id: number
|
||||
senderId: number
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
|
||||
results.push({
|
||||
id: session.id,
|
||||
startTs: session.startTs,
|
||||
endTs: session.endTs,
|
||||
messageCount: session.messageCount,
|
||||
isComplete: session.messageCount <= previewCount,
|
||||
previewMessages,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
return coreSearchSessions(adapter, keywords, timeFilter, limit, previewCount, ftsMatchExpression)
|
||||
} catch (error) {
|
||||
console.error('searchSessions error:', error)
|
||||
return []
|
||||
@@ -137,89 +43,17 @@ export function searchSessions(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的完整消息(用于 AI 工具)
|
||||
*
|
||||
* @param sessionId 数据库会话ID
|
||||
* @param chatSessionId 会话索引中的会话ID
|
||||
* @param limit 返回数量限制,默认 500
|
||||
* @returns 会话的完整消息
|
||||
*/
|
||||
export function getSessionMessages(
|
||||
sessionId: string,
|
||||
chatSessionId: number,
|
||||
limit: number = 500
|
||||
): SessionMessagesResult | null {
|
||||
): SessionMessagesData | null {
|
||||
const db = openReadonlyDatabase(sessionId)
|
||||
if (!db) {
|
||||
return null
|
||||
}
|
||||
if (!db) return null
|
||||
|
||||
try {
|
||||
// 1. 获取会话基本信息
|
||||
const sessionSql = `
|
||||
SELECT
|
||||
id,
|
||||
start_ts as startTs,
|
||||
end_ts as endTs,
|
||||
message_count as messageCount
|
||||
FROM chat_session
|
||||
WHERE id = ?
|
||||
`
|
||||
const session = db.prepare(sessionSql).get(chatSessionId) as
|
||||
| {
|
||||
id: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
}
|
||||
| undefined
|
||||
|
||||
if (!session) {
|
||||
db.close()
|
||||
return null
|
||||
}
|
||||
|
||||
// 2. 获取会话消息
|
||||
const messagesSql = `
|
||||
SELECT
|
||||
m.id,
|
||||
mb.id as senderId,
|
||||
COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
mb.platform_id as senderPlatformId,
|
||||
m.content,
|
||||
m.ts as timestamp
|
||||
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 = ?
|
||||
ORDER BY m.ts ASC
|
||||
LIMIT ?
|
||||
`
|
||||
const messages = db.prepare(messagesSql).all(chatSessionId, limit) as Array<{
|
||||
id: number
|
||||
senderId: number
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
|
||||
// 3. 统计参与者
|
||||
const participantsSet = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
participantsSet.add(msg.senderName)
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
startTs: session.startTs,
|
||||
endTs: session.endTs,
|
||||
messageCount: session.messageCount,
|
||||
returnedCount: messages.length,
|
||||
participants: Array.from(participantsSet),
|
||||
messages,
|
||||
}
|
||||
const adapter = wrapAsDatabaseAdapter(db)
|
||||
return coreGetSessionMessages(adapter, chatSessionId, limit)
|
||||
} catch (error) {
|
||||
console.error('getSessionMessages error:', error)
|
||||
return null
|
||||
|
||||
@@ -7,44 +7,8 @@
|
||||
export { DEFAULT_SESSION_GAP_THRESHOLD } from '@openchatlab/core'
|
||||
export type { ChatSessionItem, SessionIndexStats } from '@openchatlab/core'
|
||||
|
||||
/**
|
||||
* 会话搜索结果项类型(用于 AI 工具)
|
||||
*/
|
||||
export interface SessionSearchResultItem {
|
||||
id: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
/** 是否为完整会话(消息数 <= 预览条数) */
|
||||
isComplete: boolean
|
||||
/** 预览消息列表 */
|
||||
previewMessages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息结果类型(用于 AI 工具)
|
||||
*/
|
||||
export interface SessionMessagesResult {
|
||||
sessionId: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
returnedCount: number
|
||||
/** 参与者列表 */
|
||||
participants: string[]
|
||||
/** 消息列表 */
|
||||
messages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
}
|
||||
// AI tool types — re-exported from core via aiTools.ts
|
||||
export type { SessionSearchResultItem, SessionMessagesResult } from './aiTools'
|
||||
|
||||
/**
|
||||
* 自定义筛选消息类型(完整信息,兼容 MessageList 组件)
|
||||
|
||||
@@ -772,62 +772,23 @@ export async function getSessions(sessionId: string): Promise<ChatSessionItem[]>
|
||||
|
||||
// ==================== AI 工具专用查询函数 ====================
|
||||
|
||||
/**
|
||||
* 会话搜索结果项类型(用于 AI 工具)
|
||||
*/
|
||||
export interface SessionSearchResultItem {
|
||||
id: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
isComplete: boolean
|
||||
previewMessages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
}
|
||||
export type { SessionSearchResultItem, SessionMessagesResult } from './query/session'
|
||||
|
||||
/**
|
||||
* 搜索会话(用于 AI 工具)
|
||||
*/
|
||||
export async function searchSessions(
|
||||
sessionId: string,
|
||||
keywords?: string[],
|
||||
timeFilter?: { startTs: number; endTs: number },
|
||||
limit?: number,
|
||||
previewCount?: number
|
||||
): Promise<SessionSearchResultItem[]> {
|
||||
): Promise<import('./query/session').SessionSearchResultItem[]> {
|
||||
return sendToWorker('searchSessions', { sessionId, keywords, timeFilter, limit, previewCount })
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息结果类型(用于 AI 工具)
|
||||
*/
|
||||
export interface SessionMessagesResult {
|
||||
sessionId: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
returnedCount: number
|
||||
participants: string[]
|
||||
messages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的完整消息(用于 AI 工具)
|
||||
*/
|
||||
export async function getSessionMessages(
|
||||
sessionId: string,
|
||||
chatSessionId: number,
|
||||
limit?: number
|
||||
): Promise<SessionMessagesResult | null> {
|
||||
): Promise<import('./query/session').SessionMessagesResult | null> {
|
||||
return sendToWorker('getSessionMessages', { sessionId, chatSessionId, limit })
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ export type {
|
||||
SessionSummaryData,
|
||||
ChatSessionItem,
|
||||
SessionIndexStats,
|
||||
SessionPreviewMessage,
|
||||
SearchSessionsOptions,
|
||||
MemberActivity,
|
||||
HourlyActivity,
|
||||
DailyActivity,
|
||||
|
||||
@@ -36,6 +36,8 @@ export type {
|
||||
SessionSummaryData,
|
||||
ChatSessionItem,
|
||||
SessionIndexStats,
|
||||
SessionPreviewMessage,
|
||||
SearchSessionsOptions,
|
||||
} from './session-queries'
|
||||
|
||||
export {
|
||||
|
||||
@@ -196,18 +196,22 @@ export interface ChatOverviewData {
|
||||
topMembers: Array<{ id: number; name: string; count: number }>
|
||||
}
|
||||
|
||||
export interface SessionPreviewMessage {
|
||||
id: number
|
||||
senderId: number
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface SessionSearchItem {
|
||||
id: number
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
isComplete: boolean
|
||||
previewMessages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
previewMessages: SessionPreviewMessage[]
|
||||
}
|
||||
|
||||
export interface SessionMessagesData {
|
||||
@@ -217,12 +221,16 @@ export interface SessionMessagesData {
|
||||
messageCount: number
|
||||
returnedCount: number
|
||||
participants: string[]
|
||||
messages: Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
messages: SessionPreviewMessage[]
|
||||
}
|
||||
|
||||
export interface SearchSessionsOptions {
|
||||
keywords?: string[]
|
||||
timeFilter?: TimeFilter
|
||||
limit?: number
|
||||
previewCount?: number
|
||||
/** Pre-tokenized FTS match expression. When provided, uses FTS index for keyword filtering. */
|
||||
ftsMatchExpression?: string
|
||||
}
|
||||
|
||||
export interface SessionSummaryData {
|
||||
@@ -266,14 +274,15 @@ export function getChatOverview(db: DatabaseAdapter, topN: number = 10): ChatOve
|
||||
/**
|
||||
* Search chat sessions with optional keyword and time filters.
|
||||
* Requires chat_session and message_context tables (session indexing).
|
||||
* Uses LIKE-based keyword search (no FTS dependency).
|
||||
* Supports LIKE-based search and optional FTS when ftsMatchExpression is provided.
|
||||
*/
|
||||
export function searchSessions(
|
||||
db: DatabaseAdapter,
|
||||
keywords?: string[],
|
||||
timeFilter?: TimeFilter,
|
||||
limit: number = 20,
|
||||
previewCount: number = 5
|
||||
previewCount: number = 5,
|
||||
ftsMatchExpression?: string
|
||||
): SessionSearchItem[] {
|
||||
if (!hasTable(db, 'chat_session')) return []
|
||||
|
||||
@@ -293,16 +302,26 @@ export function searchSessions(
|
||||
}
|
||||
|
||||
if (keywords && keywords.length > 0) {
|
||||
const keywordConditions = keywords.map(() => 'm.content LIKE ?').join(' OR ')
|
||||
sessionSql += `
|
||||
AND cs.id IN (
|
||||
SELECT DISTINCT mc.session_id FROM message_context mc
|
||||
JOIN message m ON m.id = mc.message_id
|
||||
WHERE (${keywordConditions})
|
||||
)
|
||||
`
|
||||
for (const kw of keywords) {
|
||||
params.push(`%${kw}%`)
|
||||
if (ftsMatchExpression) {
|
||||
sessionSql += `
|
||||
AND cs.id IN (
|
||||
SELECT DISTINCT mc.session_id FROM message_context mc
|
||||
WHERE mc.message_id IN (SELECT rowid FROM message_fts WHERE content MATCH ?)
|
||||
)
|
||||
`
|
||||
params.push(ftsMatchExpression)
|
||||
} else {
|
||||
const keywordConditions = keywords.map(() => 'm.content LIKE ?').join(' OR ')
|
||||
sessionSql += `
|
||||
AND cs.id IN (
|
||||
SELECT DISTINCT mc.session_id FROM message_context mc
|
||||
JOIN message m ON m.id = mc.message_id
|
||||
WHERE (${keywordConditions})
|
||||
)
|
||||
`
|
||||
for (const kw of keywords) {
|
||||
params.push(`%${kw}%`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +336,9 @@ export function searchSessions(
|
||||
}>
|
||||
|
||||
const previewSql = `
|
||||
SELECT m.id, COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
SELECT m.id, mb.id as senderId,
|
||||
COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
mb.platform_id as senderPlatformId,
|
||||
m.content, m.ts as timestamp
|
||||
FROM message_context mc
|
||||
JOIN message m ON m.id = mc.message_id
|
||||
@@ -326,12 +347,7 @@ export function searchSessions(
|
||||
`
|
||||
|
||||
return sessions.map((session) => {
|
||||
const previewMessages = db.prepare(previewSql).all(session.id, previewCount) as Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
const previewMessages = db.prepare(previewSql).all(session.id, previewCount) as unknown as SessionPreviewMessage[]
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
@@ -365,19 +381,16 @@ export function getSessionMessages(
|
||||
|
||||
const messages = db
|
||||
.prepare(
|
||||
`SELECT m.id, COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
`SELECT m.id, mb.id as senderId,
|
||||
COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
mb.platform_id as senderPlatformId,
|
||||
m.content, m.ts as timestamp
|
||||
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 = ? ORDER BY m.ts ASC LIMIT ?`
|
||||
)
|
||||
.all(chatSessionId, limit) as Array<{
|
||||
id: number
|
||||
senderName: string
|
||||
content: string | null
|
||||
timestamp: number
|
||||
}>
|
||||
.all(chatSessionId, limit) as unknown as SessionPreviewMessage[]
|
||||
|
||||
const participantsSet = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
|
||||
Reference in new issue
Block a user