@actalk/inkos-studio 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/dist/api/errors.d.ts +0 -1
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +0 -3
- package/dist/api/errors.js.map +1 -1
- package/dist/api/safety.d.ts +0 -28
- package/dist/api/safety.d.ts.map +1 -1
- package/dist/api/safety.js +1 -53
- package/dist/api/safety.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +118 -76
- package/dist/api/server.js.map +1 -1
- package/dist/assets/index-CrV75dEH.css +1 -0
- package/dist/assets/index-tV8pf10O.js +395 -0
- package/dist/index.html +2 -2
- package/package.json +4 -9
- package/dist/assets/index-BIif-YIZ.css +0 -1
- package/dist/assets/index-DW3qJzCW.js +0 -395
package/dist/api/errors.d.ts
CHANGED
package/dist/api/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D"}
|
package/dist/api/errors.js
CHANGED
package/dist/api/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
|
package/dist/api/safety.d.ts
CHANGED
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security validation utilities for Studio API.
|
|
3
|
-
* Ported from PR #97 (PapainTea) — ReDoS prevention, path traversal, port parsing.
|
|
4
|
-
*/
|
|
5
|
-
/** Validates fileId against whitelist to prevent path traversal. */
|
|
6
|
-
export declare function isSafeUploadFileId(fileId: string): boolean;
|
|
7
1
|
/** Validates bookId — blocks traversal sequences and null bytes. */
|
|
8
2
|
export declare function isSafeBookId(bookId: string): boolean;
|
|
9
|
-
/**
|
|
10
|
-
* Builds a regex from user-provided pattern with ReDoS prevention.
|
|
11
|
-
* Blocks grouping, alternation, backreferences, and lookahead/behind.
|
|
12
|
-
*/
|
|
13
|
-
export declare function buildImportRegex(pattern: string): RegExp;
|
|
14
|
-
/** Strips filesystem paths from upload responses — only returns relative/safe fields. */
|
|
15
|
-
export declare function createUploadResponse(input: {
|
|
16
|
-
readonly fileId: string;
|
|
17
|
-
readonly size: number;
|
|
18
|
-
readonly chapterCount: number;
|
|
19
|
-
readonly firstTitle: string;
|
|
20
|
-
readonly totalChars: number;
|
|
21
|
-
}): {
|
|
22
|
-
readonly ok: true;
|
|
23
|
-
readonly fileId: string;
|
|
24
|
-
readonly size: number;
|
|
25
|
-
readonly chapterCount: number;
|
|
26
|
-
readonly firstTitle: string;
|
|
27
|
-
readonly totalChars: number;
|
|
28
|
-
};
|
|
29
|
-
/** Parses port from environment with safe fallback. */
|
|
30
|
-
export declare function resolveServerPort(env?: Record<string, string | undefined>): number;
|
|
31
3
|
//# sourceMappingURL=safety.d.ts.map
|
package/dist/api/safety.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUpD"}
|
package/dist/api/safety.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Security validation utilities for Studio API.
|
|
3
|
-
* Ported from PR #97 (PapainTea) — ReDoS prevention, path traversal, port parsing.
|
|
4
|
-
*/
|
|
5
|
-
const SAFE_UPLOAD_FILE_ID = /^[A-Za-z0-9-]{1,64}$/;
|
|
6
|
-
const MAX_IMPORT_PATTERN_LENGTH = 160;
|
|
7
|
-
const SAFE_IMPORT_PATTERN = /^[\p{L}\p{N}\s[\]\\.^$*+?{}\-,。::、_]+$/u;
|
|
8
|
-
/** Validates fileId against whitelist to prevent path traversal. */
|
|
9
|
-
export function isSafeUploadFileId(fileId) {
|
|
10
|
-
return typeof fileId === "string" && SAFE_UPLOAD_FILE_ID.test(fileId);
|
|
11
|
-
}
|
|
1
|
+
// Validates book IDs to prevent path traversal in API requests.
|
|
12
2
|
/** Validates bookId — blocks traversal sequences and null bytes. */
|
|
13
3
|
export function isSafeBookId(bookId) {
|
|
14
4
|
return (typeof bookId === "string"
|
|
@@ -19,46 +9,4 @@ export function isSafeBookId(bookId) {
|
|
|
19
9
|
&& !bookId.includes("..")
|
|
20
10
|
&& !/[\\/\0]/.test(bookId));
|
|
21
11
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Builds a regex from user-provided pattern with ReDoS prevention.
|
|
24
|
-
* Blocks grouping, alternation, backreferences, and lookahead/behind.
|
|
25
|
-
*/
|
|
26
|
-
export function buildImportRegex(pattern) {
|
|
27
|
-
const normalized = String(pattern ?? "").trim();
|
|
28
|
-
if (!normalized) {
|
|
29
|
-
throw new Error("Import pattern is required");
|
|
30
|
-
}
|
|
31
|
-
if (normalized.length > MAX_IMPORT_PATTERN_LENGTH) {
|
|
32
|
-
throw new Error(`Import pattern too long (max ${MAX_IMPORT_PATTERN_LENGTH} chars)`);
|
|
33
|
-
}
|
|
34
|
-
if (/[()|]/.test(normalized) || /\\[1-9]/.test(normalized) || normalized.includes("(?")) {
|
|
35
|
-
throw new Error("Import pattern uses unsafe regex features");
|
|
36
|
-
}
|
|
37
|
-
if (!SAFE_IMPORT_PATTERN.test(normalized)) {
|
|
38
|
-
throw new Error("Import pattern is invalid");
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
return new RegExp(normalized, "g");
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
throw new Error("Import pattern is invalid");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** Strips filesystem paths from upload responses — only returns relative/safe fields. */
|
|
48
|
-
export function createUploadResponse(input) {
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
fileId: input.fileId,
|
|
52
|
-
size: input.size,
|
|
53
|
-
chapterCount: input.chapterCount,
|
|
54
|
-
firstTitle: input.firstTitle,
|
|
55
|
-
totalChars: input.totalChars,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/** Parses port from environment with safe fallback. */
|
|
59
|
-
export function resolveServerPort(env = process.env) {
|
|
60
|
-
const raw = env.INKOS_STUDIO_PORT ?? env.PORT ?? "4567";
|
|
61
|
-
const parsed = Number.parseInt(raw, 10);
|
|
62
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4567;
|
|
63
|
-
}
|
|
64
12
|
//# sourceMappingURL=safety.js.map
|
package/dist/api/safety.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/api/safety.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;WACvB,MAAM,CAAC,MAAM,GAAG,CAAC;WACjB,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;WACxB,MAAM,KAAK,GAAG;WACd,MAAM,KAAK,IAAI;WACf,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;WACtB,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAC3B,CAAC;AACJ,CAAC"}
|
package/dist/api/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAaL,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAqB5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EAswC5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
|
package/dist/api/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import { streamSSE } from "hono/streaming";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
|
-
import { StateManager, PipelineRunner, createLLMClient, createLogger, computeAnalytics, loadProjectConfig, } from "@actalk/inkos-core";
|
|
5
|
+
import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionInput, processProjectInteractionRequest, resolveSessionActiveBook, } from "@actalk/inkos-core";
|
|
6
6
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { isSafeBookId } from "./safety.js";
|
|
@@ -54,7 +54,7 @@ export function createStudioServer(initialConfig, root) {
|
|
|
54
54
|
cachedConfig = freshConfig;
|
|
55
55
|
return freshConfig;
|
|
56
56
|
}
|
|
57
|
-
async function buildPipelineConfig() {
|
|
57
|
+
async function buildPipelineConfig(overrides) {
|
|
58
58
|
const currentConfig = await loadCurrentProjectConfig();
|
|
59
59
|
const logger = createLogger({ tag: "studio", sinks: [sseSink] });
|
|
60
60
|
return {
|
|
@@ -74,6 +74,7 @@ export function createStudioServer(initialConfig, root) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
externalContext: overrides?.externalContext,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
80
|
// --- Books ---
|
|
@@ -131,9 +132,23 @@ export function createStudioServer(initialConfig, root) {
|
|
|
131
132
|
broadcast("book:creating", { bookId, title: body.title });
|
|
132
133
|
bookCreateStatus.set(bookId, { status: "creating" });
|
|
133
134
|
const pipeline = new PipelineRunner(await buildPipelineConfig());
|
|
134
|
-
pipeline
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
const tools = createInteractionToolsFromDeps(pipeline, state);
|
|
136
|
+
processProjectInteractionRequest({
|
|
137
|
+
projectRoot: root,
|
|
138
|
+
request: {
|
|
139
|
+
intent: "create_book",
|
|
140
|
+
title: body.title,
|
|
141
|
+
genre: body.genre,
|
|
142
|
+
language: body.language === "en" ? "en" : body.language === "zh" ? "zh" : undefined,
|
|
143
|
+
platform: body.platform,
|
|
144
|
+
chapterWordCount: body.chapterWordCount,
|
|
145
|
+
targetChapters: body.targetChapters,
|
|
146
|
+
},
|
|
147
|
+
tools,
|
|
148
|
+
}).then((result) => {
|
|
149
|
+
const createdBookId = result.details?.bookId ?? result.session.activeBookId ?? bookId;
|
|
150
|
+
bookCreateStatus.delete(createdBookId);
|
|
151
|
+
broadcast("book:created", { bookId: createdBookId });
|
|
137
152
|
}, (e) => {
|
|
138
153
|
const error = e instanceof Error ? e.message : String(e);
|
|
139
154
|
bookCreateStatus.set(bookId, { status: "error", error });
|
|
@@ -266,9 +281,19 @@ export function createStudioServer(initialConfig, root) {
|
|
|
266
281
|
const num = parseInt(c.req.param("num"), 10);
|
|
267
282
|
try {
|
|
268
283
|
const index = await state.loadChapterIndex(id);
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
const target = index.find((ch) => ch.number === num);
|
|
285
|
+
if (!target) {
|
|
286
|
+
return c.json({ error: `Chapter ${num} not found` }, 404);
|
|
287
|
+
}
|
|
288
|
+
const rollbackTarget = num - 1;
|
|
289
|
+
const discarded = await state.rollbackToChapter(id, rollbackTarget);
|
|
290
|
+
return c.json({
|
|
291
|
+
ok: true,
|
|
292
|
+
chapterNumber: num,
|
|
293
|
+
status: "rejected",
|
|
294
|
+
rolledBackTo: rollbackTarget,
|
|
295
|
+
discarded,
|
|
296
|
+
});
|
|
272
297
|
}
|
|
273
298
|
catch (e) {
|
|
274
299
|
return c.json({ error: String(e) }, 500);
|
|
@@ -434,22 +459,44 @@ export function createStudioServer(initialConfig, root) {
|
|
|
434
459
|
}
|
|
435
460
|
});
|
|
436
461
|
// --- Agent chat ---
|
|
462
|
+
app.get("/api/interaction/session", async (c) => {
|
|
463
|
+
const session = await loadProjectSession(root);
|
|
464
|
+
const activeBookId = await resolveSessionActiveBook(root, session);
|
|
465
|
+
return c.json({
|
|
466
|
+
session: activeBookId && session.activeBookId !== activeBookId
|
|
467
|
+
? { ...session, activeBookId }
|
|
468
|
+
: session,
|
|
469
|
+
activeBookId,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
437
472
|
app.post("/api/agent", async (c) => {
|
|
438
|
-
const { instruction } = await c.req.json();
|
|
473
|
+
const { instruction, activeBookId } = await c.req.json();
|
|
439
474
|
if (!instruction?.trim()) {
|
|
440
475
|
return c.json({ error: "No instruction provided" }, 400);
|
|
441
476
|
}
|
|
442
|
-
broadcast("agent:start", { instruction });
|
|
477
|
+
broadcast("agent:start", { instruction, activeBookId });
|
|
443
478
|
try {
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
479
|
+
const pipeline = new PipelineRunner(await buildPipelineConfig());
|
|
480
|
+
const tools = createInteractionToolsFromDeps(pipeline, state);
|
|
481
|
+
const result = await processProjectInteractionInput({
|
|
482
|
+
projectRoot: root,
|
|
483
|
+
input: instruction,
|
|
484
|
+
tools,
|
|
485
|
+
activeBookId,
|
|
486
|
+
});
|
|
487
|
+
const response = result.responseText ?? "Acknowledged.";
|
|
488
|
+
broadcast("agent:complete", { instruction, activeBookId, response });
|
|
489
|
+
return c.json({ response, session: result.session, request: result.request });
|
|
448
490
|
}
|
|
449
491
|
catch (e) {
|
|
450
492
|
const msg = e instanceof Error ? e.message : String(e);
|
|
451
|
-
broadcast("agent:error", { instruction, error: msg });
|
|
452
|
-
return c.json({
|
|
493
|
+
broadcast("agent:error", { instruction, activeBookId, error: msg });
|
|
494
|
+
return c.json({
|
|
495
|
+
error: {
|
|
496
|
+
code: "INTERACTION_ERROR",
|
|
497
|
+
message: msg,
|
|
498
|
+
},
|
|
499
|
+
}, 500);
|
|
453
500
|
}
|
|
454
501
|
});
|
|
455
502
|
// --- Language setup ---
|
|
@@ -505,7 +552,9 @@ export function createStudioServer(initialConfig, root) {
|
|
|
505
552
|
const id = c.req.param("id");
|
|
506
553
|
const chapterNum = parseInt(c.req.param("chapter"), 10);
|
|
507
554
|
const bookDir = state.bookDir(id);
|
|
508
|
-
const body = await c.req
|
|
555
|
+
const body = await c.req
|
|
556
|
+
.json()
|
|
557
|
+
.catch(() => ({ mode: "spot-fix", brief: undefined }));
|
|
509
558
|
broadcast("revise:start", { bookId: id, chapter: chapterNum });
|
|
510
559
|
try {
|
|
511
560
|
const book = await state.loadBookConfig(id);
|
|
@@ -515,25 +564,11 @@ export function createStudioServer(initialConfig, root) {
|
|
|
515
564
|
const match = files.find((f) => f.startsWith(paddedNum) && f.endsWith(".md"));
|
|
516
565
|
if (!match)
|
|
517
566
|
return c.json({ error: "Chapter not found" }, 404);
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
const index = await state.loadChapterIndex(id);
|
|
521
|
-
const chapterMeta = index.find((ch) => ch.number === chapterNum);
|
|
522
|
-
const issues = (chapterMeta?.auditIssues ?? []).map((desc) => ({
|
|
523
|
-
severity: "warning",
|
|
524
|
-
category: "general",
|
|
525
|
-
description: desc,
|
|
526
|
-
suggestion: "",
|
|
567
|
+
const pipeline = new PipelineRunner(await buildPipelineConfig({
|
|
568
|
+
externalContext: body.brief,
|
|
527
569
|
}));
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const reviser = new ReviserAgent({
|
|
531
|
-
client: createLLMClient(currentConfig.llm),
|
|
532
|
-
model: currentConfig.llm.model,
|
|
533
|
-
projectRoot: root,
|
|
534
|
-
bookId: id,
|
|
535
|
-
});
|
|
536
|
-
const result = await reviser.reviseChapter(bookDir, content, chapterNum, issues, (body.mode ?? "spot-fix"), book.genre);
|
|
570
|
+
const normalizedMode = body.mode ?? "spot-fix";
|
|
571
|
+
const result = await pipeline.reviseDraft(id, chapterNum, normalizedMode);
|
|
537
572
|
broadcast("revise:complete", { bookId: id, chapter: chapterNum });
|
|
538
573
|
return c.json(result);
|
|
539
574
|
}
|
|
@@ -602,43 +637,30 @@ export function createStudioServer(initialConfig, root) {
|
|
|
602
637
|
app.post("/api/books/:id/export-save", async (c) => {
|
|
603
638
|
const id = c.req.param("id");
|
|
604
639
|
const { format, approvedOnly } = await c.req.json().catch(() => ({ format: "txt", approvedOnly: false }));
|
|
605
|
-
const bookDir = state.bookDir(id);
|
|
606
|
-
const chaptersDir = join(bookDir, "chapters");
|
|
607
640
|
const fmt = format ?? "txt";
|
|
608
641
|
try {
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const toc = chapters.map((ch, i) => `<li><a href="#ch${i}">${ch.title}</a></li>`).join("\n");
|
|
632
|
-
const chapterHtml = chapters.map((ch, i) => `<h2 id="ch${i}">${ch.title}</h2>\n${ch.html}`).join("\n<hr/>\n");
|
|
633
|
-
body = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${book.title}</title><style>body{font-family:serif;max-width:40em;margin:auto;padding:2em;line-height:1.8}h2{margin-top:3em}</style></head><body><h1>${book.title}</h1><nav><ol>${toc}</ol></nav><hr/>${chapterHtml}</body></html>`;
|
|
634
|
-
outputPath = join(bookDir, `${id}.html`);
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
body = contents.join("\n\n");
|
|
638
|
-
outputPath = join(bookDir, `${id}.txt`);
|
|
639
|
-
}
|
|
640
|
-
await writeFileFs(outputPath, body, "utf-8");
|
|
641
|
-
return c.json({ ok: true, path: outputPath, format: fmt, chapters: filteredFiles.length });
|
|
642
|
+
const pipeline = new PipelineRunner(await buildPipelineConfig());
|
|
643
|
+
const tools = createInteractionToolsFromDeps(pipeline, state);
|
|
644
|
+
const bookDir = state.bookDir(id);
|
|
645
|
+
const outputPath = join(bookDir, `${id}.${fmt === "epub" ? "epub" : fmt}`);
|
|
646
|
+
const result = await processProjectInteractionRequest({
|
|
647
|
+
projectRoot: root,
|
|
648
|
+
request: {
|
|
649
|
+
intent: "export_book",
|
|
650
|
+
bookId: id,
|
|
651
|
+
format: fmt,
|
|
652
|
+
approvedOnly,
|
|
653
|
+
outputPath,
|
|
654
|
+
},
|
|
655
|
+
tools,
|
|
656
|
+
activeBookId: id,
|
|
657
|
+
});
|
|
658
|
+
return c.json({
|
|
659
|
+
ok: true,
|
|
660
|
+
path: result.details?.outputPath ?? outputPath,
|
|
661
|
+
format: fmt,
|
|
662
|
+
chapters: result.details?.chaptersExported ?? 0,
|
|
663
|
+
});
|
|
642
664
|
}
|
|
643
665
|
catch (e) {
|
|
644
666
|
return c.json({ error: String(e) }, 500);
|
|
@@ -779,21 +801,41 @@ export function createStudioServer(initialConfig, root) {
|
|
|
779
801
|
app.post("/api/books/:id/rewrite/:chapter", async (c) => {
|
|
780
802
|
const id = c.req.param("id");
|
|
781
803
|
const chapterNum = parseInt(c.req.param("chapter"), 10);
|
|
804
|
+
const body = await c.req
|
|
805
|
+
.json()
|
|
806
|
+
.catch(() => ({}));
|
|
782
807
|
broadcast("rewrite:start", { bookId: id, chapter: chapterNum });
|
|
783
808
|
try {
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
809
|
+
const rollbackTarget = chapterNum - 1;
|
|
810
|
+
const discarded = await state.rollbackToChapter(id, rollbackTarget);
|
|
811
|
+
const pipeline = new PipelineRunner(await buildPipelineConfig({
|
|
812
|
+
externalContext: body.brief,
|
|
813
|
+
}));
|
|
789
814
|
pipeline.writeNextChapter(id).then((result) => broadcast("rewrite:complete", { bookId: id, chapterNumber: result.chapterNumber, title: result.title, wordCount: result.wordCount }), (e) => broadcast("rewrite:error", { bookId: id, error: e instanceof Error ? e.message : String(e) }));
|
|
790
|
-
return c.json({ status: "rewriting", bookId: id, chapter: chapterNum });
|
|
815
|
+
return c.json({ status: "rewriting", bookId: id, chapter: chapterNum, rolledBackTo: rollbackTarget, discarded });
|
|
791
816
|
}
|
|
792
817
|
catch (e) {
|
|
793
818
|
broadcast("rewrite:error", { bookId: id, error: String(e) });
|
|
794
819
|
return c.json({ error: String(e) }, 500);
|
|
795
820
|
}
|
|
796
821
|
});
|
|
822
|
+
app.post("/api/books/:id/resync/:chapter", async (c) => {
|
|
823
|
+
const id = c.req.param("id");
|
|
824
|
+
const chapterNum = parseInt(c.req.param("chapter"), 10);
|
|
825
|
+
const body = await c.req
|
|
826
|
+
.json()
|
|
827
|
+
.catch(() => ({}));
|
|
828
|
+
try {
|
|
829
|
+
const pipeline = new PipelineRunner(await buildPipelineConfig({
|
|
830
|
+
externalContext: body.brief,
|
|
831
|
+
}));
|
|
832
|
+
const result = await pipeline.resyncChapterArtifacts(id, chapterNum);
|
|
833
|
+
return c.json(result);
|
|
834
|
+
}
|
|
835
|
+
catch (e) {
|
|
836
|
+
return c.json({ error: String(e) }, 500);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
797
839
|
// --- Detect All chapters ---
|
|
798
840
|
app.post("/api/books/:id/detect-all", async (c) => {
|
|
799
841
|
const id = c.req.param("id");
|