@musnows/scriverse 0.5.5 → 0.5.7

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/dist/app.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import express from "express";
2
+ import JSZip from "jszip";
2
3
  import multer from "multer";
3
4
  import mammoth from "mammoth";
4
5
  import { randomUUID } from "node:crypto";
@@ -6,6 +7,7 @@ import { dirname, extname, join } from "node:path";
6
7
  import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from "node:fs";
7
8
  import { rm } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
10
+ import { pipeline } from "node:stream/promises";
9
11
  import { z, ZodError } from "zod";
10
12
  import { AttachmentStorage } from "./attachment-storage.js";
11
13
  import { AiManager } from "./ai.js";
@@ -993,6 +995,12 @@ export function createRuntime(options) {
993
995
  const input = parse(z.object({ volumeId: identifier, title: nonEmpty.max(300), content: z.string().max(2_000_000).optional(), chapterType: chapterTypeSchema.optional() }), request.body);
994
996
  data(response, store.createChapter(request.params.workId, input), 201);
995
997
  });
998
+ app.get("/api/works/:workId/deleted-chapters", (request, response) => {
999
+ const pagination = parsePagination(request.query);
1000
+ data(response, pagination
1001
+ ? store.listDeletedChaptersPage(request.params.workId, pagination)
1002
+ : store.listDeletedChapters(request.params.workId));
1003
+ });
996
1004
  app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
997
1005
  app.patch("/api/chapters/:chapterId", (request, response) => {
998
1006
  const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
@@ -1013,6 +1021,26 @@ export function createRuntime(options) {
1013
1021
  const pagination = parsePagination(request.query);
1014
1022
  data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
1015
1023
  });
1024
+ app.get("/api/chapters/:chapterId/annotations", (request, response) => data(response, store.listChapterAnnotations(request.params.chapterId)));
1025
+ app.post("/api/chapters/:chapterId/annotations", (request, response) => {
1026
+ const input = parse(z.object({
1027
+ kind: z.enum(["note", "todo"]),
1028
+ startLine: z.number().int().positive(),
1029
+ endLine: z.number().int().positive(),
1030
+ note: z.string().trim().min(1).max(2000)
1031
+ }).strict().refine((value) => value.endLine >= value.startLine, { message: "结束行不能早于开始行", path: ["endLine"] }), request.body);
1032
+ data(response, store.createChapterAnnotation(request.params.chapterId, input), 201);
1033
+ });
1034
+ app.patch("/api/chapter-annotations/:annotationId", (request, response) => {
1035
+ const input = parse(z.object({ note: z.string().trim().min(1).max(2000).optional(), status: z.enum(["open", "resolved"]).optional(), expectedVersionNo: expectedVersionNoSchema }).strict().refine((value) => value.note !== undefined || value.status !== undefined, { message: "至少需要修改一项" }), request.body);
1036
+ const { expectedVersionNo, ...update } = input;
1037
+ data(response, store.updateChapterAnnotation(request.params.annotationId, update, expectedVersionNo));
1038
+ });
1039
+ app.delete("/api/chapter-annotations/:annotationId", (request, response) => {
1040
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1041
+ store.deleteChapterAnnotation(request.params.annotationId, input.expectedVersionNo);
1042
+ noContent(response);
1043
+ });
1016
1044
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
1017
1045
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1018
1046
  const chapter = store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo);
@@ -1023,6 +1051,17 @@ export function createRuntime(options) {
1023
1051
  const { expectedVersionNo, ...moveInput } = input;
1024
1052
  data(response, store.moveChapter(request.params.chapterId, moveInput, expectedVersionNo));
1025
1053
  });
1054
+ app.post("/api/works/:workId/chapters/batch", (request, response) => {
1055
+ const selectedChapters = z.array(z.object({ id: identifier, expectedVersionNo: z.number().int().positive() }).strict()).min(1).max(200);
1056
+ const action = z.discriminatedUnion("type", [
1057
+ z.object({ type: z.literal("move"), volumeId: identifier }).strict(),
1058
+ z.object({ type: z.literal("setType"), chapterType: chapterTypeSchema }).strict(),
1059
+ z.object({ type: z.literal("setAnalysisExclusion"), excludedFromAnalysis: z.boolean() }).strict(),
1060
+ z.object({ type: z.literal("delete") }).strict()
1061
+ ]);
1062
+ const input = parse(z.object({ chapters: selectedChapters, action }).strict(), request.body);
1063
+ data(response, store.batchManageChapters(request.params.workId, input.chapters, input.action));
1064
+ });
1026
1065
  app.get("/api/works/:workId/outlines", (request, response) => {
1027
1066
  const pagination = parsePagination(request.query);
1028
1067
  data(response, pagination ? store.listChapterOutlinesPage(request.params.workId, pagination) : store.listChapterOutlines(request.params.workId));
@@ -1818,21 +1857,44 @@ export function createRuntime(options) {
1818
1857
  const query = parse(z.string().trim().min(1).max(500), request.query.q);
1819
1858
  data(response, store.search(request.params.workId, query));
1820
1859
  });
1821
- app.get("/api/works/:workId/export", (request, response) => {
1860
+ app.get("/api/works/:workId/export", async (request, response) => {
1822
1861
  const format = parse(z.enum(["json", "txt", "markdown"]), request.query.format ?? "json");
1823
1862
  if (format === "json") {
1824
1863
  response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.json`);
1825
1864
  data(response, store.exportWork(request.params.workId));
1826
1865
  return;
1827
1866
  }
1828
- response.type(format === "txt" ? "text/plain" : "text/markdown");
1829
- response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.${format === "markdown" ? "md" : "txt"}`);
1867
+ if (format === "markdown") {
1868
+ const exportName = `novel-${request.params.workId}`;
1869
+ const archive = new JSZip();
1870
+ archive.file(`${exportName}.md`, store.exportText(request.params.workId, format));
1871
+ response.type("application/zip");
1872
+ response.setHeader("Content-Disposition", `attachment; filename=${exportName}.zip`);
1873
+ await pipeline(archive.generateNodeStream({
1874
+ type: "nodebuffer",
1875
+ streamFiles: true,
1876
+ compression: "DEFLATE",
1877
+ compressionOptions: { level: 6 }
1878
+ }), response);
1879
+ return;
1880
+ }
1881
+ response.type("text/plain");
1882
+ response.setHeader("Content-Disposition", `attachment; filename=novel-${request.params.workId}.txt`);
1830
1883
  response.send(store.exportText(request.params.workId, format));
1831
1884
  });
1832
1885
  app.get("/api/works/:workId/audit-logs", (request, response) => {
1833
1886
  const pagination = parsePagination(request.query);
1834
1887
  data(response, pagination ? store.listAuditLogsPage(request.params.workId, pagination) : store.listAuditLogs(request.params.workId));
1835
1888
  });
1889
+ app.get("/api/works/:workId/writing-progress", (request, response) => data(response, store.getWritingProgress(request.params.workId)));
1890
+ app.put("/api/works/:workId/writing-goal", (request, response) => {
1891
+ const input = parse(z.object({
1892
+ dailyGoal: z.number().int().min(0).max(1_000_000),
1893
+ targetTotal: z.number().int().min(0).max(100_000_000),
1894
+ deadline: z.string().date().nullable()
1895
+ }).strict(), request.body);
1896
+ data(response, store.updateWritingGoal(request.params.workId, input));
1897
+ });
1836
1898
  if (options.serveUi ?? true) {
1837
1899
  const publicPath = options.publicPath ?? join(process.cwd(), "src", "public");
1838
1900
  // index.html 按登录态动态下发:未登录时注入 login-route 类,首帧直接渲染登录页;
@@ -1883,6 +1945,12 @@ export function createRuntime(options) {
1883
1945
  app.use((_request, _response, next) => next(new AppError(404, "ROUTE_NOT_FOUND", "请求的接口不存在")));
1884
1946
  app.use((error, request, response, _next) => {
1885
1947
  const commonFields = { method: request.method, path: sanitizeRequestPath(request.path), error: sanitizeError(error) };
1948
+ if (response.headersSent || response.destroyed) {
1949
+ logger.warn("http.request.response_stream_failed", commonFields);
1950
+ if (!response.destroyed)
1951
+ response.destroy(error instanceof Error ? error : undefined);
1952
+ return;
1953
+ }
1886
1954
  if (error instanceof ZodError) {
1887
1955
  logger.warn("http.request.validation_failed", { ...commonFields, issuePaths: error.issues.map((issue) => issue.path.join(".")) });
1888
1956
  response.status(400).json({