feat: ship v0.5.0 with auto and wizard writing modes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user