mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
refactor: unify session list/detail queries into @openchatlab/core
Extract duplicated session info assembly from electron/main/worker/query/sessions.ts, packages/server/src/http/routes/web.ts, and sessions.ts into shared core functions. - Add CoreSessionInfo type covering all single-DB queryable AnalysisSession fields - Add buildSessionInfo pure mapper (meta + overview → flat info) - Add getSessionInfo convenience (one-call DB query) - Add getSummaryCount and getLastPlatformMessageId helpers - Electron: thin adapter with cache-first overview, platform-specific dbPath/avatar/aiCount - Server web.ts & sessions.ts: delegate to getSessionInfo, eliminate inline assembly - Add 9 unit tests for all new core session functions Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
9ad0c49acf
commit
f4d5010e77
7 files changed
+360
-227
No files matched your search
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* 会话管理查询模块
|
||||
* 负责会话列表与单会话基础信息查询
|
||||
*
|
||||
* Core session info logic delegated to @openchatlab/core (buildSessionInfo, getSessionMeta, etc.).
|
||||
* This file retains Electron-specific concerns: filesystem scanning, caching, private chat avatar,
|
||||
* dbPath, aiConversationCount, and the getChatOverview cache-first pattern.
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3'
|
||||
@@ -16,20 +20,25 @@ import {
|
||||
type OverviewCache,
|
||||
type MembersCache,
|
||||
} from '../../database/sessionCache'
|
||||
import {
|
||||
getSessionMeta,
|
||||
getSessionOverview,
|
||||
buildSessionInfo,
|
||||
getSummaryCount,
|
||||
getLastPlatformMessageId,
|
||||
type SessionOverview,
|
||||
} from '@openchatlab/core'
|
||||
import type { DatabaseAdapter } from '@openchatlab/core'
|
||||
|
||||
interface DbMeta {
|
||||
name: string
|
||||
platform: string
|
||||
type: string
|
||||
imported_at: number
|
||||
group_id: string | null
|
||||
group_avatar: string | null
|
||||
owner_id: string | null
|
||||
/**
|
||||
* Wrap a better-sqlite3 Database as a DatabaseAdapter for core query functions.
|
||||
*/
|
||||
function asCoreDb(db: Database.Database): DatabaseAdapter {
|
||||
return db as unknown as DatabaseAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为聊天会话数据库
|
||||
* 通过核心三表(meta/member/message)存在性快速识别
|
||||
* 判断是否为聊天会话数据库(local fast check, avoids core import overhead for non-session DBs)
|
||||
*/
|
||||
function isChatSessionDb(db: Database.Database): boolean {
|
||||
const requiredTableCount = db
|
||||
@@ -42,8 +51,11 @@ function isChatSessionDb(db: Database.Database): boolean {
|
||||
* 获取私聊对方成员的头像
|
||||
* 逻辑参考 private-chat/index.vue 的 otherMemberAvatar
|
||||
*/
|
||||
function getPrivateChatMemberAvatar(db: Database.Database, sessionName: string, ownerId: string | null): string | null {
|
||||
// 获取所有非系统消息成员(按消息数排序)
|
||||
function getPrivateChatMemberAvatar(
|
||||
db: Database.Database,
|
||||
sessionName: string,
|
||||
ownerId: string | null | undefined
|
||||
): string | null {
|
||||
const members = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
@@ -58,22 +70,34 @@ function getPrivateChatMemberAvatar(db: Database.Database, sessionName: string,
|
||||
|
||||
if (members.length === 0) return null
|
||||
|
||||
// 1. 优先排除 ownerId,找到另一成员的头像
|
||||
if (ownerId) {
|
||||
const other = members.find((m) => m.platformId !== ownerId)
|
||||
if (other?.avatar) return other.avatar
|
||||
}
|
||||
|
||||
// 2. 尝试匹配会话名称(私聊名称通常是对方昵称)
|
||||
const sameName = members.find((m) => m.name === sessionName)
|
||||
if (sameName?.avatar) return sameName.avatar
|
||||
|
||||
// 3. 如果只有两个成员且有 ownerId,取另一个(即使没有头像也返回 null)
|
||||
// 如果没有 ownerId,返回第一个有头像的成员
|
||||
const firstWithAvatar = members.find((m) => m.avatar)
|
||||
return firstWithAvatar?.avatar || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve overview from cache (with compute-on-miss), or fall back to live SQL via core.
|
||||
*/
|
||||
function resolveOverview(db: Database.Database, sessionId: string, cacheDir: string | null): SessionOverview {
|
||||
let cached = cacheDir ? getCache<OverviewCache>(sessionId, CACHE_KEY_OVERVIEW, cacheDir) : null
|
||||
if (!cached && cacheDir) {
|
||||
try {
|
||||
cached = computeAndSetOverviewCache(db, sessionId, cacheDir)
|
||||
} catch {
|
||||
// cache compute failure — fall through to live query
|
||||
}
|
||||
}
|
||||
if (cached) return cached
|
||||
return getSessionOverview(asCoreDb(db))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有会话列表
|
||||
*/
|
||||
@@ -94,89 +118,31 @@ export function getAllSessions(): any[] {
|
||||
const db = new Database(dbPath)
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
// 跳过非聊天会话数据库(例如内部索引库)
|
||||
if (!isChatSessionDb(db)) {
|
||||
db.close()
|
||||
continue
|
||||
}
|
||||
|
||||
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
|
||||
const coreDb = asCoreDb(db)
|
||||
const meta = getSessionMeta(coreDb)
|
||||
|
||||
if (meta) {
|
||||
// 优先从缓存读取,未命中则实时查询并生成缓存
|
||||
const cacheDir = getCacheDir()
|
||||
let overview = getCache<OverviewCache>(sessionId, CACHE_KEY_OVERVIEW, cacheDir)
|
||||
if (!overview && cacheDir) {
|
||||
try {
|
||||
overview = computeAndSetOverviewCache(db, sessionId, cacheDir)
|
||||
} catch {
|
||||
// 缓存计算失败不影响正常流程
|
||||
}
|
||||
}
|
||||
const overview = resolveOverview(db, sessionId, cacheDir)
|
||||
const summaryCount = getSummaryCount(coreDb)
|
||||
const info = buildSessionInfo(meta, overview, summaryCount)
|
||||
|
||||
const messageCount =
|
||||
overview?.totalMessages ??
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
const memberCount =
|
||||
overview?.totalMembers ??
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM member
|
||||
WHERE COALESCE(account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
// 私聊:获取对方成员头像
|
||||
let memberAvatar: string | null = null
|
||||
if (meta.type === 'private') {
|
||||
memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.owner_id)
|
||||
}
|
||||
|
||||
// 摘要数量:安全查询(chat_session 表可能不存在)
|
||||
let summaryCount = 0
|
||||
try {
|
||||
const hasChatSessionTable = db
|
||||
.prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='chat_session'")
|
||||
.get() as { cnt: number }
|
||||
if (hasChatSessionTable.cnt > 0) {
|
||||
summaryCount = (
|
||||
db
|
||||
.prepare("SELECT COUNT(*) as count FROM chat_session WHERE summary IS NOT NULL AND summary != ''")
|
||||
.get() as { count: number }
|
||||
).count
|
||||
}
|
||||
} catch {
|
||||
// 忽略查询错误
|
||||
memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.ownerId)
|
||||
}
|
||||
|
||||
sessions.push({
|
||||
...info,
|
||||
id: sessionId,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
importedAt: meta.imported_at,
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath,
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
ownerId: meta.owner_id || null,
|
||||
memberAvatar, // 私聊对方头像
|
||||
lastMessageTs: overview?.lastMessageTs ?? null,
|
||||
summaryCount,
|
||||
aiConversationCount: 0, // 将在 IPC 层由主进程填充
|
||||
memberAvatar,
|
||||
aiConversationCount: 0, // filled by IPC layer
|
||||
})
|
||||
}
|
||||
|
||||
@@ -196,101 +162,47 @@ export function getSession(sessionId: string): any | null {
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return null
|
||||
|
||||
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
|
||||
const coreDb = asCoreDb(db)
|
||||
const meta = getSessionMeta(coreDb)
|
||||
if (!meta) return null
|
||||
|
||||
const overview = getCache<OverviewCache>(sessionId, CACHE_KEY_OVERVIEW, getCacheDir())
|
||||
|
||||
const messageCount =
|
||||
overview?.totalMessages ??
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
const memberCount =
|
||||
overview?.totalMembers ??
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM member
|
||||
WHERE COALESCE(account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
// 时间范围(优先从 cache 获取)
|
||||
const firstTimestamp =
|
||||
overview?.firstMessageTs ??
|
||||
(db.prepare('SELECT MIN(ts) as v FROM message').get() as { v: number | null })?.v ??
|
||||
null
|
||||
const lastTimestamp =
|
||||
overview?.lastMessageTs ?? (db.prepare('SELECT MAX(ts) as v FROM message').get() as { v: number | null })?.v ?? null
|
||||
|
||||
// 最新的 platform_message_id(用于增量边界参考)
|
||||
const lastPmidRow = db
|
||||
.prepare('SELECT platform_message_id FROM message WHERE platform_message_id IS NOT NULL ORDER BY ts DESC LIMIT 1')
|
||||
.get() as { platform_message_id: string } | undefined
|
||||
const overview = resolveOverview(db, sessionId, getCacheDir())
|
||||
const info = buildSessionInfo(meta, overview)
|
||||
|
||||
return {
|
||||
...info,
|
||||
id: sessionId,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
importedAt: meta.imported_at,
|
||||
messageCount,
|
||||
memberCount,
|
||||
dbPath: getDbPath(sessionId),
|
||||
groupId: meta.group_id || null,
|
||||
groupAvatar: meta.group_avatar || null,
|
||||
ownerId: meta.owner_id || null,
|
||||
firstTimestamp,
|
||||
lastTimestamp,
|
||||
lastPlatformMessageId: lastPmidRow?.platform_message_id ?? null,
|
||||
firstTimestamp: overview.firstMessageTs,
|
||||
lastTimestamp: overview.lastMessageTs,
|
||||
lastPlatformMessageId: getLastPlatformMessageId(coreDb),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天概览(AI 工具使用)
|
||||
* 优先从缓存读取,miss 则计算回填
|
||||
* Cache-first: overview cache + members cache, fallback to live SQL via core.
|
||||
*/
|
||||
export function getChatOverview(sessionId: string, topN: number = 10) {
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return null
|
||||
|
||||
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
|
||||
const coreDb = asCoreDb(db)
|
||||
const meta = getSessionMeta(coreDb)
|
||||
if (!meta) return null
|
||||
|
||||
const cacheDir = getCacheDir()
|
||||
const overview = resolveOverview(db, sessionId, cacheDir)
|
||||
|
||||
// 读取 overview 缓存
|
||||
let overview = getCache<OverviewCache>(sessionId, CACHE_KEY_OVERVIEW, cacheDir)
|
||||
if (!overview) {
|
||||
try {
|
||||
overview = computeAndSetOverviewCache(db, sessionId, cacheDir)
|
||||
} catch {
|
||||
// fallback: 实时查询
|
||||
}
|
||||
}
|
||||
|
||||
// 读取 members 缓存
|
||||
let membersCache = getCache<MembersCache>(sessionId, CACHE_KEY_MEMBERS, cacheDir)
|
||||
if (!membersCache) {
|
||||
try {
|
||||
membersCache = computeAndSetMembersCache(db, sessionId, cacheDir)
|
||||
} catch {
|
||||
// fallback: 无成员数据
|
||||
// fallback: no member data
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存计算 Top N 活跃成员
|
||||
let topMembers: Array<{ id: number; name: string; count: number }> = []
|
||||
if (membersCache?.members) {
|
||||
topMembers = Object.entries(membersCache.members)
|
||||
@@ -299,35 +211,14 @@ export function getChatOverview(sessionId: string, topN: number = 10) {
|
||||
.slice(0, topN)
|
||||
}
|
||||
|
||||
// fallback 统计值
|
||||
const totalMessages =
|
||||
overview?.totalMessages ??
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
|
||||
const totalMembers =
|
||||
overview?.totalMembers ??
|
||||
(
|
||||
db.prepare(`SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'`).get() as {
|
||||
count: number
|
||||
}
|
||||
).count
|
||||
|
||||
return {
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
totalMessages,
|
||||
totalMembers,
|
||||
firstMessageTs: overview?.firstMessageTs ?? null,
|
||||
lastMessageTs: overview?.lastMessageTs ?? null,
|
||||
totalMessages: overview.totalMessages,
|
||||
totalMembers: overview.totalMembers,
|
||||
firstMessageTs: overview.firstMessageTs,
|
||||
lastMessageTs: overview.lastMessageTs,
|
||||
topMembers,
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ export {
|
||||
searchSessions,
|
||||
getSessionMessages,
|
||||
getSessionSummaries,
|
||||
buildSessionInfo,
|
||||
getSessionInfo,
|
||||
getSummaryCount,
|
||||
getLastPlatformMessageId,
|
||||
getTimeRange,
|
||||
getAvailableYears,
|
||||
getMemberActivity,
|
||||
@@ -81,6 +85,7 @@ export type {
|
||||
SessionMeta,
|
||||
SessionOverview,
|
||||
SessionInfo,
|
||||
CoreSessionInfo,
|
||||
ChatOverviewData,
|
||||
SessionSearchItem,
|
||||
SessionMessagesData,
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Minimal tests for session query functions.
|
||||
*
|
||||
* Verifies buildSessionInfo pure mapper and getSessionInfo / getSummaryCount / getLastPlatformMessageId
|
||||
* against a mock DatabaseAdapter.
|
||||
*
|
||||
* Run: npx tsx --test packages/core/src/query/__tests__/session-queries.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import type { SessionMeta, SessionOverview } from '../session-queries'
|
||||
import { buildSessionInfo, getSessionInfo, getSummaryCount, getLastPlatformMessageId } from '../session-queries'
|
||||
import type { DatabaseAdapter } from '../../interfaces'
|
||||
|
||||
// ==================== Mock helpers ====================
|
||||
|
||||
function makeMeta(overrides?: Partial<SessionMeta>): SessionMeta {
|
||||
return {
|
||||
name: 'Test Group',
|
||||
platform: 'wechat',
|
||||
type: 'group',
|
||||
importedAt: 1700000000,
|
||||
groupId: 'g001',
|
||||
groupAvatar: null,
|
||||
ownerId: 'u001',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function makeOverview(overrides?: Partial<SessionOverview>): SessionOverview {
|
||||
return {
|
||||
totalMessages: 500,
|
||||
totalMembers: 10,
|
||||
firstMessageTs: 1600000000,
|
||||
lastMessageTs: 1700000000,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
type QueryResult = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Create a mock DatabaseAdapter whose prepare().get()/all() respond based on SQL patterns.
|
||||
*/
|
||||
function createMockDb(handlers: Record<string, (args: unknown[]) => QueryResult | QueryResult[]>): DatabaseAdapter {
|
||||
return {
|
||||
prepare(sql: string) {
|
||||
return {
|
||||
get(...args: unknown[]) {
|
||||
for (const [pattern, handler] of Object.entries(handlers)) {
|
||||
if (sql.includes(pattern)) {
|
||||
return handler(args)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
all(...args: unknown[]) {
|
||||
for (const [pattern, handler] of Object.entries(handlers)) {
|
||||
if (sql.includes(pattern)) {
|
||||
const result = handler(args)
|
||||
return Array.isArray(result) ? result : [result]
|
||||
}
|
||||
}
|
||||
return []
|
||||
},
|
||||
run() {
|
||||
return { changes: 0, lastInsertRowid: 0 }
|
||||
},
|
||||
}
|
||||
},
|
||||
} as unknown as DatabaseAdapter
|
||||
}
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('buildSessionInfo', () => {
|
||||
it('composes meta + overview into flat CoreSessionInfo', () => {
|
||||
const meta = makeMeta()
|
||||
const overview = makeOverview()
|
||||
const info = buildSessionInfo(meta, overview, 3)
|
||||
|
||||
assert.equal(info.name, 'Test Group')
|
||||
assert.equal(info.platform, 'wechat')
|
||||
assert.equal(info.type, 'group')
|
||||
assert.equal(info.importedAt, 1700000000)
|
||||
assert.equal(info.messageCount, 500)
|
||||
assert.equal(info.memberCount, 10)
|
||||
assert.equal(info.firstMessageTs, 1600000000)
|
||||
assert.equal(info.lastMessageTs, 1700000000)
|
||||
assert.equal(info.groupId, 'g001')
|
||||
assert.equal(info.groupAvatar, null)
|
||||
assert.equal(info.ownerId, 'u001')
|
||||
assert.equal(info.summaryCount, 3)
|
||||
})
|
||||
|
||||
it('defaults summaryCount to 0 when omitted', () => {
|
||||
const info = buildSessionInfo(makeMeta(), makeOverview())
|
||||
assert.equal(info.summaryCount, 0)
|
||||
})
|
||||
|
||||
it('preserves null timestamps when overview has no messages', () => {
|
||||
const overview = makeOverview({ firstMessageTs: null, lastMessageTs: null })
|
||||
const info = buildSessionInfo(makeMeta(), overview)
|
||||
assert.equal(info.firstMessageTs, null)
|
||||
assert.equal(info.lastMessageTs, null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSessionInfo', () => {
|
||||
it('returns null when meta table is empty', () => {
|
||||
const db = createMockDb({
|
||||
'FROM meta': () => undefined as unknown as QueryResult,
|
||||
})
|
||||
assert.equal(getSessionInfo(db), null)
|
||||
})
|
||||
|
||||
it('returns combined CoreSessionInfo from DB', () => {
|
||||
const db = createMockDb({
|
||||
'FROM meta': () => ({
|
||||
name: 'Chat',
|
||||
platform: 'telegram',
|
||||
type: 'private',
|
||||
imported_at: 1700000000,
|
||||
group_id: null,
|
||||
group_avatar: null,
|
||||
owner_id: 'me',
|
||||
}),
|
||||
'COUNT(*)': () => ({ count: 42 }),
|
||||
'MIN(ts)': () => ({ v: 1600000000 }),
|
||||
'MAX(ts)': () => ({ v: 1700000000 }),
|
||||
sqlite_master: () => ({ cnt: 0 }),
|
||||
})
|
||||
|
||||
const info = getSessionInfo(db)
|
||||
assert.ok(info)
|
||||
assert.equal(info.name, 'Chat')
|
||||
assert.equal(info.platform, 'telegram')
|
||||
assert.equal(info.type, 'private')
|
||||
assert.equal(info.ownerId, 'me')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSummaryCount', () => {
|
||||
it('returns 0 when chat_session table does not exist', () => {
|
||||
const db = createMockDb({
|
||||
sqlite_master: () => ({ cnt: 0 }),
|
||||
})
|
||||
assert.equal(getSummaryCount(db), 0)
|
||||
})
|
||||
|
||||
it('returns count when chat_session table exists', () => {
|
||||
const db = createMockDb({
|
||||
sqlite_master: () => ({ cnt: 1 }),
|
||||
'FROM chat_session': () => ({ count: 7 }),
|
||||
})
|
||||
assert.equal(getSummaryCount(db), 7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLastPlatformMessageId', () => {
|
||||
it('returns null when no platform_message_id exists', () => {
|
||||
const db = createMockDb({
|
||||
platform_message_id: () => undefined as unknown as QueryResult,
|
||||
})
|
||||
assert.equal(getLastPlatformMessageId(db), null)
|
||||
})
|
||||
|
||||
it('returns the latest platform_message_id', () => {
|
||||
const db = createMockDb({
|
||||
platform_message_id: () => ({ platform_message_id: 'pmid-999' }),
|
||||
})
|
||||
assert.equal(getLastPlatformMessageId(db), 'pmid-999')
|
||||
})
|
||||
})
|
||||
@@ -9,11 +9,16 @@ export {
|
||||
searchSessions,
|
||||
getSessionMessages,
|
||||
getSessionSummaries,
|
||||
buildSessionInfo,
|
||||
getSessionInfo,
|
||||
getSummaryCount,
|
||||
getLastPlatformMessageId,
|
||||
} from './session-queries'
|
||||
export type {
|
||||
SessionMeta,
|
||||
SessionOverview,
|
||||
SessionInfo,
|
||||
CoreSessionInfo,
|
||||
ChatOverviewData,
|
||||
SessionSearchItem,
|
||||
SessionMessagesData,
|
||||
|
||||
@@ -32,6 +32,87 @@ export interface SessionInfo extends SessionMeta {
|
||||
overview: SessionOverview
|
||||
}
|
||||
|
||||
// ==================== Flat session info (for AnalysisSession-like consumers) ====================
|
||||
|
||||
/**
|
||||
* Platform-agnostic session info built from meta + overview.
|
||||
* Covers all AnalysisSession fields that are queryable from a single DB.
|
||||
* Platform-specific fields (dbPath, memberAvatar, aiConversationCount) are left to callers.
|
||||
*/
|
||||
export interface CoreSessionInfo {
|
||||
name: string
|
||||
platform: string
|
||||
type: string
|
||||
importedAt: number
|
||||
messageCount: number
|
||||
memberCount: number
|
||||
groupId: string | null
|
||||
groupAvatar: string | null
|
||||
ownerId: string | null
|
||||
firstMessageTs: number | null
|
||||
lastMessageTs: number | null
|
||||
summaryCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure mapper: compose SessionMeta + SessionOverview into flat CoreSessionInfo.
|
||||
* Callers provide the inputs which may come from cache or fresh SQL.
|
||||
*/
|
||||
export function buildSessionInfo(
|
||||
meta: SessionMeta,
|
||||
overview: SessionOverview,
|
||||
summaryCount: number = 0
|
||||
): CoreSessionInfo {
|
||||
return {
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
importedAt: meta.importedAt,
|
||||
messageCount: overview.totalMessages,
|
||||
memberCount: overview.totalMembers,
|
||||
groupId: meta.groupId,
|
||||
groupAvatar: meta.groupAvatar,
|
||||
ownerId: meta.ownerId,
|
||||
firstMessageTs: overview.firstMessageTs,
|
||||
lastMessageTs: overview.lastMessageTs,
|
||||
summaryCount,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: read meta + overview from DB and return flat CoreSessionInfo.
|
||||
*/
|
||||
export function getSessionInfo(db: DatabaseAdapter): CoreSessionInfo | null {
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) return null
|
||||
const overview = getSessionOverview(db)
|
||||
const sc = getSummaryCount(db)
|
||||
return buildSessionInfo(meta, overview, sc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of chat sessions that have an AI-generated summary.
|
||||
*/
|
||||
export function getSummaryCount(db: DatabaseAdapter): number {
|
||||
if (!hasTable(db, 'chat_session')) return 0
|
||||
const row = db
|
||||
.prepare("SELECT COUNT(*) as count FROM chat_session WHERE summary IS NOT NULL AND summary != ''")
|
||||
.get() as { count: number } | undefined
|
||||
return row?.count ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest platform_message_id (used as incremental import boundary).
|
||||
*/
|
||||
export function getLastPlatformMessageId(db: DatabaseAdapter): string | null {
|
||||
const row = db
|
||||
.prepare('SELECT platform_message_id FROM message WHERE platform_message_id IS NOT NULL ORDER BY ts DESC LIMIT 1')
|
||||
.get() as { platform_message_id: string } | undefined
|
||||
return row?.platform_message_id ?? null
|
||||
}
|
||||
|
||||
// ==================== Core identification ====================
|
||||
|
||||
/**
|
||||
* 判断数据库是否为聊天会话数据库
|
||||
* 通过核心三表(meta/member/message)存在性快速识别
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import {
|
||||
getSessionInfo,
|
||||
getSessionMeta,
|
||||
getSessionOverview,
|
||||
queryMessages,
|
||||
@@ -35,19 +36,18 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
.map((id) => {
|
||||
const db = dbManager.open(id)
|
||||
if (!db) return null
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) return null
|
||||
const overview = getSessionOverview(db)
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) return null
|
||||
return {
|
||||
id,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
groupId: meta.groupId || undefined,
|
||||
messageCount: overview.totalMessages,
|
||||
memberCount: overview.totalMembers,
|
||||
firstTimestamp: overview.firstMessageTs,
|
||||
lastTimestamp: overview.lastMessageTs,
|
||||
name: info.name,
|
||||
platform: info.platform,
|
||||
type: info.type,
|
||||
groupId: info.groupId || undefined,
|
||||
messageCount: info.messageCount,
|
||||
memberCount: info.memberCount,
|
||||
firstTimestamp: info.firstMessageTs,
|
||||
lastTimestamp: info.lastMessageTs,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -57,19 +57,18 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
// GET /api/v1/sessions/:id — Single session detail
|
||||
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id', async (request) => {
|
||||
const db = ensureDb(dbManager, request.params.id)
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) throw sessionNotFound(request.params.id)
|
||||
const overview = getSessionOverview(db)
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) throw sessionNotFound(request.params.id)
|
||||
return successResponse({
|
||||
id: request.params.id,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
groupId: meta.groupId || undefined,
|
||||
messageCount: overview.totalMessages,
|
||||
memberCount: overview.totalMembers,
|
||||
firstTimestamp: overview.firstMessageTs,
|
||||
lastTimestamp: overview.lastMessageTs,
|
||||
name: info.name,
|
||||
platform: info.platform,
|
||||
type: info.type,
|
||||
groupId: info.groupId || undefined,
|
||||
messageCount: info.messageCount,
|
||||
memberCount: info.memberCount,
|
||||
firstTimestamp: info.firstMessageTs,
|
||||
lastTimestamp: info.lastMessageTs,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import {
|
||||
getSessionMeta,
|
||||
getSessionOverview,
|
||||
getSessionInfo,
|
||||
getTimeRange,
|
||||
getAvailableYears,
|
||||
getMemberActivity,
|
||||
@@ -80,24 +79,13 @@ export function registerWebRoutes(server: FastifyInstance, dbManager: DatabaseMa
|
||||
.map((id) => {
|
||||
const db = dbManager.open(id)
|
||||
if (!db) return null
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) return null
|
||||
const overview = getSessionOverview(db)
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) return null
|
||||
return {
|
||||
...info,
|
||||
id,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
importedAt: meta.importedAt ?? 0,
|
||||
messageCount: overview.totalMessages,
|
||||
memberCount: overview.totalMembers,
|
||||
dbPath: '',
|
||||
groupId: meta.groupId ?? null,
|
||||
groupAvatar: meta.groupAvatar ?? null,
|
||||
ownerId: meta.ownerId ?? null,
|
||||
memberAvatar: null,
|
||||
lastMessageTs: overview.lastMessageTs ?? null,
|
||||
summaryCount: 0,
|
||||
aiConversationCount: 0,
|
||||
}
|
||||
})
|
||||
@@ -106,24 +94,13 @@ export function registerWebRoutes(server: FastifyInstance, dbManager: DatabaseMa
|
||||
|
||||
server.get<{ Params: { id: string } }>('/_web/sessions/:id', async (request) => {
|
||||
const db = ensureDb(dbManager, request.params.id)
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) return null
|
||||
const overview = getSessionOverview(db)
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) return null
|
||||
return {
|
||||
...info,
|
||||
id: request.params.id,
|
||||
name: meta.name,
|
||||
platform: meta.platform,
|
||||
type: meta.type,
|
||||
importedAt: meta.importedAt ?? 0,
|
||||
messageCount: overview.totalMessages,
|
||||
memberCount: overview.totalMembers,
|
||||
dbPath: '',
|
||||
groupId: meta.groupId ?? null,
|
||||
groupAvatar: meta.groupAvatar ?? null,
|
||||
ownerId: meta.ownerId ?? null,
|
||||
memberAvatar: null,
|
||||
lastMessageTs: overview.lastMessageTs ?? null,
|
||||
summaryCount: 0,
|
||||
aiConversationCount: 0,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user