feat: ship v0.8.0 with writing logs and AI knowledge extraction

Deliver P5.2 writing day logs, cockpit heatmap, streak tracking, and chapter knowledge auto-extraction with merge review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 18:26:29 +08:00
parent f331ddae86
commit a39e06ff07
51 changed files with 1436 additions and 89 deletions
+13 -3
View File
@@ -7,6 +7,9 @@ import { AiClientService } from '../../services/ai-client.service'
import { buildFlowContextPayload, readFlowSystemPrompt } from '../../services/flow-context.util'
import { checkInteractiveGate } from '../../services/interactive-gate.service'
import { AutoWritingService } from '../../services/auto-writing.service'
import { scheduleChapterExtraction } from '../../services/knowledge-extraction-runner'
import { trackerFor } from '../../services/chapter-writing-tracker.service'
import type { WritingLogService } from '../../services/writing-log.service'
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
import { wrap } from '../result'
@@ -33,9 +36,9 @@ function readContextText(registry: BookRegistryService, bookId: string, flowId:
export function registerAutoHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService,
aiClient: AiClientService
aiClient: AiClientService,
writingLogs: WritingLogService
): void {
void settings
ipcMain.handle(IPC.AUTO_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
wrap(() => checkInteractiveGate(registry.getDb(bookId)))
@@ -203,7 +206,14 @@ export function registerAutoHandlers(
(
_e,
{ bookId, flowId, volumeId, title }: { bookId: string; flowId: string; volumeId: string; title?: string }
) => wrap(() => serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title))
) =>
wrap(() => {
const result = serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title)
const ch = registry.getChapterRepo(bookId).get(result.chapterId)!
trackerFor(registry, bookId, writingLogs).supplementOnFinish(result.chapterId, ch.wordCount)
scheduleChapterExtraction(registry, bookId, result.chapterId, aiClient, settings)
return result
})
)
ipcMain.handle(IPC.AUTO_ABORT, (_e, { flowId }: { flowId: string }) =>
+7 -1
View File
@@ -2,10 +2,15 @@ import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { ChapterStatus, PublishStatus } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { trackerFor } from '../../services/chapter-writing-tracker.service'
import type { WritingLogService } from '../../services/writing-log.service'
import { ftsSync } from '../../services/fts-sync.service'
import { wrap } from '../result'
export function registerChapterHandlers(registry: BookRegistryService): void {
export function registerChapterHandlers(
registry: BookRegistryService,
writingLogs: WritingLogService
): void {
ipcMain.handle(
IPC.VOLUME_CREATE,
(_event, { bookId, name }: { bookId: string; name: string }) =>
@@ -91,6 +96,7 @@ export function registerChapterHandlers(registry: BookRegistryService): void {
chapter.title,
chapter.content
)
trackerFor(registry, bookId, writingLogs).recordDelta(chapterId, chapter.wordCount)
registry.updateMeta(bookId, { lastChapterId: chapterId })
return chapter
})
+6 -2
View File
@@ -3,16 +3,20 @@ import { IPC } from '../../../shared/ipc-channels'
import { BookRegistryService } from '../../services/book-registry'
import { CockpitService } from '../../services/cockpit.service'
import type { GlobalSettingsService } from '../../services/global-settings'
import type { WritingLogService } from '../../services/writing-log.service'
import { wrap } from '../result'
export function registerCockpitHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService
settings: GlobalSettingsService,
writingLogs: WritingLogService
): void {
ipcMain.handle(
IPC.COCKPIT_SUMMARY,
(_e, { bookId, volumeId }: { bookId: string; volumeId?: string }) =>
wrap(() => new CockpitService(registry.getDb(bookId), settings).getSummary(volumeId))
wrap(() =>
new CockpitService(registry.getDb(bookId), settings, writingLogs).getSummary(volumeId, bookId)
)
)
ipcMain.handle(IPC.COCKPIT_MARK_SEEN, (_e, { bookId }: { bookId: string }) =>
+12 -2
View File
@@ -7,6 +7,9 @@ import { AiClientService } from '../../services/ai-client.service'
import { buildFlowContextPayload, readFlowSystemPrompt } from '../../services/flow-context.util'
import { checkInteractiveGate } from '../../services/interactive-gate.service'
import { InteractiveWritingService } from '../../services/interactive-writing.service'
import { scheduleChapterExtraction } from '../../services/knowledge-extraction-runner'
import { trackerFor } from '../../services/chapter-writing-tracker.service'
import type { WritingLogService } from '../../services/writing-log.service'
import { wrap } from '../result'
const activeInteractive = new Map<string, AbortController>()
@@ -24,7 +27,8 @@ function enrich(registry: BookRegistryService, bookId: string, aiClient: AiClien
export function registerInteractiveHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService,
aiClient: AiClientService
aiClient: AiClientService,
writingLogs: WritingLogService
): void {
ipcMain.handle(IPC.INTERACTIVE_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
wrap(() => checkInteractiveGate(registry.getDb(bookId)))
@@ -194,7 +198,13 @@ export function registerInteractiveHandlers(
title
}: { bookId: string; flowId: string; volumeId: string; title?: string }
) =>
wrap(() => serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title))
wrap(() => {
const result = serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title)
const ch = registry.getChapterRepo(bookId).get(result.chapterId)!
trackerFor(registry, bookId, writingLogs).supplementOnFinish(result.chapterId, ch.wordCount)
scheduleChapterExtraction(registry, bookId, result.chapterId, aiClient, settings)
return result
})
)
ipcMain.handle(IPC.INTERACTIVE_ABORT, (_e, { flowId }: { flowId: string }) =>
+41 -3
View File
@@ -1,15 +1,24 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { CreateKnowledgeInput, KnowledgeStatus, KnowledgeType, KnowledgeSuggestOptions } from '../../../shared/types'
import type {
CreateKnowledgeInput,
KnowledgeStatus,
KnowledgeType,
KnowledgeSuggestOptions
} from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { GlobalSettingsService } from '../../services/global-settings'
import type { AiClientService } from '../../services/ai-client.service'
import type { GlobalSettingsService } from '../../services/global-settings'
import { KnowledgeRepository } from '../../db/repositories/knowledge.repo'
import { KnowledgeExtractionService } from '../../services/knowledge-extraction.service'
import { KnowledgeRetrievalService } from '../../services/knowledge-retrieval.service'
import { KnowledgeService } from '../../services/knowledge.service'
import { wrap } from '../result'
export function registerKnowledgeHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService
settings: GlobalSettingsService,
aiClient: AiClientService
): void {
ipcMain.handle(
IPC.KNOWLEDGE_LIST,
@@ -82,4 +91,33 @@ export function registerKnowledgeHandlers(
})
})
)
ipcMain.handle(
IPC.KNOWLEDGE_EXTRACT_CHAPTER,
(_e, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
wrap(() =>
new KnowledgeExtractionService(registry.getDb(bookId), aiClient).extractForChapter(chapterId)
)
)
ipcMain.handle(
IPC.KNOWLEDGE_APPROVE_MERGE,
(_e, { bookId, pendingId }: { bookId: string; pendingId: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).approveMerge(pendingId))
)
ipcMain.handle(
IPC.KNOWLEDGE_SAVE_MERGE_AS_NEW,
(_e, { bookId, pendingId }: { bookId: string; pendingId: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).saveMergeAsNew(pendingId))
)
ipcMain.handle(
IPC.KNOWLEDGE_BATCH_APPROVE_HIGH_CONFIDENCE,
(_e, { bookId, threshold }: { bookId: string; threshold?: number }) =>
wrap(() => {
const t = threshold ?? settings.get().knowledgeExtractConfidenceThreshold ?? 0.8
return new KnowledgeService(registry.getDb(bookId)).batchApproveHighConfidence(t)
})
)
}
+10
View File
@@ -0,0 +1,10 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { WritingLogService } from '../../services/writing-log.service'
import { wrap } from '../result'
export function registerWritingHandlers(writingLogs: WritingLogService): void {
ipcMain.handle(IPC.WRITING_GET_STATS, (_e, { bookId }: { bookId: string }) =>
wrap(() => writingLogs.getStats(bookId))
)
}