feat: ship v0.6.0 with cockpit, knowledge base, and chapter bridge

Deliver P5 writer workflow: writing cockpit, manual knowledge CRUD with review, chapter bridge with optional AI, publish status, and stock buffer reminders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 23:03:11 +08:00
parent 78f046890d
commit b33d2e7b34
45 changed files with 2389 additions and 29 deletions
+35
View File
@@ -0,0 +1,35 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import { BookRegistryService } from '../../services/book-registry'
import { ChapterBridgeService } from '../../services/chapter-bridge.service'
import { AiClientService } from '../../services/ai-client.service'
import { wrap } from '../result'
export function registerBridgeHandlers(
registry: BookRegistryService,
aiClient: AiClientService
): void {
ipcMain.handle(
IPC.BRIDGE_GET,
(
_e,
{ bookId, chapterId, withAi }: { bookId: string; chapterId: string; withAi?: boolean }
) =>
wrap(() =>
new ChapterBridgeService(registry.getDb(bookId), aiClient).getBridge(
chapterId,
withAi ?? false
)
)
)
ipcMain.handle(IPC.BRIDGE_DISMISS, (_e, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
wrap(() => new ChapterBridgeService(registry.getDb(bookId)).dismiss(chapterId))
)
ipcMain.handle(
IPC.BRIDGE_SHOULD_PROMPT,
(_e, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
wrap(() => new ChapterBridgeService(registry.getDb(bookId)).shouldPrompt(chapterId))
)
}
+9 -1
View File
@@ -1,6 +1,6 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { ChapterStatus } from '../../../shared/types'
import type { ChapterStatus, PublishStatus } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { ftsSync } from '../../services/fts-sync.service'
import { wrap } from '../result'
@@ -124,4 +124,12 @@ export function registerChapterHandlers(registry: BookRegistryService): void {
) =>
wrap(() => registry.getChapterRepo(bookId).moveToVolume(chapterId, targetVolumeId, targetIndex))
)
ipcMain.handle(
IPC.CHAPTER_SET_PUBLISH_STATUS,
(
_event,
{ bookId, chapterId, publishStatus }: { bookId: string; chapterId: string; publishStatus: PublishStatus }
) => wrap(() => registry.getChapterRepo(bookId).setPublishStatus(chapterId, publishStatus))
)
}
+27
View File
@@ -0,0 +1,27 @@
import { ipcMain } from 'electron'
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 { wrap } from '../result'
export function registerCockpitHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService
): void {
ipcMain.handle(
IPC.COCKPIT_SUMMARY,
(_e, { bookId, volumeId }: { bookId: string; volumeId?: string }) =>
wrap(() => new CockpitService(registry.getDb(bookId), settings).getSummary(volumeId))
)
ipcMain.handle(IPC.COCKPIT_MARK_SEEN, (_e, { bookId }: { bookId: string }) =>
wrap(() => {
new CockpitService(registry.getDb(bookId), settings).markSeen()
})
)
ipcMain.handle(IPC.COCKPIT_SHOULD_SHOW, (_e, { bookId }: { bookId: string }) =>
wrap(() => new CockpitService(registry.getDb(bookId), settings).shouldShowOnOpen())
)
}
@@ -0,0 +1,68 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { CreateKnowledgeInput, KnowledgeStatus, KnowledgeType } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { KnowledgeService } from '../../services/knowledge.service'
import { wrap } from '../result'
export function registerKnowledgeHandlers(registry: BookRegistryService): void {
ipcMain.handle(
IPC.KNOWLEDGE_LIST,
(
_e,
{
bookId,
filter
}: {
bookId: string
filter?: { status?: KnowledgeStatus; type?: KnowledgeType; forgottenOnly?: boolean }
}
) => wrap(() => new KnowledgeService(registry.getDb(bookId)).list(filter))
)
ipcMain.handle(
IPC.KNOWLEDGE_CREATE,
(_e, { bookId, input }: { bookId: string; input: CreateKnowledgeInput }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).create(input))
)
ipcMain.handle(
IPC.KNOWLEDGE_UPDATE,
(
_e,
{
bookId,
id,
patch
}: { bookId: string; id: string; patch: Partial<CreateKnowledgeInput & { status: KnowledgeStatus }> }
) => wrap(() => new KnowledgeService(registry.getDb(bookId)).update(id, patch))
)
ipcMain.handle(IPC.KNOWLEDGE_DELETE, (_e, { bookId, id }: { bookId: string; id: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).delete(id))
)
ipcMain.handle(IPC.KNOWLEDGE_APPROVE, (_e, { bookId, id }: { bookId: string; id: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).approve(id))
)
ipcMain.handle(IPC.KNOWLEDGE_REJECT, (_e, { bookId, id }: { bookId: string; id: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).reject(id))
)
ipcMain.handle(
IPC.KNOWLEDGE_BATCH_APPROVE,
(_e, { bookId, ids }: { bookId: string; ids: string[] }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).batchApprove(ids))
)
ipcMain.handle(
IPC.KNOWLEDGE_CREATE_FROM_BOOKMARK,
(_e, { bookId, bookmarkId }: { bookId: string; bookmarkId: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).createFromBookmark(bookmarkId))
)
ipcMain.handle(IPC.KNOWLEDGE_STATS, (_e, { bookId }: { bookId: string }) =>
wrap(() => new KnowledgeService(registry.getDb(bookId)).getForeshadowStats())
)
}