mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-30 17:22:02 +08:00
1141 lines
43 KiB
TypeScript
1141 lines
43 KiB
TypeScript
import type { AutomationMode } from "./modes.js";
|
||
import { routeInteractionRequest } from "./request-router.js";
|
||
import type { InteractionRequest } from "./intents.js";
|
||
import type { ExecutionState, InteractionEvent } from "./events.js";
|
||
import type { PendingDecision, InteractionSession } from "./session.js";
|
||
import {
|
||
appendInteractionEvent,
|
||
bindActiveBook,
|
||
clearCreationDraft,
|
||
clearPendingDecision,
|
||
updateCreationDraft,
|
||
updateAutomationMode,
|
||
} from "./session.js";
|
||
|
||
type ReviseMode = "local-fix" | "rewrite";
|
||
type RuntimeLanguage = "zh" | "en";
|
||
|
||
export interface InteractionRuntimeTools {
|
||
readonly listBooks: () => Promise<ReadonlyArray<string>>;
|
||
readonly createBook?: (input: {
|
||
readonly title: string;
|
||
readonly genre?: string;
|
||
readonly platform?: string;
|
||
readonly language?: "zh" | "en";
|
||
readonly chapterWordCount?: number;
|
||
readonly targetChapters?: number;
|
||
readonly blurb?: string;
|
||
readonly worldPremise?: string;
|
||
readonly settingNotes?: string;
|
||
readonly protagonist?: string;
|
||
readonly supportingCast?: string;
|
||
readonly conflictCore?: string;
|
||
readonly volumeOutline?: string;
|
||
readonly constraints?: string;
|
||
readonly authorIntent?: string;
|
||
readonly currentFocus?: string;
|
||
}) => Promise<unknown>;
|
||
readonly exportBook?: (bookId: string, options: {
|
||
readonly format?: "txt" | "md" | "epub";
|
||
readonly approvedOnly?: boolean;
|
||
readonly outputPath?: string;
|
||
}) => Promise<unknown>;
|
||
readonly chat?: (
|
||
input: string,
|
||
options: {
|
||
readonly bookId?: string;
|
||
readonly automationMode: AutomationMode;
|
||
},
|
||
) => Promise<unknown>;
|
||
readonly writeNextChapter: (bookId: string) => Promise<unknown>;
|
||
readonly reviseDraft: (bookId: string, chapterNumber: number, mode: ReviseMode) => Promise<unknown>;
|
||
readonly patchChapterText: (
|
||
bookId: string,
|
||
chapterNumber: number,
|
||
targetText: string,
|
||
replacementText: string,
|
||
) => Promise<unknown>;
|
||
readonly replaceChapterText: (
|
||
bookId: string,
|
||
chapterNumber: number,
|
||
fullText: string,
|
||
) => Promise<unknown>;
|
||
readonly renameEntity: (
|
||
bookId: string,
|
||
oldValue: string,
|
||
newValue: string,
|
||
) => Promise<unknown>;
|
||
readonly updateCurrentFocus: (bookId: string, content: string) => Promise<unknown>;
|
||
readonly updateAuthorIntent: (bookId: string, content: string) => Promise<unknown>;
|
||
readonly writeTruthFile: (bookId: string, fileName: string, content: string) => Promise<unknown>;
|
||
}
|
||
|
||
export interface InteractionRuntimeResult {
|
||
readonly session: InteractionSession;
|
||
readonly responseText?: string;
|
||
readonly details?: Readonly<Record<string, unknown>>;
|
||
}
|
||
|
||
interface InteractionToolMetadata {
|
||
readonly events?: ReadonlyArray<InteractionEvent>;
|
||
readonly activeChapterNumber?: number;
|
||
readonly currentExecution?: ExecutionState;
|
||
readonly pendingDecision?: PendingDecision;
|
||
readonly responseText?: string;
|
||
readonly details?: Readonly<Record<string, unknown>>;
|
||
}
|
||
|
||
function extractToolMetadata(value: unknown): InteractionToolMetadata {
|
||
const chapterNumber = typeof value === "object" && value !== null && "chapterNumber" in value
|
||
&& typeof (value as { chapterNumber?: unknown }).chapterNumber === "number"
|
||
? (value as { chapterNumber: number }).chapterNumber
|
||
: undefined;
|
||
|
||
if (!value || typeof value !== "object" || !("__interaction" in value)) {
|
||
return {
|
||
...(chapterNumber !== undefined ? { activeChapterNumber: chapterNumber } : {}),
|
||
};
|
||
}
|
||
|
||
const interaction = (value as {
|
||
readonly __interaction?: InteractionToolMetadata;
|
||
}).__interaction;
|
||
|
||
return {
|
||
...interaction,
|
||
...(interaction?.activeChapterNumber === undefined && chapterNumber !== undefined
|
||
? { activeChapterNumber: chapterNumber }
|
||
: {}),
|
||
};
|
||
}
|
||
|
||
function resolveRuntimeLanguage(request: InteractionRequest): RuntimeLanguage {
|
||
return request.language === "en" ? "en" : "zh";
|
||
}
|
||
|
||
function localize<T>(language: RuntimeLanguage, messages: { zh: T; en: T }): T {
|
||
return language === "en" ? messages.en : messages.zh;
|
||
}
|
||
|
||
function localizeMode(mode: AutomationMode, language: RuntimeLanguage): string {
|
||
if (language === "en") {
|
||
return mode;
|
||
}
|
||
|
||
return {
|
||
auto: "自动",
|
||
semi: "半自动",
|
||
manual: "手动",
|
||
}[mode] ?? mode;
|
||
}
|
||
|
||
function renderCreationDraft(
|
||
draft: NonNullable<InteractionSession["creationDraft"]>,
|
||
language: RuntimeLanguage,
|
||
): string {
|
||
const lines = language === "en"
|
||
? [
|
||
"# Current Book Draft",
|
||
draft.title ? `- Title: ${draft.title}` : undefined,
|
||
draft.genre ? `- Genre: ${draft.genre}` : undefined,
|
||
draft.platform ? `- Platform: ${draft.platform}` : undefined,
|
||
draft.worldPremise ? `- World: ${draft.worldPremise}` : undefined,
|
||
draft.protagonist ? `- Protagonist: ${draft.protagonist}` : undefined,
|
||
draft.conflictCore ? `- Core Conflict: ${draft.conflictCore}` : undefined,
|
||
draft.volumeOutline ? `- Volume Direction: ${draft.volumeOutline}` : undefined,
|
||
draft.blurb ? `- Blurb: ${draft.blurb}` : undefined,
|
||
draft.nextQuestion ? `- Next: ${draft.nextQuestion}` : undefined,
|
||
]
|
||
: [
|
||
"# 当前创作草案",
|
||
draft.title ? `- 书名:${draft.title}` : undefined,
|
||
draft.genre ? `- 题材:${draft.genre}` : undefined,
|
||
draft.platform ? `- 平台:${draft.platform}` : undefined,
|
||
draft.worldPremise ? `- 世界观:${draft.worldPremise}` : undefined,
|
||
draft.protagonist ? `- 主角:${draft.protagonist}` : undefined,
|
||
draft.conflictCore ? `- 核心冲突:${draft.conflictCore}` : undefined,
|
||
draft.volumeOutline ? `- 卷纲方向:${draft.volumeOutline}` : undefined,
|
||
draft.blurb ? `- 简介:${draft.blurb}` : undefined,
|
||
draft.nextQuestion ? `- 下一步:${draft.nextQuestion}` : undefined,
|
||
];
|
||
return lines.filter(Boolean).join("\n");
|
||
}
|
||
|
||
function buildTaskStartedState(
|
||
session: InteractionSession,
|
||
request: InteractionRequest,
|
||
language: RuntimeLanguage,
|
||
): ExecutionState {
|
||
switch (request.intent) {
|
||
case "write_next":
|
||
case "continue_book":
|
||
return {
|
||
status: "planning",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "准备章节输入",
|
||
en: "preparing chapter inputs",
|
||
}),
|
||
};
|
||
case "create_book":
|
||
return {
|
||
status: "planning",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
stageLabel: localize(language, {
|
||
zh: "创建作品基础",
|
||
en: "creating book foundation",
|
||
}),
|
||
};
|
||
case "export_book":
|
||
return {
|
||
status: "persisting",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "导出作品文件",
|
||
en: "exporting book artifacts",
|
||
}),
|
||
};
|
||
case "revise_chapter":
|
||
case "rewrite_chapter":
|
||
return {
|
||
status: "repairing",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: request.chapterNumber ?? session.activeChapterNumber,
|
||
stageLabel: request.intent === "rewrite_chapter"
|
||
? localize(language, { zh: "重写章节", en: "rewriting chapter" })
|
||
: localize(language, { zh: "修订章节", en: "revising chapter" }),
|
||
};
|
||
case "update_focus":
|
||
case "update_author_intent":
|
||
case "edit_truth":
|
||
return {
|
||
status: "persisting",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "应用项目修改",
|
||
en: "applying project edit",
|
||
}),
|
||
};
|
||
case "pause_book":
|
||
case "discard_book_draft":
|
||
return {
|
||
status: "blocked",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "已由用户暂停",
|
||
en: "paused by user",
|
||
}),
|
||
};
|
||
default:
|
||
return {
|
||
status: "planning",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: `处理中:${request.intent}`,
|
||
en: `handling ${request.intent}`,
|
||
}),
|
||
};
|
||
}
|
||
}
|
||
|
||
function shouldWaitForHuman(
|
||
automationMode: AutomationMode,
|
||
request: InteractionRequest,
|
||
): boolean {
|
||
const contentIntent = request.intent === "write_next"
|
||
|| request.intent === "continue_book"
|
||
|| request.intent === "revise_chapter"
|
||
|| request.intent === "rewrite_chapter"
|
||
|| request.intent === "patch_chapter_text"
|
||
|| request.intent === "replace_chapter_text";
|
||
const editIntent = request.intent === "update_focus"
|
||
|| request.intent === "update_author_intent"
|
||
|| request.intent === "edit_truth"
|
||
|| request.intent === "rename_entity";
|
||
|
||
if (automationMode === "auto") {
|
||
return false;
|
||
}
|
||
if (automationMode === "semi") {
|
||
return contentIntent;
|
||
}
|
||
return contentIntent || editIntent;
|
||
}
|
||
|
||
function buildPendingDecision(
|
||
session: InteractionSession,
|
||
request: InteractionRequest,
|
||
language: RuntimeLanguage,
|
||
chapterNumber?: number,
|
||
): PendingDecision | undefined {
|
||
if (!shouldWaitForHuman(session.automationMode, request)) {
|
||
return undefined;
|
||
}
|
||
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
return undefined;
|
||
}
|
||
|
||
return {
|
||
kind: "review-next-step",
|
||
bookId,
|
||
...(chapterNumber !== undefined ? { chapterNumber } : {}),
|
||
summary: session.automationMode === "manual"
|
||
? localize(language, {
|
||
zh: "执行已完成。请明确选择下一步操作。",
|
||
en: "Execution finished. Choose the next action explicitly.",
|
||
})
|
||
: localize(language, {
|
||
zh: "执行已完成,等待你的下一步决定。",
|
||
en: "Execution finished. Waiting for your next decision.",
|
||
}),
|
||
};
|
||
}
|
||
|
||
function buildWaitingExecution(
|
||
session: InteractionSession,
|
||
request: InteractionRequest,
|
||
language: RuntimeLanguage,
|
||
chapterNumber?: number,
|
||
): ExecutionState {
|
||
return {
|
||
status: "waiting_human",
|
||
bookId: request.bookId ?? session.activeBookId,
|
||
...(chapterNumber !== undefined ? { chapterNumber } : {}),
|
||
stageLabel: localize(language, {
|
||
zh: "等待你的下一步决定",
|
||
en: "waiting for your next decision",
|
||
}),
|
||
};
|
||
}
|
||
|
||
function appendToolEvents(
|
||
session: InteractionSession,
|
||
events: ReadonlyArray<InteractionEvent> | undefined,
|
||
): InteractionSession {
|
||
if (!events || events.length === 0) {
|
||
return session;
|
||
}
|
||
|
||
const baseTimestamp = Date.now();
|
||
return events.reduce((nextSession, event, index) => appendInteractionEvent(nextSession, {
|
||
...event,
|
||
timestamp: baseTimestamp - events.length + index,
|
||
}), session);
|
||
}
|
||
|
||
interface RuntimeRequestHelpers {
|
||
readonly language: RuntimeLanguage;
|
||
readonly addEvent: (
|
||
nextSession: InteractionSession,
|
||
kind: string,
|
||
status: InteractionEvent["status"],
|
||
detail: string,
|
||
) => InteractionSession;
|
||
readonly markCompleted: (nextSession: InteractionSession) => InteractionSession;
|
||
}
|
||
|
||
async function handleDraftLifecycleRequest(params: {
|
||
readonly session: InteractionSession;
|
||
readonly request: InteractionRequest;
|
||
readonly tools: InteractionRuntimeTools;
|
||
readonly helpers: RuntimeRequestHelpers;
|
||
}): Promise<InteractionRuntimeResult | undefined> {
|
||
const { session, request, tools, helpers } = params;
|
||
const { language, addEvent, markCompleted } = helpers;
|
||
|
||
switch (request.intent) {
|
||
case "show_book_draft": {
|
||
if (!session.creationDraft) {
|
||
return {
|
||
session: markCompleted(session),
|
||
responseText: localize(language, {
|
||
zh: "当前还没有创作草案。先告诉我你想写什么,再逐步把书收出来。",
|
||
en: "There is no active book draft yet. Start by telling me what you want to write.",
|
||
}),
|
||
};
|
||
}
|
||
return {
|
||
session: markCompleted(session),
|
||
responseText: renderCreationDraft(session.creationDraft, language),
|
||
};
|
||
}
|
||
case "create_book": {
|
||
if (!tools.createBook) {
|
||
throw new Error(localize(language, {
|
||
zh: "交互运行时暂未实现创建作品。",
|
||
en: "Book creation is not implemented in the interaction runtime yet.",
|
||
}));
|
||
}
|
||
const effectiveDraft = session.creationDraft;
|
||
const title = request.title ?? effectiveDraft?.title;
|
||
if (!title) {
|
||
throw new Error(localize(language, {
|
||
zh: "创建作品需要标题。",
|
||
en: "Book creation requires a title.",
|
||
}));
|
||
}
|
||
const toolResult = await tools.createBook({
|
||
title,
|
||
genre: request.genre ?? effectiveDraft?.genre,
|
||
platform: request.platform ?? effectiveDraft?.platform,
|
||
language: request.language ?? effectiveDraft?.language,
|
||
chapterWordCount: request.chapterWordCount ?? effectiveDraft?.chapterWordCount,
|
||
targetChapters: request.targetChapters ?? effectiveDraft?.targetChapters,
|
||
blurb: request.blurb ?? effectiveDraft?.blurb,
|
||
worldPremise: request.worldPremise ?? effectiveDraft?.worldPremise,
|
||
settingNotes: request.settingNotes ?? effectiveDraft?.settingNotes,
|
||
protagonist: request.protagonist ?? effectiveDraft?.protagonist,
|
||
supportingCast: request.supportingCast ?? effectiveDraft?.supportingCast,
|
||
conflictCore: request.conflictCore ?? effectiveDraft?.conflictCore,
|
||
volumeOutline: request.volumeOutline ?? effectiveDraft?.volumeOutline,
|
||
constraints: request.constraints ?? effectiveDraft?.constraints,
|
||
authorIntent: request.authorIntent ?? effectiveDraft?.authorIntent,
|
||
currentFocus: request.currentFocus ?? effectiveDraft?.currentFocus,
|
||
});
|
||
const metadata = extractToolMetadata(toolResult);
|
||
const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult
|
||
&& typeof (toolResult as { bookId?: unknown }).bookId === "string"
|
||
? (toolResult as { bookId: string }).bookId
|
||
: undefined;
|
||
if (!createdBookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "创建作品工具没有返回作品 ID。",
|
||
en: "Create-book tool did not return a book id.",
|
||
}));
|
||
}
|
||
const nextSession = appendToolEvents(
|
||
clearCreationDraft(bindActiveBook(session, createdBookId)),
|
||
metadata.events,
|
||
);
|
||
const completed = {
|
||
...markCompleted(nextSession),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(nextSession).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已创建作品 ${createdBookId}。`,
|
||
en: `Created ${createdBookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? localize(language, {
|
||
zh: `已创建作品 ${createdBookId}。`,
|
||
en: `Created ${createdBookId}.`,
|
||
}),
|
||
details: metadata.details,
|
||
};
|
||
}
|
||
case "discard_book_draft": {
|
||
const completed = markCompleted(clearCreationDraft(session));
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: "已丢弃当前创作草案。",
|
||
en: "Discarded the current book draft.",
|
||
})),
|
||
responseText: localize(language, {
|
||
zh: "已丢弃当前创作草案。",
|
||
en: "Discarded the current book draft.",
|
||
}),
|
||
};
|
||
}
|
||
default:
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
async function handleBookSelectionRequest(params: {
|
||
readonly session: InteractionSession;
|
||
readonly request: InteractionRequest;
|
||
readonly tools: InteractionRuntimeTools;
|
||
readonly helpers: RuntimeRequestHelpers;
|
||
}): Promise<InteractionRuntimeResult | undefined> {
|
||
const { session, request, tools, helpers } = params;
|
||
const { language, addEvent, markCompleted } = helpers;
|
||
|
||
switch (request.intent) {
|
||
case "list_books": {
|
||
const books = await tools.listBooks();
|
||
const completed = markCompleted(session);
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已列出 ${books.length} 本作品。`,
|
||
en: `Listed ${books.length} book(s).`,
|
||
})),
|
||
responseText: books.length > 0
|
||
? localize(language, {
|
||
zh: `作品列表:${books.join("、")}`,
|
||
en: `Books: ${books.join(", ")}`,
|
||
})
|
||
: localize(language, {
|
||
zh: "当前项目下没有作品。",
|
||
en: "No books found in this project.",
|
||
}),
|
||
};
|
||
}
|
||
case "select_book": {
|
||
if (!request.bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "切换作品需要提供作品 ID。",
|
||
en: "Book selection requires a book id.",
|
||
}));
|
||
}
|
||
const books = await tools.listBooks();
|
||
if (!books.includes(request.bookId)) {
|
||
throw new Error(localize(language, {
|
||
zh: `当前项目中找不到作品「${request.bookId}」。`,
|
||
en: `Book "${request.bookId}" not found in this project.`,
|
||
}));
|
||
}
|
||
const completed = markCompleted(bindActiveBook(session, request.bookId));
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已切换当前作品到 ${request.bookId}。`,
|
||
en: `Bound active book to ${request.bookId}.`,
|
||
})),
|
||
responseText: localize(language, {
|
||
zh: `当前作品:${request.bookId}`,
|
||
en: `Active book: ${request.bookId}`,
|
||
}),
|
||
};
|
||
}
|
||
default:
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
export async function runInteractionRequest(params: {
|
||
readonly session: InteractionSession;
|
||
readonly request: InteractionRequest;
|
||
readonly tools: InteractionRuntimeTools;
|
||
}): Promise<InteractionRuntimeResult> {
|
||
const request = routeInteractionRequest(params.request);
|
||
const language = resolveRuntimeLanguage(request);
|
||
let session = params.session;
|
||
const addEvent = (
|
||
nextSession: InteractionSession,
|
||
kind: string,
|
||
status: InteractionEvent["status"],
|
||
detail: string,
|
||
): InteractionSession => appendInteractionEvent(nextSession, {
|
||
kind,
|
||
timestamp: Date.now(),
|
||
status,
|
||
bookId: nextSession.activeBookId,
|
||
chapterNumber: nextSession.activeChapterNumber,
|
||
detail,
|
||
});
|
||
|
||
if (request.mode) {
|
||
session = updateAutomationMode(session, request.mode as AutomationMode);
|
||
}
|
||
|
||
session = clearPendingDecision({
|
||
...session,
|
||
currentExecution: buildTaskStartedState(session, request, language),
|
||
});
|
||
session = addEvent(session, "task.started", session.currentExecution!.status, localize(language, {
|
||
zh: `开始执行 ${request.intent}。`,
|
||
en: `Started ${request.intent}.`,
|
||
}));
|
||
|
||
const markCompleted = (nextSession: InteractionSession): InteractionSession => ({
|
||
...nextSession,
|
||
currentExecution: {
|
||
status: "completed",
|
||
bookId: nextSession.activeBookId,
|
||
chapterNumber: nextSession.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "已完成",
|
||
en: "completed",
|
||
}),
|
||
},
|
||
});
|
||
|
||
const helperContext: RuntimeRequestHelpers = {
|
||
language,
|
||
addEvent,
|
||
markCompleted,
|
||
};
|
||
|
||
const draftLifecycleResult = await handleDraftLifecycleRequest({
|
||
session,
|
||
request,
|
||
tools: params.tools,
|
||
helpers: helperContext,
|
||
});
|
||
if (draftLifecycleResult) {
|
||
return draftLifecycleResult;
|
||
}
|
||
|
||
const bookSelectionResult = await handleBookSelectionRequest({
|
||
session,
|
||
request,
|
||
tools: params.tools,
|
||
helpers: helperContext,
|
||
});
|
||
if (bookSelectionResult) {
|
||
return bookSelectionResult;
|
||
}
|
||
|
||
switch (request.intent) {
|
||
case "write_next":
|
||
case "continue_book": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.writeNextChapter(bookId);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
|
||
session,
|
||
request,
|
||
language,
|
||
metadata.activeChapterNumber,
|
||
);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language, metadata.activeChapterNumber),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已为 ${bookId} 完成下一章写作。`,
|
||
en: `Completed write_next for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已为 ${bookId} 完成下一章写作,等待你的下一步决定。`,
|
||
en: `Completed write_next for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已为 ${bookId} 完成下一章写作。`,
|
||
en: `Completed write_next for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "revise_chapter":
|
||
case "rewrite_chapter": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.chapterNumber) {
|
||
throw new Error(localize(language, {
|
||
zh: "修订章节需要章节号。",
|
||
en: "Chapter number is required for chapter revision.",
|
||
}));
|
||
}
|
||
const mode: ReviseMode = request.intent === "rewrite_chapter" ? "rewrite" : "local-fix";
|
||
const toolResult = await params.tools.reviseDraft(bookId, request.chapterNumber, mode);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
const chapterNumber = metadata.activeChapterNumber ?? request.chapterNumber;
|
||
session = bindActiveBook(session, bookId, chapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
|
||
session,
|
||
request,
|
||
language,
|
||
chapterNumber,
|
||
);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language, chapterNumber),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: request.intent === "rewrite_chapter"
|
||
? `已为 ${bookId} 完成章节重写。`
|
||
: `已为 ${bookId} 完成章节修订。`,
|
||
en: `Completed ${request.intent} for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: request.intent === "rewrite_chapter"
|
||
? `已为 ${bookId} 完成章节重写,等待你的下一步决定。`
|
||
: `已为 ${bookId} 完成章节修订,等待你的下一步决定。`,
|
||
en: `Completed ${request.intent} for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: request.intent === "rewrite_chapter"
|
||
? `已为 ${bookId} 完成章节重写。`
|
||
: `已为 ${bookId} 完成章节修订。`,
|
||
en: `Completed ${request.intent} for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "patch_chapter_text": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.chapterNumber || !request.targetText || !request.replacementText) {
|
||
throw new Error(localize(language, {
|
||
zh: "正文修补需要章节号、目标文本和替换文本。",
|
||
en: "Chapter patch requires chapter number, target text, and replacement text.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.patchChapterText(
|
||
bookId,
|
||
request.chapterNumber,
|
||
request.targetText,
|
||
request.replacementText,
|
||
);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
const chapterNumber = metadata.activeChapterNumber ?? request.chapterNumber;
|
||
session = bindActiveBook(session, bookId, chapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
|
||
session,
|
||
request,
|
||
language,
|
||
chapterNumber,
|
||
);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language, chapterNumber),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已修补 ${bookId} 的第 ${chapterNumber} 章。`,
|
||
en: `Patched chapter ${chapterNumber} for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已修补 ${bookId} 的第 ${chapterNumber} 章,等待你的下一步决定。`,
|
||
en: `Patched chapter ${chapterNumber} for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已修补 ${bookId} 的第 ${chapterNumber} 章。`,
|
||
en: `Patched chapter ${chapterNumber} for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "replace_chapter_text": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.chapterNumber || !request.fullText) {
|
||
throw new Error(localize(language, {
|
||
zh: "整章替换需要章节号和完整正文。",
|
||
en: "Whole-chapter replacement requires chapter number and fullText.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.replaceChapterText(
|
||
bookId,
|
||
request.chapterNumber,
|
||
request.fullText,
|
||
);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
const chapterNumber = metadata.activeChapterNumber ?? request.chapterNumber;
|
||
session = bindActiveBook(session, bookId, chapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
|
||
session,
|
||
request,
|
||
language,
|
||
chapterNumber,
|
||
);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language, chapterNumber),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已替换 ${bookId} 的第 ${chapterNumber} 章。`,
|
||
en: `Replaced chapter ${chapterNumber} for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已替换 ${bookId} 的第 ${chapterNumber} 章,等待你的下一步决定。`,
|
||
en: `Replaced chapter ${chapterNumber} for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已替换 ${bookId} 的第 ${chapterNumber} 章。`,
|
||
en: `Replaced chapter ${chapterNumber} for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "rename_entity": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.oldValue || !request.newValue) {
|
||
throw new Error(localize(language, {
|
||
zh: "实体改名需要旧值和新值。",
|
||
en: "Entity rename requires old and new values.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.renameEntity(bookId, request.oldValue, request.newValue);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(
|
||
session,
|
||
request,
|
||
language,
|
||
metadata.activeChapterNumber,
|
||
);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language, metadata.activeChapterNumber),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已在 ${bookId} 中把 ${request.oldValue} 改成 ${request.newValue}。`,
|
||
en: `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已在 ${bookId} 中把 ${request.oldValue} 改成 ${request.newValue},等待你的下一步决定。`,
|
||
en: `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已在 ${bookId} 中把 ${request.oldValue} 改成 ${request.newValue}。`,
|
||
en: `Renamed ${request.oldValue} to ${request.newValue} in ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "update_focus": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.instruction) {
|
||
throw new Error(localize(language, {
|
||
zh: "更新焦点需要提供内容。",
|
||
en: "Focus update requires instruction content.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.updateCurrentFocus(bookId, request.instruction);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request, language);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已更新 ${bookId} 的当前焦点。`,
|
||
en: `Updated current focus for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已更新 ${bookId} 的当前焦点,等待你的下一步决定。`,
|
||
en: `Updated current focus for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已更新 ${bookId} 的当前焦点。`,
|
||
en: `Updated current focus for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "update_author_intent": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.instruction) {
|
||
throw new Error(localize(language, {
|
||
zh: "更新作者意图需要提供内容。",
|
||
en: "Author intent update requires instruction content.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.updateAuthorIntent(bookId, request.instruction);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request, language);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已更新 ${bookId} 的作者意图。`,
|
||
en: `Updated author intent for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已更新 ${bookId} 的作者意图,等待你的下一步决定。`,
|
||
en: `Updated author intent for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已更新 ${bookId} 的作者意图。`,
|
||
en: `Updated author intent for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "edit_truth": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
if (!request.fileName || !request.instruction) {
|
||
throw new Error(localize(language, {
|
||
zh: "编辑真相文件需要文件名和内容。",
|
||
en: "Truth-file edit requires a file name and content.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.writeTruthFile(bookId, request.fileName, request.instruction);
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const pendingDecision = metadata.pendingDecision ?? buildPendingDecision(session, request, language);
|
||
const completed = pendingDecision
|
||
? {
|
||
...session,
|
||
pendingDecision,
|
||
currentExecution: metadata.currentExecution ?? buildWaitingExecution(session, request, language),
|
||
}
|
||
: {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已更新 ${bookId} 的 ${request.fileName}。`,
|
||
en: `Updated ${request.fileName} for ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? (
|
||
pendingDecision
|
||
? localize(language, {
|
||
zh: `已更新 ${bookId} 的 ${request.fileName},等待你的下一步决定。`,
|
||
en: `Updated ${request.fileName} for ${bookId}; waiting for your next decision.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `已更新 ${bookId} 的 ${request.fileName}。`,
|
||
en: `Updated ${request.fileName} for ${bookId}.`,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
case "export_book": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
if (!params.tools.exportBook) {
|
||
throw new Error(localize(language, {
|
||
zh: "交互运行时暂未实现导出作品。",
|
||
en: "Book export is not implemented in the interaction runtime yet.",
|
||
}));
|
||
}
|
||
if (!bookId) {
|
||
throw new Error(localize(language, {
|
||
zh: "当前交互会话还没有绑定作品。",
|
||
en: "No active book is bound to the interaction session.",
|
||
}));
|
||
}
|
||
const toolResult = await params.tools.exportBook(bookId, {
|
||
format: request.format,
|
||
approvedOnly: request.approvedOnly,
|
||
outputPath: request.outputPath,
|
||
});
|
||
const metadata = extractToolMetadata(toolResult);
|
||
session = bindActiveBook(session, bookId, metadata.activeChapterNumber);
|
||
session = appendToolEvents(session, metadata.events);
|
||
const completed = {
|
||
...markCompleted(session),
|
||
currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
|
||
};
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", localize(language, {
|
||
zh: `已导出 ${bookId}。`,
|
||
en: `Exported ${bookId}.`,
|
||
})),
|
||
responseText: metadata.responseText ?? localize(language, {
|
||
zh: `已导出 ${bookId}。`,
|
||
en: `Exported ${bookId}.`,
|
||
}),
|
||
details: metadata.details,
|
||
};
|
||
}
|
||
case "pause_book": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
const paused = {
|
||
...session,
|
||
currentExecution: {
|
||
status: "blocked" as const,
|
||
bookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "已由用户暂停",
|
||
en: "paused by user",
|
||
}),
|
||
},
|
||
};
|
||
return {
|
||
session: addEvent(paused, "task.completed", "blocked", localize(language, {
|
||
zh: `已暂停${bookId ?? "当前作品"}。`,
|
||
en: `Paused ${bookId ?? "current book"}.`,
|
||
})),
|
||
responseText: localize(language, {
|
||
zh: `已暂停${bookId ?? "当前作品"}。`,
|
||
en: `Paused ${bookId ?? "current book"}.`,
|
||
}),
|
||
};
|
||
}
|
||
case "resume_book": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
const resumed = {
|
||
...session,
|
||
currentExecution: {
|
||
status: "completed" as const,
|
||
bookId,
|
||
chapterNumber: session.activeChapterNumber,
|
||
stageLabel: localize(language, {
|
||
zh: "可继续执行",
|
||
en: "ready to continue",
|
||
}),
|
||
},
|
||
};
|
||
return {
|
||
session: addEvent(resumed, "task.completed", "completed", localize(language, {
|
||
zh: `已恢复${bookId ?? "当前作品"}。`,
|
||
en: `Resumed ${bookId ?? "current book"}.`,
|
||
})),
|
||
responseText: localize(language, {
|
||
zh: `已恢复${bookId ?? "当前作品"}。`,
|
||
en: `Resumed ${bookId ?? "current book"}.`,
|
||
}),
|
||
};
|
||
}
|
||
case "chat": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
const toolResult = params.tools.chat
|
||
? await params.tools.chat(request.instruction ?? "", {
|
||
bookId,
|
||
automationMode: session.automationMode,
|
||
})
|
||
: undefined;
|
||
const metadata = extractToolMetadata(toolResult);
|
||
const responseText = metadata.responseText ?? (bookId
|
||
? localize(language, {
|
||
zh: `我在。当前作品是 ${bookId}。你可以让我继续写、修订章节、重写、调整焦点,或者查看流水线为何停止。`,
|
||
en: `I’m here. Active book is ${bookId}. You can ask me to continue, revise a chapter, rewrite, change focus, or inspect why the pipeline stopped.`,
|
||
})
|
||
: localize(language, {
|
||
zh: "我在。当前还没有绑定作品。先打开作品、列出作品,或者直接描述你要写什么。",
|
||
en: "I’m here. No active book is bound yet. Open a book, list books, or describe what you want to write.",
|
||
}));
|
||
const completed = markCompleted(session);
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", responseText),
|
||
responseText,
|
||
};
|
||
}
|
||
case "explain_status":
|
||
case "explain_failure": {
|
||
const bookId = request.bookId ?? session.activeBookId;
|
||
const baselineExecution = params.session.currentExecution;
|
||
const stage = baselineExecution?.stageLabel ?? baselineExecution?.status ?? "idle";
|
||
const summary = request.intent === "explain_failure"
|
||
? localize(language, {
|
||
zh: `当前失败上下文:${bookId ?? "当前无激活作品"} 处于 ${stage}。`,
|
||
en: `Current failure context: ${bookId ?? "no active book"} is at ${stage}.`,
|
||
})
|
||
: localize(language, {
|
||
zh: `当前状态:${bookId ?? "当前无激活作品"} 处于 ${stage}。`,
|
||
en: `Current status: ${bookId ?? "no active book"} is at ${stage}.`,
|
||
});
|
||
const completed = markCompleted(session);
|
||
return {
|
||
session: addEvent(completed, "task.completed", "completed", summary),
|
||
responseText: summary,
|
||
};
|
||
}
|
||
default:
|
||
throw new Error(localize(language, {
|
||
zh: `交互运行时暂未实现意图「${request.intent}」。`,
|
||
en: `Intent "${request.intent}" is not implemented in the interaction runtime yet.`,
|
||
}));
|
||
}
|
||
}
|