feat: ship v0.4.0 with interactive writing mode
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { IPC } from '../../../shared/ipc-channels'
|
||||
import type { AiContextBinding, PlotSelection } 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 { checkInteractiveGate } from '../../services/interactive-gate.service'
|
||||
import { InteractiveWritingService } from '../../services/interactive-writing.service'
|
||||
import { wrap } from '../result'
|
||||
|
||||
const activeInteractive = new Map<string, AbortController>()
|
||||
|
||||
function serviceFor(registry: BookRegistryService, bookId: string, aiClient: AiClientService) {
|
||||
return new InteractiveWritingService(registry.getDb(bookId), aiClient)
|
||||
}
|
||||
|
||||
function enrich(registry: BookRegistryService, bookId: string, aiClient: AiClientService, sessionId: string) {
|
||||
const svc = serviceFor(registry, bookId, aiClient)
|
||||
const flow = svc.getFlow(sessionId)
|
||||
return flow ? svc.enrichFlow(flow) : null
|
||||
}
|
||||
|
||||
export function registerInteractiveHandlers(
|
||||
registry: BookRegistryService,
|
||||
settings: GlobalSettingsService,
|
||||
aiClient: AiClientService
|
||||
): void {
|
||||
ipcMain.handle(IPC.INTERACTIVE_CHECK_GATE, (_e, { bookId }: { bookId: string }) =>
|
||||
wrap(() => checkInteractiveGate(registry.getDb(bookId)))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_GET_FLOW,
|
||||
(_e, { bookId, sessionId }: { bookId: string; sessionId: string }) =>
|
||||
wrap(() => enrich(registry, bookId, aiClient, sessionId))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_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.INTERACTIVE_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 flow = serviceFor(registry, bookId, aiClient).confirmContext(
|
||||
flowId,
|
||||
binding,
|
||||
summary
|
||||
)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_SUGGEST_PLOTS,
|
||||
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
|
||||
wrap(() => serviceFor(registry, bookId, aiClient).suggestPlots(flowId))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_SELECT_PLOT,
|
||||
(
|
||||
_e,
|
||||
{ bookId, flowId, selection }: { bookId: string; flowId: string; selection: PlotSelection }
|
||||
) =>
|
||||
wrap(() => {
|
||||
const flow = serviceFor(registry, bookId, aiClient).selectPlot(flowId, selection)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_GENERATE_SCENE,
|
||||
(event, { bookId, flowId }: { bookId: string; flowId: string }) =>
|
||||
wrap(async () => {
|
||||
const controller = new AbortController()
|
||||
activeInteractive.set(flowId, controller)
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
try {
|
||||
const flow = await serviceFor(registry, bookId, aiClient).generateScene(
|
||||
flowId,
|
||||
(delta, done) => {
|
||||
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
|
||||
flowId,
|
||||
phase: 'scene',
|
||||
delta,
|
||||
done
|
||||
})
|
||||
},
|
||||
controller.signal
|
||||
)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
} finally {
|
||||
activeInteractive.delete(flowId)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_RESOLVE_NAMING,
|
||||
(
|
||||
event,
|
||||
{ bookId, flowId, chosenName }: { bookId: string; flowId: string; chosenName: string }
|
||||
) =>
|
||||
wrap(async () => {
|
||||
const controller = new AbortController()
|
||||
activeInteractive.set(flowId, controller)
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
try {
|
||||
const flow = await serviceFor(registry, bookId, aiClient).resolveNaming(
|
||||
flowId,
|
||||
chosenName,
|
||||
(delta, done) => {
|
||||
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
|
||||
flowId,
|
||||
phase: 'naming_resume',
|
||||
delta,
|
||||
done
|
||||
})
|
||||
},
|
||||
controller.signal
|
||||
)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
} finally {
|
||||
activeInteractive.delete(flowId)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_REFINE_SCENE,
|
||||
(
|
||||
event,
|
||||
{ bookId, flowId, instruction }: { bookId: string; flowId: string; instruction: string }
|
||||
) =>
|
||||
wrap(async () => {
|
||||
const controller = new AbortController()
|
||||
activeInteractive.set(flowId, controller)
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
try {
|
||||
const flow = await serviceFor(registry, bookId, aiClient).refineScene(
|
||||
flowId,
|
||||
instruction,
|
||||
(delta, done) => {
|
||||
win?.webContents.send(IPC.INTERACTIVE_STREAM_CHUNK, {
|
||||
flowId,
|
||||
phase: 'refine',
|
||||
delta,
|
||||
done
|
||||
})
|
||||
},
|
||||
controller.signal
|
||||
)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
} finally {
|
||||
activeInteractive.delete(flowId)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_ACCEPT_SCENE,
|
||||
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
|
||||
wrap(() => {
|
||||
const flow = serviceFor(registry, bookId, aiClient).acceptScene(flowId)
|
||||
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_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.INTERACTIVE_ABORT, (_e, { flowId }: { flowId: string }) =>
|
||||
wrap(() => {
|
||||
activeInteractive.get(flowId)?.abort()
|
||||
activeInteractive.delete(flowId)
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.INTERACTIVE_GET_SCENE_DRAFT,
|
||||
(_e, { bookId, flowId }: { bookId: string; flowId: string }) =>
|
||||
wrap(() => serviceFor(registry, bookId, aiClient).getSceneDraft(flowId))
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { registerSnapshotHandlers } from './handlers/snapshot.handler'
|
||||
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 { AiClientService } from '../services/ai-client.service'
|
||||
import { NetworkMonitorService } from '../services/network-monitor.service'
|
||||
|
||||
@@ -42,6 +43,7 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
|
||||
registerBookmarkHandlers(books)
|
||||
registerSearchHandlers(books, settings)
|
||||
registerAiHandlers(books, settings, new AiClientService(() => settings.get().aiConfig))
|
||||
registerInteractiveHandlers(books, settings, new AiClientService(() => settings.get().aiConfig))
|
||||
|
||||
return { settings, books, shortcuts: shortcutManager }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user