|
@@ -41,6 +41,71 @@ export async function processTuiAgentInput(params: {
|
|
|
timestamp: userTimestamp,
|
|
timestamp: userTimestamp,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ if (!resolvedBookId && isCreateBookInstruction(params.input)) {
|
|
|
|
|
+ const book = parseBookCreationRequest(params.input);
|
|
|
|
|
+ if (book) {
|
|
|
|
|
+ await pipeline.initBook(book, {
|
|
|
|
|
+ externalContext: params.input,
|
|
|
|
|
+ authorIntent: params.input,
|
|
|
|
|
+ });
|
|
|
|
|
+ const responseText = `已创建《${book.title}》,接下来可以直接输入“写第1章”。`;
|
|
|
|
|
+ nextSession = appendInteractionMessage({
|
|
|
|
|
+ ...nextSession,
|
|
|
|
|
+ activeBookId: book.id,
|
|
|
|
|
+ currentExecution: {
|
|
|
|
|
+ status: "completed",
|
|
|
|
|
+ bookId: book.id,
|
|
|
|
|
+ stageLabel: "architect",
|
|
|
|
|
+ },
|
|
|
|
|
+ }, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content: responseText,
|
|
|
|
|
+ timestamp: userTimestamp + 1,
|
|
|
|
|
+ });
|
|
|
|
|
+ await persistProjectSession(params.projectRoot, nextSession);
|
|
|
|
|
+ return {
|
|
|
|
|
+ responseText,
|
|
|
|
|
+ session: nextSession,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (resolvedBookId && isWriteNextInstruction(params.input)) {
|
|
|
|
|
+ const writeResult = await pipeline.writeNextChapter(resolvedBookId);
|
|
|
|
|
+ const chapterNumber = getResultNumber(writeResult, "chapterNumber");
|
|
|
|
|
+ const title = getResultString(writeResult, "title");
|
|
|
|
|
+ const wordCount = getResultNumber(writeResult, "wordCount");
|
|
|
|
|
+ const status = getResultString(writeResult, "status");
|
|
|
|
|
+ const responseText = [
|
|
|
|
|
+ `已为 ${resolvedBookId} 完成`,
|
|
|
|
|
+ chapterNumber ? `第 ${chapterNumber} 章` : "下一章",
|
|
|
|
|
+ title ? `《${title}》` : "",
|
|
|
|
|
+ wordCount ? `,字数 ${wordCount}` : "",
|
|
|
|
|
+ status ? `,状态 ${status}` : "",
|
|
|
|
|
+ "。",
|
|
|
|
|
+ ].join("");
|
|
|
|
|
+ nextSession = appendInteractionMessage({
|
|
|
|
|
+ ...nextSession,
|
|
|
|
|
+ activeBookId: resolvedBookId,
|
|
|
|
|
+ currentExecution: {
|
|
|
|
|
+ status: "completed",
|
|
|
|
|
+ bookId: resolvedBookId,
|
|
|
|
|
+ ...(chapterNumber ? { chapterNumber } : {}),
|
|
|
|
|
+ stageLabel: "writer",
|
|
|
|
|
+ },
|
|
|
|
|
+ ...(chapterNumber ? { activeChapterNumber: chapterNumber } : {}),
|
|
|
|
|
+ }, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content: responseText,
|
|
|
|
|
+ timestamp: userTimestamp + 1,
|
|
|
|
|
+ });
|
|
|
|
|
+ await persistProjectSession(params.projectRoot, nextSession);
|
|
|
|
|
+ return {
|
|
|
|
|
+ responseText,
|
|
|
|
|
+ session: nextSession,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const result = await runAgentSession(
|
|
const result = await runAgentSession(
|
|
|
{
|
|
{
|
|
|
sessionId: params.session.sessionId,
|
|
sessionId: params.session.sessionId,
|
|
@@ -61,14 +126,17 @@ export async function processTuiAgentInput(params: {
|
|
|
params.input,
|
|
params.input,
|
|
|
initialMessages,
|
|
initialMessages,
|
|
|
);
|
|
);
|
|
|
|
|
+ const createdBookId = extractCreatedBookId(result.messages);
|
|
|
|
|
+ const activeBookId = createdBookId ?? resolvedBookId;
|
|
|
|
|
|
|
|
if (result.responseText?.trim()) {
|
|
if (result.responseText?.trim()) {
|
|
|
const lastAssistant = result.messages.filter((message: any) => message.role === "assistant").pop() as { thinking?: string } | undefined;
|
|
const lastAssistant = result.messages.filter((message: any) => message.role === "assistant").pop() as { thinking?: string } | undefined;
|
|
|
nextSession = appendInteractionMessage({
|
|
nextSession = appendInteractionMessage({
|
|
|
...nextSession,
|
|
...nextSession,
|
|
|
|
|
+ ...(activeBookId ? { activeBookId } : {}),
|
|
|
currentExecution: {
|
|
currentExecution: {
|
|
|
status: "completed",
|
|
status: "completed",
|
|
|
- ...(resolvedBookId ? { bookId: resolvedBookId } : {}),
|
|
|
|
|
|
|
+ ...(activeBookId ? { bookId: activeBookId } : {}),
|
|
|
...(params.session.activeChapterNumber ? { chapterNumber: params.session.activeChapterNumber } : {}),
|
|
...(params.session.activeChapterNumber ? { chapterNumber: params.session.activeChapterNumber } : {}),
|
|
|
stageLabel: "agent",
|
|
stageLabel: "agent",
|
|
|
},
|
|
},
|
|
@@ -81,9 +149,10 @@ export async function processTuiAgentInput(params: {
|
|
|
} else {
|
|
} else {
|
|
|
nextSession = {
|
|
nextSession = {
|
|
|
...nextSession,
|
|
...nextSession,
|
|
|
|
|
+ ...(activeBookId ? { activeBookId } : {}),
|
|
|
currentExecution: {
|
|
currentExecution: {
|
|
|
status: "completed",
|
|
status: "completed",
|
|
|
- ...(resolvedBookId ? { bookId: resolvedBookId } : {}),
|
|
|
|
|
|
|
+ ...(activeBookId ? { bookId: activeBookId } : {}),
|
|
|
...(params.session.activeChapterNumber ? { chapterNumber: params.session.activeChapterNumber } : {}),
|
|
...(params.session.activeChapterNumber ? { chapterNumber: params.session.activeChapterNumber } : {}),
|
|
|
stageLabel: "agent",
|
|
stageLabel: "agent",
|
|
|
},
|
|
},
|
|
@@ -96,3 +165,100 @@ export async function processTuiAgentInput(params: {
|
|
|
session: nextSession,
|
|
session: nextSession,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function isWriteNextInstruction(instruction: string): boolean {
|
|
|
|
|
+ const trimmed = instruction.trim();
|
|
|
|
|
+ return /^(continue|继续|继续写|写下一章|write next|下一章|再来一章)$/i.test(trimmed)
|
|
|
|
|
+ || /^写第\s*\d+\s*章$/i.test(trimmed)
|
|
|
|
|
+ || /(继续写|写下一章|下一章|再来一章|write\s+next)/i.test(trimmed);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isCreateBookInstruction(instruction: string): boolean {
|
|
|
|
|
+ return /(建书|新建书|创建|开书|create\s+(?:a\s+)?book)/i.test(instruction)
|
|
|
|
|
+ && /(?:标题|书名|title)\s*[《"“]/i.test(instruction);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseBookCreationRequest(instruction: string): {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ genre: string;
|
|
|
|
|
+ platform: "tomato" | "feilu" | "qidian" | "other";
|
|
|
|
|
+ language: "zh" | "en";
|
|
|
|
|
+ status: "outlining";
|
|
|
|
|
+ targetChapters: number;
|
|
|
|
|
+ chapterWordCount: number;
|
|
|
|
|
+ createdAt: string;
|
|
|
|
|
+ updatedAt: string;
|
|
|
|
|
+} | undefined {
|
|
|
|
|
+ const title = extractTitle(instruction);
|
|
|
|
|
+ if (!title) return undefined;
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: deriveBookId(title),
|
|
|
|
|
+ title,
|
|
|
|
|
+ genre: inferGenre(instruction),
|
|
|
|
|
+ platform: inferPlatform(instruction),
|
|
|
|
|
+ language: /[\u4e00-\u9fff]/.test(instruction) ? "zh" : "en",
|
|
|
|
|
+ status: "outlining",
|
|
|
|
|
+ targetChapters: extractNumber(instruction, /(\d+)\s*章/) ?? 200,
|
|
|
|
|
+ chapterWordCount: extractNumber(instruction, /(?:每章|章节|章)\D{0,8}(\d{3,5})\s*字/) ?? 3000,
|
|
|
|
|
+ createdAt: now,
|
|
|
|
|
+ updatedAt: now,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractTitle(instruction: string): string | undefined {
|
|
|
|
|
+ const match = instruction.match(/(?:标题|书名|title)\s*[《"“]([^》"”]+)[》"”]/i);
|
|
|
|
|
+ const title = match?.[1]?.trim();
|
|
|
|
|
+ return title && title.length > 0 ? title : undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractNumber(instruction: string, pattern: RegExp): number | undefined {
|
|
|
|
|
+ const value = Number.parseInt(instruction.match(pattern)?.[1] ?? "", 10);
|
|
|
|
|
+ return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function inferPlatform(instruction: string): "tomato" | "feilu" | "qidian" | "other" {
|
|
|
|
|
+ if (/番茄|tomato/i.test(instruction)) return "tomato";
|
|
|
|
|
+ if (/飞卢|feilu/i.test(instruction)) return "feilu";
|
|
|
|
|
+ if (/起点|qidian/i.test(instruction)) return "qidian";
|
|
|
|
|
+ return "other";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function inferGenre(instruction: string): string {
|
|
|
|
|
+ if (/都市/.test(instruction)) return "urban";
|
|
|
|
|
+ if (/悬疑|推理|mystery/i.test(instruction)) return "mystery";
|
|
|
|
|
+ if (/玄幻|xuanhuan/i.test(instruction)) return "xuanhuan";
|
|
|
|
|
+ if (/科幻|sci[-\s]?fi/i.test(instruction)) return "sci-fi";
|
|
|
|
|
+ if (/言情|甜宠|romance/i.test(instruction)) return "romance";
|
|
|
|
|
+ return "other";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function deriveBookId(title: string): string {
|
|
|
|
|
+ return title
|
|
|
|
|
+ .toLowerCase()
|
|
|
|
|
+ .replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
|
|
|
|
|
+ .replace(/-+/g, "-")
|
|
|
|
|
+ .replace(/^-|-$/g, "")
|
|
|
|
|
+ .slice(0, 30) || `book-${Date.now().toString(36)}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractCreatedBookId(messages: ReadonlyArray<unknown>): string | undefined {
|
|
|
|
|
+ for (const message of messages) {
|
|
|
|
|
+ const details = (message as { details?: { kind?: string; bookId?: string } }).details;
|
|
|
|
|
+ if (details?.kind === "book_created" && details.bookId) {
|
|
|
|
|
+ return details.bookId;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getResultString(value: unknown, key: string): string | undefined {
|
|
|
|
|
+ const raw = (value as Record<string, unknown>)[key];
|
|
|
|
|
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getResultNumber(value: unknown, key: string): number | undefined {
|
|
|
|
|
+ const raw = (value as Record<string, unknown>)[key];
|
|
|
|
|
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
|
|
|
+}
|