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))
)
}
+10 -5
View File
@@ -4,6 +4,8 @@ import { GlobalSettingsService } from '../services/global-settings'
import { BookRegistryService } from '../services/book-registry'
import { SnapshotService } from '../services/snapshot.service'
import { ShortcutManager } from '../shortcuts/manager'
import { WritingLogRepository } from '../db/repositories/writing-log.repo'
import { WritingLogService } from '../services/writing-log.service'
import { registerSettingsHandlers } from './handlers/settings.handler'
import { registerBookHandlers } from './handlers/book.handler'
import { registerChapterHandlers } from './handlers/chapter.handler'
@@ -20,6 +22,7 @@ import { registerAutoHandlers } from './handlers/auto.handler'
import { registerWizardHandlers } from './handlers/wizard.handler'
import { registerKnowledgeHandlers } from './handlers/knowledge.handler'
import { registerCockpitHandlers } from './handlers/cockpit.handler'
import { registerWritingHandlers } from './handlers/writing.handler'
import { registerBridgeHandlers } from './handlers/bridge.handler'
import { AiClientService } from '../services/ai-client.service'
import { NetworkMonitorService } from '../services/network-monitor.service'
@@ -33,13 +36,14 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
const settings = new GlobalSettingsService(userData)
const books = new BookRegistryService(userData)
const writingLogs = new WritingLogService(new WritingLogRepository(userData), settings)
shortcutManager = new ShortcutManager(settings)
snapshotService = new SnapshotService(() => settings.get())
networkMonitor = new NetworkMonitorService()
registerSettingsHandlers(settings)
registerBookHandlers(books)
registerChapterHandlers(books)
registerChapterHandlers(books, writingLogs)
registerShortcutHandlers(shortcutManager)
registerOutlineHandlers(books)
registerSettingHandlers(books)
@@ -49,11 +53,12 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
registerSearchHandlers(books, settings)
const aiClient = new AiClientService(() => settings.get().aiConfig)
registerAiHandlers(books, settings, aiClient)
registerInteractiveHandlers(books, settings, aiClient)
registerAutoHandlers(books, settings, aiClient)
registerInteractiveHandlers(books, settings, aiClient, writingLogs)
registerAutoHandlers(books, settings, aiClient, writingLogs)
registerWizardHandlers(books, settings, aiClient)
registerKnowledgeHandlers(books, settings)
registerCockpitHandlers(books, settings)
registerKnowledgeHandlers(books, settings, aiClient)
registerCockpitHandlers(books, settings, writingLogs)
registerWritingHandlers(writingLogs)
registerBridgeHandlers(books, aiClient)
return { settings, books, shortcuts: shortcutManager }