mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(summary): track source message coverage
This commit is contained in:
1 parent
2a412b228a
commit
3c55352d4b
16 files changed
+246
-44
No files matched your search
@@ -996,6 +996,8 @@ export interface ChatSessionItem {
|
||||
endTs: number
|
||||
messageCount: number
|
||||
firstMessageId: number
|
||||
summary?: string | null
|
||||
summaryMessageCount?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getChatSessionList,
|
||||
getSegmentSummary,
|
||||
saveSegmentSummary,
|
||||
loadSegmentMessages,
|
||||
updateSessionGapThreshold,
|
||||
clearSessionIndex,
|
||||
generateSessionIndex,
|
||||
@@ -85,7 +86,8 @@ function createSqliteDb(): TestSqliteDb {
|
||||
end_ts INTEGER NOT NULL,
|
||||
message_count INTEGER NOT NULL,
|
||||
is_manual INTEGER DEFAULT 0,
|
||||
summary TEXT
|
||||
summary TEXT,
|
||||
summary_message_count INTEGER
|
||||
);
|
||||
CREATE TABLE message_context (
|
||||
message_id INTEGER NOT NULL,
|
||||
@@ -276,6 +278,53 @@ describe('getSegmentSummary / saveSegmentSummary', () => {
|
||||
saveSegmentSummary(db, 1, 'Test summary')
|
||||
assert.equal(getSegmentSummary(db, 1), 'Test summary')
|
||||
})
|
||||
|
||||
it('keeps an appended summary visible but treats it as stale', () => {
|
||||
const db = createSqliteDb()
|
||||
seedMessages(db, [{ id: 1, ts: 1000 }])
|
||||
generateSessionIndex(db, 2000)
|
||||
saveSegmentSummary(db, 1, 'Summary before append')
|
||||
|
||||
assert.equal(getSegmentSummary(db, 1), 'Summary before append')
|
||||
seedMessages(db, [{ id: 2, ts: 1500 }])
|
||||
generateIncrementalSessionIndex(db, 2000)
|
||||
|
||||
assert.equal(getSegmentSummary(db, 1), null)
|
||||
assert.deepEqual(getChatSessionList(db)[0], {
|
||||
id: 1,
|
||||
startTs: 1000,
|
||||
endTs: 1500,
|
||||
messageCount: 2,
|
||||
summary: 'Summary before append',
|
||||
summaryMessageCount: 1,
|
||||
firstMessageId: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadSegmentMessages', () => {
|
||||
it('loads every message for summary generation instead of truncating at 500', () => {
|
||||
const db = createSqliteDb()
|
||||
db.exec(`
|
||||
ALTER TABLE message ADD COLUMN sender_id INTEGER;
|
||||
ALTER TABLE message ADD COLUMN content TEXT;
|
||||
CREATE TABLE member (
|
||||
id INTEGER PRIMARY KEY,
|
||||
platform_id TEXT NOT NULL,
|
||||
account_name TEXT,
|
||||
group_nickname TEXT
|
||||
);
|
||||
INSERT INTO member (id, platform_id, account_name) VALUES (1, 'alice', 'Alice');
|
||||
`)
|
||||
|
||||
const insertMessage = db.prepare('INSERT INTO message (id, ts, sender_id, content) VALUES (?, ?, 1, ?)')
|
||||
for (let id = 1; id <= 600; id++) insertMessage.run(id, 1000 + id, `message-${id}`)
|
||||
generateSessionIndex(db, 2000)
|
||||
|
||||
const messages = loadSegmentMessages(db, 1)
|
||||
assert.equal(messages?.length, 600)
|
||||
assert.equal(messages?.[599]?.content, 'message-600')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSessionGapThreshold', () => {
|
||||
|
||||
@@ -56,10 +56,10 @@ function createSessionDb(): SqliteTestAdapter {
|
||||
(1, 1, 100, 0, 'one', 'pm-1'),
|
||||
(2, 1, 300, 0, 'three', 'pm-3'),
|
||||
(3, 2, 200, 0, 'two', NULL);
|
||||
INSERT INTO segment (start_ts, end_ts, message_count, summary) VALUES
|
||||
(100, 150, 1, 'summary one'),
|
||||
(200, 250, 1, NULL),
|
||||
(300, 350, 1, 'summary two');
|
||||
INSERT INTO segment (start_ts, end_ts, message_count, summary, summary_message_count) VALUES
|
||||
(100, 150, 1, 'summary one', 1),
|
||||
(200, 250, 1, NULL, NULL),
|
||||
(300, 350, 1, 'summary two', 1);
|
||||
`)
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -95,9 +95,14 @@ export function getSessionInfo(db: DatabaseAdapter): CoreSessionInfo | null {
|
||||
*/
|
||||
export function getSummaryCount(db: DatabaseAdapter): number {
|
||||
if (!hasTable(db, 'segment')) return 0
|
||||
const row = db.prepare("SELECT COUNT(*) as count FROM segment WHERE summary IS NOT NULL AND summary != ''").get() as
|
||||
| { count: number }
|
||||
| undefined
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM segment
|
||||
WHERE summary IS NOT NULL AND summary != ''
|
||||
AND summary_message_count = message_count`
|
||||
)
|
||||
.get() as { count: number } | undefined
|
||||
return row?.count ?? 0
|
||||
}
|
||||
|
||||
@@ -320,6 +325,7 @@ export function getSegmentSummaries(
|
||||
cs.message_count as messageCount, cs.summary
|
||||
FROM segment cs
|
||||
WHERE cs.summary IS NOT NULL AND cs.summary != ''
|
||||
AND cs.summary_message_count = cs.message_count
|
||||
`
|
||||
const params: unknown[] = []
|
||||
|
||||
@@ -388,6 +394,8 @@ export interface ChatSessionItem {
|
||||
messageCount: number
|
||||
firstMessageId: number
|
||||
summary?: string | null
|
||||
/** Number of messages covered when the summary was generated. Null means legacy/unknown coverage. */
|
||||
summaryMessageCount?: number | null
|
||||
}
|
||||
|
||||
export interface SessionIndexStats {
|
||||
@@ -449,6 +457,7 @@ export function getSessionsByTimeRange(db: DatabaseAdapter, startTs: number, end
|
||||
`SELECT
|
||||
id, start_ts as startTs, end_ts as endTs,
|
||||
message_count as messageCount, summary,
|
||||
summary_message_count as summaryMessageCount,
|
||||
(SELECT mc.message_id FROM message_context mc
|
||||
JOIN message first_msg ON first_msg.id = mc.message_id
|
||||
WHERE mc.segment_id = cs.id ORDER BY first_msg.ts ASC, first_msg.id ASC LIMIT 1) as firstMessageId
|
||||
@@ -473,6 +482,7 @@ export function getRecentChatSessions(db: DatabaseAdapter, limit: number): ChatS
|
||||
`SELECT
|
||||
id, start_ts as startTs, end_ts as endTs,
|
||||
message_count as messageCount, summary,
|
||||
summary_message_count as summaryMessageCount,
|
||||
(SELECT mc.message_id FROM message_context mc
|
||||
JOIN message first_msg ON first_msg.id = mc.message_id
|
||||
WHERE mc.segment_id = cs.id ORDER BY first_msg.ts ASC, first_msg.id ASC LIMIT 1) as firstMessageId
|
||||
@@ -500,6 +510,7 @@ export function getChatSessionList(db: DatabaseAdapter): ChatSessionItem[] {
|
||||
cs.end_ts as endTs,
|
||||
cs.message_count as messageCount,
|
||||
cs.summary,
|
||||
cs.summary_message_count as summaryMessageCount,
|
||||
(SELECT mc.message_id FROM message_context mc
|
||||
JOIN message first_msg ON first_msg.id = mc.message_id
|
||||
WHERE mc.segment_id = cs.id ORDER BY first_msg.ts ASC, first_msg.id ASC LIMIT 1) as firstMessageId
|
||||
@@ -518,22 +529,24 @@ export function getChatSessionList(db: DatabaseAdapter): ChatSessionItem[] {
|
||||
export function loadSegmentMessages(
|
||||
db: DatabaseAdapter,
|
||||
segmentId: number,
|
||||
limit: number = 500
|
||||
limit?: number
|
||||
): Array<{ senderName: string; content: string | null }> | null {
|
||||
try {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT
|
||||
const limitClause = limit === undefined ? '' : '\n LIMIT ?'
|
||||
const statement = db.prepare(
|
||||
`SELECT
|
||||
COALESCE(mb.group_nickname, mb.account_name, mb.platform_id) as senderName,
|
||||
m.content
|
||||
FROM message_context mc
|
||||
JOIN message m ON m.id = mc.message_id
|
||||
JOIN member mb ON mb.id = m.sender_id
|
||||
WHERE mc.segment_id = ?
|
||||
ORDER BY m.ts ASC
|
||||
LIMIT ?`
|
||||
)
|
||||
.all(segmentId, limit) as unknown as Array<{ senderName: string; content: string | null }>
|
||||
ORDER BY m.ts ASC, m.id ASC${limitClause}`
|
||||
)
|
||||
return statement.all(...(limit === undefined ? [segmentId] : [segmentId, limit])) as unknown as Array<{
|
||||
senderName: string
|
||||
content: string | null
|
||||
}>
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -544,10 +557,15 @@ export function loadSegmentMessages(
|
||||
*/
|
||||
export function getSegmentSummary(db: DatabaseAdapter, segmentId: number): string | null {
|
||||
try {
|
||||
const row = db.prepare('SELECT summary FROM segment WHERE id = ?').get(segmentId) as
|
||||
| { summary: string | null }
|
||||
const row = db
|
||||
.prepare(
|
||||
'SELECT summary, summary_message_count as summaryMessageCount, message_count as messageCount FROM segment WHERE id = ?'
|
||||
)
|
||||
.get(segmentId) as
|
||||
| { summary: string | null; summaryMessageCount: number | null; messageCount: number }
|
||||
| undefined
|
||||
return row?.summary ?? null
|
||||
if (!row?.summary || row.summaryMessageCount !== row.messageCount) return null
|
||||
return row.summary
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -557,7 +575,10 @@ export function getSegmentSummary(db: DatabaseAdapter, segmentId: number): strin
|
||||
* Save summary text for a chat session.
|
||||
*/
|
||||
export function saveSegmentSummary(db: DatabaseAdapter, segmentId: number, summary: string): void {
|
||||
db.prepare('UPDATE segment SET summary = ? WHERE id = ?').run(summary, segmentId)
|
||||
db.prepare('UPDATE segment SET summary = ?, summary_message_count = message_count WHERE id = ?').run(
|
||||
summary,
|
||||
segmentId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* 所有 CREATE TABLE / INDEX 语句的单一事实来源。
|
||||
* 新建数据库时使用完整 Schema,现有数据库通过迁移脚本演进。
|
||||
*
|
||||
* 当前 Schema 版本:9
|
||||
* 当前 Schema 版本:10
|
||||
*/
|
||||
|
||||
/** 当前 Schema 版本(最新迁移的版本号) */
|
||||
export const CURRENT_SCHEMA_VERSION = 9
|
||||
export const CURRENT_SCHEMA_VERSION = 10
|
||||
|
||||
/**
|
||||
* Table DDL only (no indexes). Used by bulk-import workflows that defer
|
||||
@@ -66,7 +66,8 @@ export const CHAT_DB_TABLES = `
|
||||
end_ts INTEGER NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
is_manual INTEGER DEFAULT 0,
|
||||
summary TEXT
|
||||
summary TEXT,
|
||||
summary_message_count INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_context (
|
||||
|
||||
@@ -244,6 +244,61 @@ test('open safely upgrades a v8 session that has no FTS table', () => {
|
||||
manager.closeAll()
|
||||
})
|
||||
|
||||
test('open adds summary coverage metadata without trusting legacy summaries', () => {
|
||||
const root = makeTempDir()
|
||||
const dbDir = path.join(root, 'data', 'databases')
|
||||
fs.mkdirSync(dbDir, { recursive: true })
|
||||
const dbPath = path.join(dbDir, 'v9-summary-coverage.db')
|
||||
|
||||
const rawDb = new Database(dbPath, { nativeBinding })
|
||||
rawDb.exec(`
|
||||
CREATE TABLE meta (
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
imported_at INTEGER NOT NULL,
|
||||
schema_version INTEGER DEFAULT 9
|
||||
);
|
||||
INSERT INTO meta (name, platform, type, imported_at, schema_version)
|
||||
VALUES ('V9 Summary', 'qq', 'group', 1000, 9);
|
||||
|
||||
CREATE TABLE member (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE TABLE message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_id INTEGER NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE segment (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
start_ts INTEGER NOT NULL,
|
||||
end_ts INTEGER NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
is_manual INTEGER DEFAULT 0,
|
||||
summary TEXT
|
||||
);
|
||||
INSERT INTO segment (start_ts, end_ts, message_count, summary)
|
||||
VALUES (1000, 1100, 2, 'legacy summary');
|
||||
`)
|
||||
rawDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
|
||||
const db = manager.open('v9-summary-coverage')
|
||||
assert.ok(db)
|
||||
|
||||
assert.deepEqual(db.prepare('SELECT schema_version FROM meta').get(), { schema_version: CURRENT_SCHEMA_VERSION })
|
||||
assert.deepEqual(db.prepare('SELECT summary, summary_message_count FROM segment').get(), {
|
||||
summary: 'legacy summary',
|
||||
summary_message_count: null,
|
||||
})
|
||||
|
||||
manager.closeAll()
|
||||
})
|
||||
|
||||
test('open migrates v7 databases to include analysis tool indexes', () => {
|
||||
const root = makeTempDir()
|
||||
const dbDir = path.join(root, 'data', 'databases')
|
||||
|
||||
@@ -54,6 +54,14 @@ export function getChatDbMigrations(): CoreMigration[] {
|
||||
return tableInfo.some((col) => col.name === columnName)
|
||||
}
|
||||
|
||||
const hasTable = (db: DatabaseAdapter, tableName: string): boolean => {
|
||||
return Boolean(
|
||||
db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
)
|
||||
}
|
||||
|
||||
const addColumnIfMissing = (db: DatabaseAdapter, tableName: string, columnName: string, definition: string): void => {
|
||||
if (!hasColumn(db, tableName, columnName)) {
|
||||
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`)
|
||||
@@ -372,5 +380,14 @@ export function getChatDbMigrations(): CoreMigration[] {
|
||||
db.exec('DROP TABLE IF EXISTS message_fts')
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 10,
|
||||
description: 'Track message coverage for generated segment summaries',
|
||||
up: (db: DatabaseAdapter) => {
|
||||
// Existing summaries may have been truncated or preserved after incremental imports.
|
||||
// Keep their text for users to review, but leave coverage unknown so the new runtime marks them stale.
|
||||
if (hasTable(db, 'segment')) addColumnIfMissing(db, 'segment', 'summary_message_count', 'INTEGER')
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* and summary generation loop — shared across CLI Web and Electron.
|
||||
*/
|
||||
|
||||
import { getSegmentSummary, saveSegmentSummary, getChatSessionList, getSegmentMessages } from '@openchatlab/core'
|
||||
import { getSegmentSummary, saveSegmentSummary, getChatSessionList, loadSegmentMessages } from '@openchatlab/core'
|
||||
import type { DatabaseAdapter } from '@openchatlab/core'
|
||||
import {
|
||||
generateSessionSummary,
|
||||
@@ -29,10 +29,8 @@ export interface SummaryServiceDeps {
|
||||
function buildSummaryDeps(db: DatabaseAdapter, llmConfig: LlmConfig, deps: SummaryServiceDeps): SummaryDeps {
|
||||
const piModel = deps.buildPiModel(llmConfig)
|
||||
return {
|
||||
loadMessages(segmentId, limit = 500) {
|
||||
const data = getSegmentMessages(db, segmentId, limit)
|
||||
if (!data) return null
|
||||
return data.messages.map((m) => ({ senderName: m.senderName, content: m.content }))
|
||||
loadMessages(segmentId, limit) {
|
||||
return loadSegmentMessages(db, segmentId, limit)
|
||||
},
|
||||
saveSummary(segmentId, summary) {
|
||||
saveSegmentSummary(db, segmentId, summary)
|
||||
@@ -109,10 +107,8 @@ export function checkCanGenerate(
|
||||
): Record<number, { canGenerate: boolean; reason?: string }> {
|
||||
const db = adapter.ensureReadonly(sessionId)
|
||||
const deps: Pick<SummaryDeps, 'loadMessages' | 't'> = {
|
||||
loadMessages(segmentId, limit = 500) {
|
||||
const data = getSegmentMessages(db, segmentId, limit)
|
||||
if (!data) return null
|
||||
return data.messages.map((m) => ({ senderName: m.senderName, content: m.content }))
|
||||
loadMessages(segmentId, limit) {
|
||||
return loadSegmentMessages(db, segmentId, limit)
|
||||
},
|
||||
t: (key: string) => key,
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ interface SessionItem {
|
||||
messageCount: number
|
||||
// 与 sessionApi 返回值保持一致,历史数据里 summary 可能不存在。
|
||||
summary?: string | null
|
||||
summaryMessageCount?: number | null
|
||||
}
|
||||
const sessions = ref<SessionItem[]>([])
|
||||
const isLoading = ref(false)
|
||||
@@ -122,10 +123,18 @@ const timeRange = computed(() => {
|
||||
const canGenerateMap = ref<Record<number, { canGenerate: boolean; reason?: string }>>({})
|
||||
const isChecking = ref(false)
|
||||
|
||||
function isSummaryStale(session: SessionItem): boolean {
|
||||
return Boolean(session.summary) && session.summaryMessageCount !== session.messageCount
|
||||
}
|
||||
|
||||
function hasFreshSummary(session: SessionItem): boolean {
|
||||
return Boolean(session.summary) && !isSummaryStale(session)
|
||||
}
|
||||
|
||||
// 待生成的会话(排除已有摘要的 + 消息太少的)
|
||||
const pendingSessions = computed(() => {
|
||||
return sessions.value.filter((s) => {
|
||||
if (s.summary) return false
|
||||
if (hasFreshSummary(s)) return false
|
||||
const checkResult = canGenerateMap.value[s.id]
|
||||
return checkResult?.canGenerate !== false
|
||||
})
|
||||
@@ -133,13 +142,17 @@ const pendingSessions = computed(() => {
|
||||
|
||||
// 已有摘要的会话数
|
||||
const existingSummaryCount = computed(() => {
|
||||
return sessions.value.filter((s) => s.summary).length
|
||||
return sessions.value.filter(hasFreshSummary).length
|
||||
})
|
||||
|
||||
const staleSummaryCount = computed(() => {
|
||||
return sessions.value.filter(isSummaryStale).length
|
||||
})
|
||||
|
||||
// 消息数量太少的会话数(无摘要但无法生成)
|
||||
const tooFewMessagesCount = computed(() => {
|
||||
return sessions.value.filter((s) => {
|
||||
if (s.summary) return false
|
||||
if (hasFreshSummary(s)) return false
|
||||
const checkResult = canGenerateMap.value[s.id]
|
||||
return checkResult?.canGenerate === false
|
||||
}).length
|
||||
@@ -199,7 +212,7 @@ async function fetchSessions() {
|
||||
|
||||
// 批量检查会话是否可以生成摘要
|
||||
async function checkCanGenerate() {
|
||||
const noSummaryIds = sessions.value.filter((s) => !s.summary).map((s) => s.id)
|
||||
const noSummaryIds = sessions.value.filter((s) => !hasFreshSummary(s)).map((s) => s.id)
|
||||
if (noSummaryIds.length === 0) return
|
||||
|
||||
isChecking.value = true
|
||||
@@ -291,6 +304,7 @@ async function startGenerate() {
|
||||
const idx = sessions.value.findIndex((s) => s.id === session.id)
|
||||
if (idx !== -1) {
|
||||
sessions.value[idx].summary = result.summary || ''
|
||||
sessions.value[idx].summaryMessageCount = sessions.value[idx].messageCount
|
||||
}
|
||||
} else if (result.error && isTooFewMessagesError(result.error)) {
|
||||
// 消息数量太少:标记为跳过
|
||||
@@ -441,7 +455,7 @@ function close() {
|
||||
<p>
|
||||
{{ t('records.batchSummary.found', '找到') }} {{ sessions.length }}
|
||||
{{ t('records.batchSummary.sessionsUnit', '个会话') }}
|
||||
<template v-if="existingSummaryCount > 0 || tooFewMessagesCount > 0">
|
||||
<template v-if="existingSummaryCount > 0 || staleSummaryCount > 0 || tooFewMessagesCount > 0">
|
||||
<span class="text-gray-500">
|
||||
(
|
||||
<template v-if="existingSummaryCount > 0">
|
||||
@@ -449,7 +463,15 @@ function close() {
|
||||
{{ existingSummaryCount }} {{ t('records.batchSummary.hasSummary', '个已有摘要') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="existingSummaryCount > 0 && tooFewMessagesCount > 0">,</template>
|
||||
<template v-if="existingSummaryCount > 0 && (staleSummaryCount > 0 || tooFewMessagesCount > 0)">
|
||||
,
|
||||
</template>
|
||||
<template v-if="staleSummaryCount > 0">
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
{{ staleSummaryCount }} {{ t('records.batchSummary.needsUpdate', '个待更新') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="staleSummaryCount > 0 && tooFewMessagesCount > 0">,</template>
|
||||
<template v-if="tooFewMessagesCount > 0">
|
||||
<span class="text-gray-400">
|
||||
{{ tooFewMessagesCount }} {{ t('records.batchSummary.tooFewMessages', '个消息太少') }}
|
||||
|
||||
@@ -190,6 +190,10 @@ function formatTime(ts: number): string {
|
||||
return date.toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function isSummaryStale(session: ChatSessionItem): boolean {
|
||||
return Boolean(session.summary) && session.summaryMessageCount !== session.messageCount
|
||||
}
|
||||
|
||||
// 获取日期键
|
||||
function getDateKey(ts: number): string {
|
||||
const date = new Date(ts * 1000)
|
||||
@@ -264,7 +268,11 @@ async function generateSummary(session: ChatSessionItem, event: Event) {
|
||||
if (result.success && result.summary) {
|
||||
const index = allSessions.value.findIndex((s) => s.id === session.id)
|
||||
if (index !== -1) {
|
||||
const updatedSession = { ...allSessions.value[index], summary: result.summary }
|
||||
const updatedSession = {
|
||||
...allSessions.value[index],
|
||||
summary: result.summary,
|
||||
summaryMessageCount: allSessions.value[index].messageCount,
|
||||
}
|
||||
allSessions.value[index] = updatedSession
|
||||
emit('summary-updated', updatedSession)
|
||||
}
|
||||
@@ -432,6 +440,12 @@ watch(
|
||||
<span class="ml-1">
|
||||
{{ (flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isSummaryStale((flatList[virtualItem.index] as { session: ChatSessionItem }).session)"
|
||||
class="ml-1 font-medium text-amber-500 dark:text-amber-400"
|
||||
>
|
||||
{{ t('records.timeline.summaryStale') }}
|
||||
</span>
|
||||
</span>
|
||||
<template #content>
|
||||
<div class="max-w-sm whitespace-pre-wrap text-sm leading-relaxed font-normal">
|
||||
@@ -445,7 +459,10 @@ watch(
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!(flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary"
|
||||
v-if="
|
||||
!(flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary ||
|
||||
isSummaryStale((flatList[virtualItem.index] as { session: ChatSessionItem }).session)
|
||||
"
|
||||
class="mt-0.5 flex min-w-0 items-center"
|
||||
>
|
||||
<!-- 无摘要且消息数>=3:显示生成按钮 -->
|
||||
@@ -462,7 +479,13 @@ watch(
|
||||
class="h-3 w-3 shrink-0 animate-spin"
|
||||
/>
|
||||
<UIcon v-else name="i-heroicons-sparkles" class="h-3 w-3 shrink-0" />
|
||||
<span class="truncate">{{ t('records.timeline.generateSummary') }}</span>
|
||||
<span class="truncate">
|
||||
{{
|
||||
isSummaryStale((flatList[virtualItem.index] as { session: ChatSessionItem }).session)
|
||||
? t('records.timeline.updateSummary')
|
||||
: t('records.timeline.generateSummary')
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- 消息数<3:显示提示 -->
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"timeline": "Summaries",
|
||||
"noSessions": "No summaries",
|
||||
"generateSummary": "Summarize",
|
||||
"updateSummary": "Update summary",
|
||||
"summaryStale": "· Update needed",
|
||||
"tooFewMessages": "Too few"
|
||||
},
|
||||
"summaryFailed": "Summary generation failed",
|
||||
@@ -54,6 +56,7 @@
|
||||
"custom": "Custom",
|
||||
"found": "Found",
|
||||
"hasSummary": "already summarized",
|
||||
"needsUpdate": "need updates",
|
||||
"tooFewMessages": "too few messages",
|
||||
"pending": "Pending:",
|
||||
"unit": "",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"timeline": "要約",
|
||||
"noSessions": "要約がありません",
|
||||
"generateSummary": "要約を作成",
|
||||
"updateSummary": "要約を更新",
|
||||
"summaryStale": "· 更新が必要",
|
||||
"tooFewMessages": "メッセージが少なすぎます"
|
||||
},
|
||||
"summaryFailed": "要約の生成に失敗しました",
|
||||
@@ -54,6 +56,7 @@
|
||||
"custom": "カスタム",
|
||||
"found": "見つかった",
|
||||
"hasSummary": "件は要約済み",
|
||||
"needsUpdate": "件は更新が必要",
|
||||
"tooFewMessages": "件はメッセージ不足",
|
||||
"pending": "未生成:",
|
||||
"unit": "件",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"timeline": "摘要",
|
||||
"noSessions": "暂无摘要",
|
||||
"generateSummary": "生成摘要",
|
||||
"updateSummary": "更新摘要",
|
||||
"summaryStale": "· 待更新",
|
||||
"tooFewMessages": "消息太少"
|
||||
},
|
||||
"summaryFailed": "摘要生成失败",
|
||||
@@ -54,6 +56,7 @@
|
||||
"custom": "自定义",
|
||||
"found": "找到",
|
||||
"hasSummary": "个已有摘要",
|
||||
"needsUpdate": "个待更新",
|
||||
"tooFewMessages": "个消息太少",
|
||||
"pending": "待生成:",
|
||||
"unit": "个",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"timeline": "摘要",
|
||||
"noSessions": "暫無摘要",
|
||||
"generateSummary": "產生摘要",
|
||||
"updateSummary": "更新摘要",
|
||||
"summaryStale": "· 待更新",
|
||||
"tooFewMessages": "訊息太少"
|
||||
},
|
||||
"summaryFailed": "摘要產生失敗",
|
||||
@@ -54,6 +56,7 @@
|
||||
"custom": "自訂",
|
||||
"found": "找到",
|
||||
"hasSummary": "個已有摘要",
|
||||
"needsUpdate": "個待更新",
|
||||
"tooFewMessages": "個訊息太少",
|
||||
"pending": "待產生:",
|
||||
"unit": "個",
|
||||
|
||||
@@ -88,7 +88,8 @@ export class FetchSessionIndexAdapter implements SessionIndexAdapter {
|
||||
async getSessions(sessionId: string): Promise<ChatSessionItem[]> {
|
||||
return getDataAdapter().pluginQuery<ChatSessionItem>(
|
||||
sessionId,
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary,
|
||||
summary_message_count as summaryMessageCount, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
FROM segment ORDER BY start_ts ASC`,
|
||||
[]
|
||||
)
|
||||
@@ -97,7 +98,8 @@ export class FetchSessionIndexAdapter implements SessionIndexAdapter {
|
||||
async getByTimeRange(sessionId: string, startTs: number, endTs: number): Promise<ChatSessionItem[]> {
|
||||
return getDataAdapter().pluginQuery<ChatSessionItem>(
|
||||
sessionId,
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary,
|
||||
summary_message_count as summaryMessageCount, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
FROM segment WHERE start_ts >= ? AND end_ts <= ? ORDER BY start_ts ASC`,
|
||||
[startTs, endTs]
|
||||
)
|
||||
@@ -106,7 +108,8 @@ export class FetchSessionIndexAdapter implements SessionIndexAdapter {
|
||||
async getRecent(sessionId: string, limit: number): Promise<ChatSessionItem[]> {
|
||||
return getDataAdapter().pluginQuery<ChatSessionItem>(
|
||||
sessionId,
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
`SELECT id, start_ts as startTs, end_ts as endTs, message_count as messageCount, summary,
|
||||
summary_message_count as summaryMessageCount, ${FIRST_MESSAGE_ID_SUBQUERY}
|
||||
FROM segment ORDER BY start_ts DESC LIMIT ?`,
|
||||
[limit]
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ChatSessionItem {
|
||||
messageCount: number
|
||||
firstMessageId: number
|
||||
summary?: string | null
|
||||
summaryMessageCount?: number | null
|
||||
}
|
||||
|
||||
export interface SummaryResult {
|
||||
|
||||
Reference in new issue
Block a user