Skip to content

Commit 690a62f

Browse files
committed
fix(create): normalize book platform aliases
1 parent 5072000 commit 690a62f

14 files changed

Lines changed: 124 additions & 85 deletions

File tree

packages/cli/src/commands/book.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Command } from "commander";
22
import { access, readFile, rm } from "node:fs/promises";
33
import { createInterface } from "node:readline";
44
import { join, resolve } from "node:path";
5-
import { PipelineRunner, StateManager, type BookConfig } from "@actalk/inkos-core";
5+
import { deriveBookIdFromTitle, normalizePlatformOrOther, PipelineRunner, StateManager, type BookConfig } from "@actalk/inkos-core";
66
import {
77
formatBookCreateCreated,
88
formatBookCreateCreating,
@@ -31,11 +31,7 @@ bookCommand
3131
try {
3232
const root = findProjectRoot();
3333

34-
const bookId = opts.title
35-
.toLowerCase()
36-
.replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
37-
.replace(/-+/g, "-")
38-
.slice(0, 30);
34+
const bookId = deriveBookIdFromTitle(opts.title) || `book-${Date.now().toString(36)}`;
3935

4036
const bookDir = join(root, "books", bookId);
4137
try {
@@ -55,7 +51,7 @@ bookCommand
5551
const book: BookConfig = {
5652
id: bookId,
5753
title: opts.title,
58-
platform: opts.platform,
54+
platform: normalizePlatformOrOther(opts.platform),
5955
genre: opts.genre,
6056
status: "outlining",
6157
targetChapters: parseInt(opts.targetChapters, 10),

packages/cli/src/commands/fanfic.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "commander";
22
import { readFile, readdir, stat } from "node:fs/promises";
33
import { join, resolve, basename } from "node:path";
4-
import { PipelineRunner, type BookConfig, type FanficMode } from "@actalk/inkos-core";
4+
import { deriveBookIdFromTitle, normalizePlatformOrOther, PipelineRunner, type BookConfig, type FanficMode } from "@actalk/inkos-core";
55
import { loadConfig, buildPipelineConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
66

77
export const fanficCommand = new Command("fanfic")
@@ -38,17 +38,13 @@ fanficCommand
3838
throw new Error(`源素材文件内容过短(${sourceText.length} 字符)。请提供至少 100 字符的原作素材。`);
3939
}
4040

41-
const bookId = opts.title
42-
.toLowerCase()
43-
.replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
44-
.replace(/-+/g, "-")
45-
.slice(0, 30);
41+
const bookId = deriveBookIdFromTitle(opts.title) || `book-${Date.now().toString(36)}`;
4642

4743
const now = new Date().toISOString();
4844
const book: BookConfig = {
4945
id: bookId,
5046
title: opts.title,
51-
platform: opts.platform,
47+
platform: normalizePlatformOrOther(opts.platform),
5248
genre: opts.genre,
5349
status: "outlining",
5450
targetChapters: parseInt(opts.targetChapters, 10),

packages/core/src/__tests__/interaction-tools.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,37 @@ describe("interaction tools", () => {
297297
);
298298
});
299299

300+
it("normalizes human-facing platform aliases before creating a book", async () => {
301+
const pipeline = {
302+
initBook: vi.fn(async () => undefined),
303+
writeNextChapter: vi.fn(),
304+
reviseDraft: vi.fn(),
305+
};
306+
const state = {
307+
ensureControlDocuments: vi.fn(async () => {}),
308+
bookDir: vi.fn((bookId: string) => join(projectRoot, "books", bookId)),
309+
loadBookConfig: vi.fn(),
310+
loadChapterIndex: vi.fn(async () => []),
311+
saveChapterIndex: vi.fn(async () => undefined),
312+
listBooks: vi.fn(async () => []),
313+
};
314+
315+
const tools = createInteractionToolsFromDeps(pipeline, state);
316+
await tools.createBook?.({
317+
title: "测试书",
318+
genre: "urban",
319+
platform: "番茄小说",
320+
});
321+
322+
expect(pipeline.initBook).toHaveBeenCalledWith(
323+
expect.objectContaining({
324+
id: "测试书",
325+
platform: "tomato",
326+
}),
327+
expect.any(Object),
328+
);
329+
});
330+
300331
it("builds a reusable chapter lookup from a single directory listing", () => {
301332
const lookup = buildChapterFileLookup([
302333
"0001_First.md",

packages/core/src/__tests__/models.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
PlatformSchema,
55
GenreSchema,
66
BookStatusSchema,
7+
normalizePlatformId,
8+
normalizePlatformOrOther,
79
} from "../models/book.js";
810
import { ChapterMetaSchema, ChapterStatusSchema } from "../models/chapter.js";
911
import {
@@ -136,6 +138,18 @@ describe("PlatformSchema", () => {
136138
it("rejects unknown platform", () => {
137139
expect(() => PlatformSchema.parse("amazon")).toThrow();
138140
});
141+
142+
it("normalizes platform ids and human-facing aliases", () => {
143+
expect(normalizePlatformId("tomato")).toBe("tomato");
144+
expect(normalizePlatformId("番茄小说")).toBe("tomato");
145+
expect(normalizePlatformId("fanqie-novel")).toBe("tomato");
146+
expect(normalizePlatformId("起点中文网")).toBe("qidian");
147+
expect(normalizePlatformId("飞卢")).toBe("feilu");
148+
expect(normalizePlatformId("royal-road")).toBe("other");
149+
expect(normalizePlatformId("Kindle Unlimited")).toBe("other");
150+
expect(normalizePlatformId("")).toBeUndefined();
151+
expect(normalizePlatformOrOther("")).toBe("other");
152+
});
139153
});
140154

141155
describe("GenreSchema", () => {

packages/core/src/__tests__/pipeline-agent.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,29 @@ describe("agent pipeline tools", () => {
164164
.resolves.toContain("mentor fallout");
165165
});
166166

167+
it("normalizes human-facing platform aliases before create_book persists config", async () => {
168+
const initBook = vi.spyOn(PipelineRunner.prototype, "initBook").mockResolvedValue(undefined);
169+
170+
const result = JSON.parse(await executeAgentTool(
171+
pipeline,
172+
state,
173+
config,
174+
"create_book",
175+
{
176+
title: "测试书",
177+
genre: "urban",
178+
platform: "番茄小说",
179+
brief: "一本文娱爽文。",
180+
},
181+
));
182+
183+
expect(result).toMatchObject({ bookId: "测试书", title: "测试书", status: "created" });
184+
expect(initBook).toHaveBeenCalledWith(expect.objectContaining({
185+
id: "测试书",
186+
platform: "tomato",
187+
}));
188+
});
189+
167190
it("keeps update_current_focus usable for explicit local overrides through the tool surface", async () => {
168191
await executeAgentTool(pipeline, state, config, "update_current_focus", {
169192
bookId,

packages/core/src/agent/agent-tools.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { assertSafeTruthFileName, createInteractionToolsFromDeps } from "../inte
99
import { writeExportArtifact } from "../interaction/export-artifact.js";
1010
import { assertSafeBookId, deriveBookIdFromTitle } from "../utils/book-id.js";
1111
import { safeChildPath } from "../utils/path-safety.js";
12+
import { normalizePlatformId, normalizePlatformOrOther } from "../models/book.js";
1213

1314
// ---------------------------------------------------------------------------
1415
// Helpers
@@ -105,36 +106,6 @@ const SubAgentParams = Type.Object({
105106
});
106107

107108
type SubAgentParamsType = Static<typeof SubAgentParams>;
108-
type ArchitectPlatform = NonNullable<SubAgentParamsType["platform"]>;
109-
110-
function normalizeArchitectPlatform(platform: unknown): ArchitectPlatform | undefined {
111-
if (typeof platform !== "string") {
112-
return undefined;
113-
}
114-
115-
const raw = platform.trim();
116-
if (!raw) {
117-
return undefined;
118-
}
119-
120-
const lowered = raw.toLowerCase();
121-
const compact = lowered.replace(/[\s_-]+/g, "");
122-
123-
if (compact === "tomato" || compact === "fanqie" || compact === "fanqienovel" || raw.includes("番茄")) {
124-
return "tomato";
125-
}
126-
if (compact === "qidian" || compact === "qidianzhongwenwang" || raw.includes("起点")) {
127-
return "qidian";
128-
}
129-
if (compact === "feilu" || raw.includes("飞卢")) {
130-
return "feilu";
131-
}
132-
if (compact === "other" || compact === "others" || raw.includes("其他") || raw.includes("其它")) {
133-
return "other";
134-
}
135-
136-
return "other";
137-
}
138109

139110
function prepareSubAgentArguments(args: unknown): SubAgentParamsType {
140111
if (!args || typeof args !== "object" || Array.isArray(args)) {
@@ -143,7 +114,7 @@ function prepareSubAgentArguments(args: unknown): SubAgentParamsType {
143114

144115
const prepared = { ...(args as Record<string, unknown>) };
145116
if ("platform" in prepared) {
146-
const platform = normalizeArchitectPlatform(prepared.platform);
117+
const platform = normalizePlatformId(prepared.platform);
147118
if (platform) {
148119
prepared.platform = platform;
149120
} else {
@@ -215,7 +186,7 @@ export function createSubAgentTool(
215186
id,
216187
title: resolvedTitle,
217188
genre: genre ?? "general",
218-
platform: normalizeArchitectPlatform(platform) ?? "other",
189+
platform: normalizePlatformOrOther(platform),
219190
language: (language ?? "zh") as any,
220191
status: "outlining" as any,
221192
targetChapters: targetChapters ?? 200,

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Models
2-
export { type BookConfig, type Platform, type Genre, type BookStatus, type FanficMode, BookConfigSchema, PlatformSchema, GenreSchema, BookStatusSchema, FanficModeSchema } from "./models/book.js";
2+
export { type BookConfig, type Platform, type Genre, type BookStatus, type FanficMode, BookConfigSchema, PlatformSchema, GenreSchema, BookStatusSchema, FanficModeSchema, normalizePlatformId, normalizePlatformOrOther } from "./models/book.js";
33
export { type ChapterMeta, type ChapterStatus, ChapterMetaSchema, ChapterStatusSchema } from "./models/chapter.js";
44
export { type ProjectConfig, type LLMConfig, type NotifyChannel, type DetectionConfig, type QualityGates, type FoundationConfig, type AgentLLMOverride, type InputGovernanceMode, ProjectConfigSchema, LLMConfigSchema, AgentLLMOverrideSchema, DetectionConfigSchema, QualityGatesSchema, FoundationConfigSchema, InputGovernanceModeSchema } from "./models/project.js";
55
export { type CurrentState, type ParticleLedger, type PendingHooks, type PendingHook, type LedgerEntry } from "./models/state.js";

packages/core/src/interaction/project-tools.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
ReviseMode,
99
LLMClient,
1010
BookConfig,
11-
Platform,
1211
ToolDefinition,
1312
} from "../index.js";
1413
import { chatCompletion, chatWithTools } from "../index.js";
@@ -17,6 +16,8 @@ import type { InteractionRuntimeTools } from "./runtime.js";
1716
import type { BookCreationDraft } from "./session.js";
1817
import { writeExportArtifact } from "./export-artifact.js";
1918
import { safeChildPath } from "../utils/path-safety.js";
19+
import { deriveBookIdFromTitle } from "../utils/book-id.js";
20+
import { normalizePlatformOrOther } from "../models/book.js";
2021

2122
const SAFE_TRUTH_FLAT_FILE_NAMES = new Set([
2223
"author_intent.md",
@@ -83,25 +84,6 @@ type InstrumentablePipelineLike = PipelineLike & {
8384
};
8485
};
8586

86-
function normalizePlatform(platform?: string): Platform {
87-
switch (platform) {
88-
case "tomato":
89-
case "feilu":
90-
case "qidian":
91-
return platform;
92-
default:
93-
return "other";
94-
}
95-
}
96-
97-
function deriveBookId(title: string): string {
98-
return title
99-
.toLowerCase()
100-
.replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
101-
.replace(/-+/g, "-")
102-
.slice(0, 30);
103-
}
104-
10587
function buildBookConfig(input: {
10688
readonly title: string;
10789
readonly genre?: string;
@@ -112,9 +94,9 @@ function buildBookConfig(input: {
11294
}): BookConfig {
11395
const now = new Date().toISOString();
11496
return {
115-
id: deriveBookId(input.title),
97+
id: deriveBookIdFromTitle(input.title) || `book-${Date.now().toString(36)}`,
11698
title: input.title,
117-
platform: normalizePlatform(input.platform),
99+
platform: normalizePlatformOrOther(input.platform),
118100
genre: input.genre ?? "other",
119101
status: "outlining",
120102
targetChapters: input.targetChapters ?? 200,

packages/core/src/models/book.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,39 @@ import { z } from "zod";
33
export const PlatformSchema = z.enum(["tomato", "feilu", "qidian", "other"]);
44
export type Platform = z.infer<typeof PlatformSchema>;
55

6+
export function normalizePlatformId(platform: unknown): Platform | undefined {
7+
if (typeof platform !== "string") {
8+
return undefined;
9+
}
10+
11+
const raw = platform.trim();
12+
if (!raw) {
13+
return undefined;
14+
}
15+
16+
const lowered = raw.toLowerCase();
17+
const compact = lowered.replace(/[\s_-]+/g, "");
18+
19+
if (compact === "tomato" || compact === "fanqie" || compact === "fanqienovel" || raw.includes("番茄")) {
20+
return "tomato";
21+
}
22+
if (compact === "qidian" || compact === "qidianzhongwenwang" || raw.includes("起点")) {
23+
return "qidian";
24+
}
25+
if (compact === "feilu" || raw.includes("飞卢")) {
26+
return "feilu";
27+
}
28+
if (compact === "other" || compact === "others" || raw.includes("其他") || raw.includes("其它")) {
29+
return "other";
30+
}
31+
32+
return "other";
33+
}
34+
35+
export function normalizePlatformOrOther(platform: unknown): Platform {
36+
return normalizePlatformId(platform) ?? "other";
37+
}
38+
639
export const GenreSchema = z.string().min(1);
740
export type Genre = z.infer<typeof GenreSchema>;
841

packages/core/src/pipeline/agent.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { chatWithTools, type AgentMessage, type ToolDefinition } from "../llm/provider.js";
22
import { PipelineRunner, type PipelineConfig } from "./runner.js";
3-
import type { Platform, Genre } from "../models/book.js";
3+
import { normalizePlatformOrOther, type Genre } from "../models/book.js";
44
import { DEFAULT_REVISE_MODE, type ReviseMode } from "../agents/reviser.js";
5+
import { deriveBookIdFromTitle } from "../utils/book-id.js";
56

67
/** Tool definitions for the agent loop. */
78
const TOOLS: ReadonlyArray<ToolDefinition> = [
@@ -420,16 +421,12 @@ export async function executeAgentTool(
420421
case "create_book": {
421422
const now = new Date().toISOString();
422423
const title = args.title as string;
423-
const bookId = title
424-
.toLowerCase()
425-
.replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
426-
.replace(/-+/g, "-")
427-
.slice(0, 30);
424+
const bookId = deriveBookIdFromTitle(title) || `book-${Date.now().toString(36)}`;
428425

429426
const book = {
430427
id: bookId,
431428
title,
432-
platform: ((args.platform as string) ?? "tomato") as Platform,
429+
platform: normalizePlatformOrOther(args.platform ?? "tomato"),
433430
genre: ((args.genre as string) ?? "xuanhuan") as Genre,
434431
status: "outlining" as const,
435432
targetChapters: 200,

0 commit comments

Comments
 (0)