diff --git a/packages/core/src/query/__tests__/global-insight.test.ts b/packages/core/src/query/__tests__/global-insight.test.ts index 406808fa..57e62721 100644 --- a/packages/core/src/query/__tests__/global-insight.test.ts +++ b/packages/core/src/query/__tests__/global-insight.test.ts @@ -204,6 +204,25 @@ test('counts an active private counterpart on each day and scopes name-match ide db.close() }) +test('ignores legacy system messages when counting private direct-contact days', () => { + const db = createSession({ id: 'private-system', type: ChatType.PRIVATE }) + db.raw.prepare('INSERT INTO member VALUES (?, ?, ?, ?, ?, ?)').run(3, 'system', '系统消息', null, '[]', null) + addMessage(db, { + id: 1, + senderId: 3, + ts: localTs(2024, 2, 1), + content: 'notification', + platformMessageId: 'system-1', + }) + + const facts = getAnnualSummarySessionFacts(db, 'private-system', range2024) + + assert.equal(facts.kind, 'analyzed') + if (facts.kind !== 'analyzed') return + assert.deepEqual(facts.directContactKeysByDay, {}) + db.close() +}) + test('reports missing and unresolved owners instead of guessing', () => { const missing = createSession({ id: 'missing', type: ChatType.PRIVATE, ownerId: null }) const unresolved = createSession({ id: 'unresolved', type: ChatType.GROUP, ownerId: 'unknown' }) diff --git a/packages/core/src/query/global-insight.ts b/packages/core/src/query/global-insight.ts index 42a2a6e1..e1672a2c 100644 --- a/packages/core/src/query/global-insight.ts +++ b/packages/core/src/query/global-insight.ts @@ -133,7 +133,7 @@ export function getAnnualSummarySessionFacts( ownerMessagesByDay, directContactKeysByDay: meta.type === ChatType.PRIVATE && privateContact - ? getPrivateDirectContactsByDay(db, sessionId, meta.platform, privateContact, range) + ? getPrivateDirectContactsByDay(db, sessionId, meta.platform, owner.id, privateContact, range) : getGroupDirectContactsByDay(db, sessionId, meta.platform, owner.id, contacts, range), messageTypeCounts, textLengthCounts, @@ -237,6 +237,7 @@ function getPrivateDirectContactsByDay( db: DatabaseAdapter, sessionId: string, platform: ChatPlatform, + ownerId: number, contact: ContactMemberRef, range: AnnualSummaryRange ): Record { @@ -244,10 +245,11 @@ function getPrivateDirectContactsByDay( .prepare( `SELECT DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date FROM message msg - WHERE msg.ts >= ? AND msg.ts <= ? AND msg.type NOT IN (${SYSTEM_MESSAGE_TYPES.join(', ')}) + WHERE msg.sender_id IN (?, ?) AND msg.ts >= ? AND msg.ts <= ? + AND msg.type NOT IN (${SYSTEM_MESSAGE_TYPES.join(', ')}) ORDER BY date` ) - .all(range.startTs, range.endTs) as Array<{ date: string }> + .all(ownerId, contact.id, range.startTs, range.endTs) as Array<{ date: string }> const key = buildContactKey( platform, contact.platformId, diff --git a/packages/node-runtime/src/services/global-insight/service.test.ts b/packages/node-runtime/src/services/global-insight/service.test.ts index 1b12ee84..4a20daf9 100644 --- a/packages/node-runtime/src/services/global-insight/service.test.ts +++ b/packages/node-runtime/src/services/global-insight/service.test.ts @@ -124,6 +124,29 @@ test('preserves failed state until explicit recompute', async (t) => { assert.equal(env.service.getAnnualSummary({ mode: 'year', year: 2026 }).cache.status, 'fresh') }) +test('reports snapshot persistence failures without restarting the task', async (t) => { + let calls = 0 + const env = createEnv(async ({ signature, range }) => { + calls++ + return makeSnapshot(range, signature) + }) + t.after(async () => { + await env.service.close() + fs.rmSync(env.dir, { recursive: true, force: true }) + }) + const insightDir = path.join(env.dir, 'insight') + fs.mkdirSync(insightDir) + fs.writeFileSync(path.join(insightDir, 'annual-summary'), 'not a directory') + + env.service.getAnnualSummary({ mode: 'year', year: 2026 }) + await flushTasks() + const failed = env.service.getAnnualSummary({ mode: 'year', year: 2026 }) + + assert.equal(failed.task.status, 'failed') + assert.ok(failed.task.lastError) + assert.equal(calls, 1) +}) + test('discards a worker result when the DB signature changes during compute', async (t) => { let finish: (() => void) | undefined const env = createEnv( diff --git a/packages/node-runtime/src/services/global-insight/service.ts b/packages/node-runtime/src/services/global-insight/service.ts index 039653f2..a78cd760 100644 --- a/packages/node-runtime/src/services/global-insight/service.ts +++ b/packages/node-runtime/src/services/global-insight/service.ts @@ -150,16 +150,17 @@ class DefaultGlobalInsightService implements GlobalInsightService { private handleTaskSuccess(id: string, inputSignature: string, snapshot: AnnualSummarySnapshot): void { if (this.inFlight?.id !== id) return - this.inFlight = null const latestSignature = buildAnnualSummarySignature(this.deps.adapter, snapshot.range) const finishedAt = this.now() if (inputSignature !== latestSignature || snapshot.signature !== latestSignature) { + this.inFlight = null this.task = { ...this.task, status: 'superseded', finishedAt, currentSessionId: undefined } appLogger.info('global-insight', 'annual summary worker result discarded because signature changed') return } try { writeAnnualSummarySnapshot(this.snapshotDir, snapshot) + this.inFlight = null this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot) this.task = { ...this.task, diff --git a/src/components/common/Settings/BatchManageTab.vue b/src/components/common/Settings/BatchManageTab.vue index bae274f3..d69bf6f3 100644 --- a/src/components/common/Settings/BatchManageTab.vue +++ b/src/components/common/Settings/BatchManageTab.vue @@ -31,9 +31,7 @@ const { sessions } = storeToRefs(sessionStore) // 搜索关键词 const searchQuery = ref('') -const ownerIssueCount = computed( - () => sessions.value.filter((session) => session.ownerStatus !== 'resolved').length -) +const ownerIssueCount = computed(() => sessions.value.filter((session) => session.ownerStatus !== 'resolved').length) // 过滤后的会话列表 const filteredSessions = computed(() => { @@ -798,12 +796,18 @@ onMounted(() => {