feat: ship v0.5.0 with auto and wizard writing modes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 20:57:31 +08:00
parent 86b66a311a
commit 6a949b54ba
38 changed files with 2581 additions and 184 deletions
+207
View File
@@ -0,0 +1,207 @@
import { BrowserWindow, ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { AutoWritingConfig } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { GlobalSettingsService } from '../../services/global-settings'
import { AiClientService } from '../../services/ai-client.service'
import { checkInteractiveGate } from '../../services/interactive-gate.service'
import { AutoWritingService } from '../../services/auto-writing.service'
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
import { wrap } from '../result'
const activeAuto = new Map<string, AbortController>()
function serviceFor(registry: BookRegistryService, bookId: string, aiClient: AiClientService) {
return new AutoWritingService(registry.getDb(bookId), aiClient)
}
function enrich(registry: BookRegistryService, bookId: string, aiClient: AiClientService, flowId: string) {
const svc = serviceFor(registry, bookId, aiClient)
const flow = new InteractiveRepository(registry.getDb(bookId)).get(flowId)
return flow ? svc.enrichFlow(flow) : null
}
function readContextText(registry: BookRegistryService, bookId: string, flowId: string): string {
const row = registry
.getDb(bookId)
.prepare('SELECT context_json FROM interactive_flows WHERE id = ?')
.get(flowId) as { context_json: string } | undefined
try {
const parsed = JSON.parse(row?.context_json ?? '{}') as { contextSummary?: string }
return parsed.contextSummary ?? '(无上下文摘要)'
} catch {
return '(无上下文摘要)'
}
}
export function registerAutoHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService,
aiClient: AiClientService
): void {
void settings
ipcMain.handle(IPC.AUTO_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
wrap(() => checkInteractiveGate(registry.getDb(bookId)))
)
ipcMain.handle(
IPC.AUTO_GET_FLOW,
(_e, { bookId, sessionId }: { bookId: string; sessionId: string }) =>
wrap(() => {
const flow = serviceFor(registry, bookId, aiClient).getFlow(sessionId)
return flow ? serviceFor(registry, bookId, aiClient).enrichFlow(flow) : null
})
)
ipcMain.handle(
IPC.AUTO_START,
(_e, { bookId, sessionId }: { bookId: string; sessionId: string }) =>
wrap(() => {
const flow = serviceFor(registry, bookId, aiClient).start(sessionId)
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
})
)
ipcMain.handle(
IPC.AUTO_SET_GOAL,
(
_e,
{ bookId, flowId, config }: { bookId: string; flowId: string; config: Partial<AutoWritingConfig> }
) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
const flow = svc.setGoal(flowId, config)
return svc.enrichFlow(flow)
})
)
ipcMain.handle(
IPC.AUTO_PLAN_SCENES,
(_e, { bookId, flowId, contextSummary }: { bookId: string; flowId: string; contextSummary?: string }) =>
wrap(async () => {
const svc = serviceFor(registry, bookId, aiClient)
if (contextSummary) svc.setContextSummary(flowId, contextSummary)
const text = contextSummary ?? readContextText(registry, bookId, flowId)
await svc.planScenes(flowId, text)
return enrich(registry, bookId, aiClient, flowId)
})
)
ipcMain.handle(
IPC.AUTO_GENERATE,
(event, { bookId, flowId, contextSummary }: { bookId: string; flowId: string; contextSummary?: string }) =>
wrap(async () => {
const controller = new AbortController()
activeAuto.set(flowId, controller)
const win = BrowserWindow.fromWebContents(event.sender)
const svc = serviceFor(registry, bookId, aiClient)
const text = contextSummary ?? readContextText(registry, bookId, flowId)
try {
const flow = await svc.generateNext(
flowId,
text,
(delta, done) => {
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
flowId,
flowMode: 'auto',
phase: 'scene',
delta,
done
})
},
controller.signal
)
return svc.enrichFlow(flow)
} finally {
activeAuto.delete(flowId)
}
})
)
ipcMain.handle(
IPC.AUTO_RESOLVE_NAMING,
(
event,
{
bookId,
flowId,
chosenName,
contextSummary
}: { bookId: string; flowId: string; chosenName: string; contextSummary?: string }
) =>
wrap(async () => {
const controller = new AbortController()
activeAuto.set(flowId, controller)
const win = BrowserWindow.fromWebContents(event.sender)
const svc = serviceFor(registry, bookId, aiClient)
const text = contextSummary ?? readContextText(registry, bookId, flowId)
try {
const flow = await svc.resolveNaming(
flowId,
chosenName,
text,
(delta, done) => {
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
flowId,
flowMode: 'auto',
phase: 'naming_resume',
delta,
done
})
},
controller.signal
)
return svc.enrichFlow(flow)
} finally {
activeAuto.delete(flowId)
}
})
)
ipcMain.handle(IPC.AUTO_PAUSE, (_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(() => {
activeAuto.get(flowId)?.abort()
activeAuto.delete(flowId)
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.pause(flowId))
})
)
ipcMain.handle(IPC.AUTO_RESUME, (_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.resume(flowId))
})
)
ipcMain.handle(
IPC.AUTO_HANDOFF_INTERACTIVE,
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.handoffToInteractive(flowId))
})
)
ipcMain.handle(
IPC.AUTO_FINISH_CHAPTER,
(
_e,
{ bookId, flowId, volumeId, title }: { bookId: string; flowId: string; volumeId: string; title?: string }
) => wrap(() => serviceFor(registry, bookId, aiClient).finishChapter(flowId, volumeId, title))
)
ipcMain.handle(IPC.AUTO_ABORT, (_e, { flowId }: { flowId: string }) =>
wrap(() => {
activeAuto.get(flowId)?.abort()
activeAuto.delete(flowId)
})
)
ipcMain.handle(
IPC.AUTO_GET_SCENE_DRAFT,
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(() => serviceFor(registry, bookId, aiClient).getSceneDraft(flowId))
)
}
+170
View File
@@ -0,0 +1,170 @@
import { BrowserWindow, ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { AiContextBinding, AutoRhythm, AutoWritingConfig, WizardWritingConfig } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { GlobalSettingsService } from '../../services/global-settings'
import { AiClientService } from '../../services/ai-client.service'
import { aiContextBuilder } from '../../services/ai-context-builder.service'
import { WizardWritingService } from '../../services/wizard-writing.service'
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
import { wrap } from '../result'
const activeWizard = new Map<string, AbortController>()
function serviceFor(registry: BookRegistryService, bookId: string, aiClient: AiClientService) {
return new WizardWritingService(registry.getDb(bookId), aiClient)
}
function readContextText(registry: BookRegistryService, bookId: string, flowId: string): string {
const row = registry
.getDb(bookId)
.prepare('SELECT context_json FROM interactive_flows WHERE id = ?')
.get(flowId) as { context_json: string } | undefined
try {
const parsed = JSON.parse(row?.context_json ?? '{}') as { contextSummary?: string }
return parsed.contextSummary ?? '(无上下文摘要)'
} catch {
return '(无上下文摘要)'
}
}
export function registerWizardHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService,
aiClient: AiClientService
): void {
ipcMain.handle(
IPC.WIZARD_GET_FLOW,
(_e, { bookId, sessionId }: { bookId: string; sessionId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
const flow = svc.getFlow(sessionId)
return flow ? svc.enrichFlow(flow) : null
})
)
ipcMain.handle(
IPC.WIZARD_START,
(_e, { bookId, sessionId }: { bookId: string; sessionId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
const flow = svc.start(sessionId)
return svc.enrichFlow(flow)
})
)
ipcMain.handle(
IPC.WIZARD_SET_GOAL,
(
_e,
{ bookId, flowId, config }: { bookId: string; flowId: string; config: Partial<AutoWritingConfig> }
) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.setGoal(flowId, config))
})
)
ipcMain.handle(
IPC.WIZARD_SET_RHYTHM,
(
_e,
{
bookId,
flowId,
rhythm,
styleNote
}: { bookId: string; flowId: string; rhythm: AutoRhythm; styleNote?: string }
) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.setRhythm(flowId, rhythm, styleNote))
})
)
ipcMain.handle(
IPC.WIZARD_SET_POV,
(_e, { bookId, flowId, povSettingId }: { bookId: string; flowId: string; povSettingId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.setPov(flowId, povSettingId))
})
)
ipcMain.handle(
IPC.WIZARD_CONFIRM_CONTEXT,
(
_e,
{ bookId, flowId, binding }: { bookId: string; flowId: string; binding: AiContextBinding }
) =>
wrap(() => {
const built = aiContextBuilder.build(binding, registry, bookId, settings.get().penName)
const summary = built.preview.map((p) => p.title).join('、') || '(空)'
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.confirmContext(flowId, binding, summary))
})
)
ipcMain.handle(
IPC.WIZARD_BUILD_PREVIEW,
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(async () => {
const svc = serviceFor(registry, bookId, aiClient)
const config = new InteractiveRepository(registry.getDb(bookId)).getModeConfig<WizardWritingConfig>(
flowId
)
const text = readContextText(registry, bookId, flowId)
const povName = svc.getPovName(config.povSettingId)
const preview = await svc.buildPreview(flowId, text, povName)
const flow = new InteractiveRepository(registry.getDb(bookId)).get(flowId)!
return { preview, flow: svc.enrichFlow(flow) }
})
)
ipcMain.handle(
IPC.WIZARD_START_GENERATE,
(event, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(async () => {
const controller = new AbortController()
activeWizard.set(flowId, controller)
const win = BrowserWindow.fromWebContents(event.sender)
const svc = serviceFor(registry, bookId, aiClient)
const text = readContextText(registry, bookId, flowId)
try {
const flow = await svc.startGenerate(
flowId,
text,
(delta, done) => {
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
flowId,
flowMode: 'wizard',
phase: 'scene',
delta,
done
})
},
controller.signal
)
return svc.enrichFlow(flow)
} finally {
activeWizard.delete(flowId)
}
})
)
ipcMain.handle(
IPC.WIZARD_HANDOFF_INTERACTIVE,
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
wrap(() => {
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.handoffToInteractive(flowId))
})
)
ipcMain.handle(IPC.WIZARD_ABORT, (_e, { flowId }: { flowId: string }) =>
wrap(() => {
activeWizard.get(flowId)?.abort()
activeWizard.delete(flowId)
})
)
}
+7 -2
View File
@@ -16,6 +16,8 @@ import { registerBookmarkHandlers } from './handlers/bookmark.handler'
import { registerSearchHandlers } from './handlers/search.handler'
import { registerAiHandlers } from './handlers/ai.handler'
import { registerInteractiveHandlers } from './handlers/interactive.handler'
import { registerAutoHandlers } from './handlers/auto.handler'
import { registerWizardHandlers } from './handlers/wizard.handler'
import { AiClientService } from '../services/ai-client.service'
import { NetworkMonitorService } from '../services/network-monitor.service'
@@ -42,8 +44,11 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
registerSnapshotHandlers(books, snapshotService!)
registerBookmarkHandlers(books)
registerSearchHandlers(books, settings)
registerAiHandlers(books, settings, new AiClientService(() => settings.get().aiConfig))
registerInteractiveHandlers(books, settings, new AiClientService(() => settings.get().aiConfig))
const aiClient = new AiClientService(() => settings.get().aiConfig)
registerAiHandlers(books, settings, aiClient)
registerInteractiveHandlers(books, settings, aiClient)
registerAutoHandlers(books, settings, aiClient)
registerWizardHandlers(books, settings, aiClient)
return { settings, books, shortcuts: shortcutManager }
}