@musnows/scriverse 0.7.13 → 0.8.1

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.
Files changed (52) hide show
  1. package/README.en.md +4 -0
  2. package/README.md +5 -0
  3. package/dist/ai-chat-tab-limit.js +14 -0
  4. package/dist/ai-chat-tab-limit.js.map +1 -0
  5. package/dist/ai-protocol.js +10 -2
  6. package/dist/ai-protocol.js.map +1 -1
  7. package/dist/ai-retry.js +52 -0
  8. package/dist/ai-retry.js.map +1 -0
  9. package/dist/ai.js +547 -119
  10. package/dist/ai.js.map +1 -1
  11. package/dist/app.js +80 -22
  12. package/dist/app.js.map +1 -1
  13. package/dist/cli-contract.js +2 -1
  14. package/dist/cli-contract.js.map +1 -1
  15. package/dist/database.js +127 -2
  16. package/dist/database.js.map +1 -1
  17. package/dist/domain.js +1 -0
  18. package/dist/domain.js.map +1 -1
  19. package/dist/public/ai-chat-tabs.js +75 -0
  20. package/dist/public/ai-request-manager.js +32 -15
  21. package/dist/public/app.js +1305 -298
  22. package/dist/public/background-task-center.d.ts +9 -0
  23. package/dist/public/background-task-center.js +18 -0
  24. package/dist/public/chapter-search.d.ts +6 -0
  25. package/dist/public/chapter-search.js +23 -0
  26. package/dist/public/character-filters.d.ts +3 -1
  27. package/dist/public/character-filters.js +17 -2
  28. package/dist/public/character-version.js +1 -0
  29. package/dist/public/display-labels.d.ts +1 -0
  30. package/dist/public/display-labels.js +5 -1
  31. package/dist/public/index.html +67 -26
  32. package/dist/public/model-config.d.ts +3 -1
  33. package/dist/public/model-config.js +13 -0
  34. package/dist/public/relationship-filters.d.ts +5 -0
  35. package/dist/public/relationship-filters.js +31 -0
  36. package/dist/public/relationship-graph.js +289 -58
  37. package/dist/public/stream-typewriter.d.ts +1 -0
  38. package/dist/public/stream-typewriter.js +12 -0
  39. package/dist/public/styles.css +185 -41
  40. package/dist/public/work-permissions.d.ts +1 -1
  41. package/dist/public/work-permissions.js +13 -1
  42. package/dist/server-runtime.js +6 -0
  43. package/dist/server-runtime.js.map +1 -1
  44. package/dist/store.js +127 -44
  45. package/dist/store.js.map +1 -1
  46. package/dist/user-auth.js +34 -7
  47. package/dist/user-auth.js.map +1 -1
  48. package/dist/version.js +6 -1
  49. package/dist/version.js.map +1 -1
  50. package/dist/work-permissions.js +13 -2
  51. package/dist/work-permissions.js.map +1 -1
  52. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -9,8 +9,9 @@ import { rm } from "node:fs/promises";
9
9
  import { tmpdir } from "node:os";
10
10
  import { pipeline } from "node:stream/promises";
11
11
  import { z, ZodError } from "zod";
12
- import { AI_PROVIDER_PROTOCOLS } from "./ai-protocol.js";
12
+ import { AI_PROVIDER_PROTOCOLS, MAX_TOKENS_PARAMETERS } from "./ai-protocol.js";
13
13
  import { aiConversationExportContentDisposition, exportAiConversationMarkdown } from "./ai-conversation-export.js";
14
+ import { DEFAULT_AI_CHAT_TAB_LIMIT } from "./ai-chat-tab-limit.js";
14
15
  import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_MS } from "./ai-stream-timeout.js";
15
16
  import { AttachmentStorage } from "./attachment-storage.js";
16
17
  import { AiManager } from "./ai.js";
@@ -20,12 +21,12 @@ import { CredentialVault } from "./credential-vault.js";
20
21
  import { Database } from "./database.js";
21
22
  import { assertSafeDocxArchive } from "./docx-security.js";
22
23
  import { EPUB_MIME_TYPE, epubContentDisposition } from "./epub-export.js";
23
- import { CREATABLE_ANALYSIS_TASK_TYPES, DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
24
+ import { CHARACTER_GENDERS, CREATABLE_ANALYSIS_TASK_TYPES, DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
24
25
  import { AppError } from "./errors.js";
25
26
  import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
26
27
  import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, readableHybridSearchTypes } from "./hybrid-search.js";
27
28
  import { applyImportFileHints, parseNovelText } from "./parser.js";
28
- import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes } from "./store.js";
29
+ import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes, WORK_AGENT_TOOL_IDS } from "./store.js";
29
30
  import { paginated, parsePagination } from "./pagination.js";
30
31
  import { normalizeUploadFileName } from "./utils.js";
31
32
  import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
@@ -39,10 +40,14 @@ import { S3BackupManager } from "./s3-backup.js";
39
40
  import { APP_VERSION } from "./version.js";
40
41
  import { ReleaseUpdateChecker } from "./release-update.js";
41
42
  import { DEFAULT_IMAGE_UPLOAD_LIMITS, formatUploadLimit } from "./upload-limits.js";
42
- import { canReadWorkModule, canWriteWorkModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
43
+ import { canReadWorkModule, canWriteWorkModule, chapterAnnotationPermissionModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
43
44
  import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, presencePageKinds } from "./collaboration-presence.js";
44
45
  import { PresenceStore } from "./presence-store.js";
45
46
  import { analysisTaskReadModules, clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, relationshipAnalysisReadModules, setSessionCookie, UserAuthService } from "./user-auth.js";
47
+ const chapterAnnotationKinds = ["note", "todo"];
48
+ function readableChapterAnnotationKinds(permissions) {
49
+ return chapterAnnotationKinds.filter((kind) => canReadWorkModule(permissions, chapterAnnotationPermissionModule(kind)));
50
+ }
46
51
  const nonEmpty = z.string().trim().min(1);
47
52
  const identifier = z.string().trim().min(1).max(200);
48
53
  const idempotencyKeySchema = z.string().trim().min(16).max(128)
@@ -109,6 +114,8 @@ const memberRoleValueSchema = z.enum(["editor", "settings-editor", "viewer"]);
109
114
  const moduleAccessSchema = z.enum(["none", "read", "write"]);
110
115
  const modulePermissionsSchema = z.object({
111
116
  prose: moduleAccessSchema,
117
+ comments: moduleAccessSchema.optional(),
118
+ todos: moduleAccessSchema.optional(),
112
119
  drafts: moduleAccessSchema.optional(),
113
120
  settings: moduleAccessSchema,
114
121
  characters: moduleAccessSchema,
@@ -123,6 +130,8 @@ const modulePermissionsSchema = z.object({
123
130
  "ai-settings": moduleAccessSchema
124
131
  }).strict().transform((permissions) => ({
125
132
  ...permissions,
133
+ comments: permissions.comments ?? permissions.prose,
134
+ todos: permissions.todos ?? permissions.prose,
126
135
  drafts: permissions.drafts ?? (permissions.prose === "write" && permissions.settings === "write"
127
136
  ? "write"
128
137
  : permissions.prose === "none" || permissions.settings === "none" ? "none" : "read")
@@ -189,7 +198,8 @@ const settingSchema = z.object({
189
198
  const globalReplaceSchema = z.object({
190
199
  find: z.string().min(1).max(500),
191
200
  replacement: z.string().max(200_000),
192
- scope: z.enum(["prose", "settings", "prose-and-settings"])
201
+ scope: z.enum(["prose", "settings", "prose-and-settings"]),
202
+ volumeId: identifier.nullable().optional()
193
203
  }).strict();
194
204
  const draftSchema = z.object({
195
205
  draftType: z.enum(["prose", "setting"]),
@@ -200,6 +210,7 @@ const draftSchema = z.object({
200
210
  }).strict();
201
211
  const characterSchema = z.object({
202
212
  name: nonEmpty.max(200),
213
+ gender: z.enum(CHARACTER_GENDERS).optional(),
203
214
  isDead: z.boolean().optional(),
204
215
  code: z.string().trim().max(200).optional(),
205
216
  aliases: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
@@ -344,6 +355,7 @@ const providerBaseSchema = z.object({
344
355
  baseUrl: z.string().url().refine((value) => value.startsWith("http://") || value.startsWith("https://"), "接口地址必须使用 HTTP 或 HTTPS"),
345
356
  apiKey: z.string().trim().min(1).max(50_000),
346
357
  protocol: z.enum(AI_PROVIDER_PROTOCOLS).optional(),
358
+ maxTokensParameter: z.enum(MAX_TOKENS_PARAMETERS).optional(),
347
359
  status: z.enum(["enabled", "disabled"]).optional(),
348
360
  note: z.string().max(10_000).optional(),
349
361
  concurrencyLimit: z.number().int().min(1).max(100).optional(),
@@ -396,6 +408,7 @@ const modelSchema = z.object({
396
408
  outputNote: z.string().max(10_000).optional(),
397
409
  preset: jsonObject.optional(),
398
410
  thinkingEnabled: z.boolean().optional(),
411
+ thinkingEffort: z.enum(["default", "low", "medium", "high", "xhigh", "max"]).optional(),
399
412
  multimodalEnabled: z.boolean().optional(),
400
413
  imageToolDefault: z.boolean().optional(),
401
414
  enabled: z.boolean().optional(),
@@ -536,11 +549,12 @@ const workAiSettingsSchema = z.object({
536
549
  autoRunBatchLimit: z.number().int().min(1).max(200).optional(),
537
550
  autoRunDailyTaskLimit: z.number().int().min(0).max(10_000).optional(),
538
551
  autoRunFailureThreshold: z.number().int().min(1).max(10).optional(),
552
+ autoRunStabilityDelayMinutes: z.number().int().min(1).max(120).optional(),
539
553
  bookSummaryContextPercent: z.number().int().min(1).max(90).optional(),
540
554
  contextCompactThreshold: z.number().int().min(50).max(90).optional(),
541
555
  agentToolCallLimit: z.number().int().min(5).optional(),
542
556
  agentToolCallGlobalMultiplier: z.number().int().min(1).max(6).optional(),
543
- agentTools: z.array(z.enum(["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"])).max(7).optional(),
557
+ agentTools: z.array(z.enum(WORK_AGENT_TOOL_IDS)).max(WORK_AGENT_TOOL_IDS.length).optional(),
544
558
  alwaysIncludeSettingInfo: z.boolean().optional(),
545
559
  titleGenerationModelId: z.string().trim().max(200).optional(),
546
560
  imageToolModelId: identifier.nullable().optional()
@@ -605,8 +619,8 @@ const relationshipAnalysisScopeSchema = z.object({
605
619
  if (scope.volumeIds && new Set(scope.volumeIds).size !== scope.volumeIds.length) {
606
620
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["volumeIds"], message: "分卷不能重复选择" });
607
621
  }
608
- if (scope.includeAllSettings && scope.type !== "book") {
609
- context.addIssue({ code: z.ZodIssueCode.custom, path: ["includeAllSettings"], message: "包含所有设定仅支持全书人物关系分析" });
622
+ if (scope.includeAllSettings && scope.type !== "book" && scope.type !== "chapter") {
623
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["includeAllSettings"], message: "包含所有设定仅支持全书或指定章节人物关系分析" });
610
624
  }
611
625
  if (scope.characterIds && new Set(scope.characterIds).size !== scope.characterIds.length) {
612
626
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["characterIds"], message: "被分析角色不能重复" });
@@ -926,12 +940,18 @@ function redactAiConversationMessage(item, permissions) {
926
940
  if (!message)
927
941
  return item;
928
942
  if (permissions.prose !== "none") {
929
- if (permissions.characters !== "none")
930
- return item;
931
943
  const metadata = recordValue(message.metadata);
932
- if (!metadata || !("mentionCharacterIds" in metadata))
944
+ if (!metadata)
945
+ return item;
946
+ const readableMetadata = { ...metadata };
947
+ if (permissions.characters === "none")
948
+ delete readableMetadata.mentionCharacterIds;
949
+ if (permissions.races === "none")
950
+ delete readableMetadata.mentionRaceIds;
951
+ if (permissions.organizations === "none")
952
+ delete readableMetadata.mentionOrganizationIds;
953
+ if (Object.keys(readableMetadata).length === Object.keys(metadata).length)
933
954
  return item;
934
- const { mentionCharacterIds: _mentionCharacterIds, ...readableMetadata } = metadata;
935
955
  return { ...message, metadata: readableMetadata };
936
956
  }
937
957
  return {
@@ -949,6 +969,13 @@ function redactAiConversation(record, permissions) {
949
969
  const result = {
950
970
  ...scopedRecord
951
971
  };
972
+ if (result.roleplayCharacter) {
973
+ result.agentTools = [
974
+ ...(permissions.characters !== "none" ? ["recall_self"] : []),
975
+ ...(permissions.characters !== "none" && permissions.relationships !== "none" ? ["recall_relationship"] : []),
976
+ "calculate_time"
977
+ ];
978
+ }
952
979
  if ((permissions.prose === "none" || permissions.characters === "none") && Array.isArray(result.messages)) {
953
980
  result.messages = result.messages.map((item) => redactAiConversationMessage(item, permissions));
954
981
  }
@@ -1007,6 +1034,7 @@ function redactVersionSnapshots(value, mapper) {
1007
1034
  }
1008
1035
  export function createRuntime(options) {
1009
1036
  const uploadLimits = options.uploadLimits ?? DEFAULT_IMAGE_UPLOAD_LIMITS;
1037
+ const aiChatTabLimit = options.aiChatTabLimit ?? DEFAULT_AI_CHAT_TAB_LIMIT;
1010
1038
  logger.info("runtime.initializing", {
1011
1039
  databasePath: options.databasePath,
1012
1040
  serveUi: options.serveUi ?? true,
@@ -1124,7 +1152,11 @@ export function createRuntime(options) {
1124
1152
  read: requiredModules,
1125
1153
  write: ["ai-analysis"]
1126
1154
  }, false, actor?.allowAdminAccess ?? false);
1127
- }, attachmentStorage, { interactiveStreamIdleTimeoutMs: options.aiStreamIdleTimeoutMs ?? DEFAULT_AI_STREAM_IDLE_TIMEOUT_MS });
1155
+ }, attachmentStorage, {
1156
+ interactiveStreamIdleTimeoutMs: options.aiStreamIdleTimeoutMs ?? DEFAULT_AI_STREAM_IDLE_TIMEOUT_MS,
1157
+ retryPolicy: options.aiRetryPolicy,
1158
+ retrySleep: options.aiRetrySleep
1159
+ });
1128
1160
  const app = express();
1129
1161
  enforceCaseInsensitiveRouting(app);
1130
1162
  const upload = multer({
@@ -1160,9 +1192,11 @@ export function createRuntime(options) {
1160
1192
  status: "ok",
1161
1193
  bootId,
1162
1194
  version: APP_VERSION,
1195
+ versionLabel: options.betaVersionLabel ?? null,
1163
1196
  protocol: "openai-chat-completions",
1164
1197
  protocols: [...AI_PROVIDER_PROTOCOLS],
1165
1198
  development: options.developmentServer === true,
1199
+ aiChatTabLimit,
1166
1200
  uploadLimits
1167
1201
  });
1168
1202
  });
@@ -1589,12 +1623,23 @@ export function createRuntime(options) {
1589
1623
  const pagination = parsePagination(request.query);
1590
1624
  data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
1591
1625
  });
1592
- app.get("/api/chapters/:chapterId/annotations", (request, response) => data(response, store.listChapterAnnotations(request.params.chapterId)));
1626
+ app.get("/api/chapters/:chapterId/annotation-counts", (request, response) => {
1627
+ const permissions = requestPermissions(request);
1628
+ data(response, store.listChapterAnnotationCounts(request.params.chapterId, readableChapterAnnotationKinds(permissions)));
1629
+ });
1630
+ app.get("/api/chapters/:chapterId/annotations", (request, response) => {
1631
+ const line = request.query.line === undefined
1632
+ ? undefined
1633
+ : parse(z.coerce.number().int().positive().max(100_000), request.query.line);
1634
+ const permissions = requestPermissions(request);
1635
+ data(response, store.listChapterAnnotations(request.params.chapterId, readableChapterAnnotationKinds(permissions), line));
1636
+ });
1593
1637
  app.get("/api/works/:workId/chapter-annotations", (request, response) => {
1594
1638
  const pagination = parsePagination(request.query);
1639
+ const permissions = requestPermissions(request, request.params.workId);
1595
1640
  data(response, pagination
1596
- ? store.listWorkChapterAnnotationsPage(request.params.workId, pagination)
1597
- : store.listWorkChapterAnnotations(request.params.workId));
1641
+ ? store.listWorkChapterAnnotationsPage(request.params.workId, pagination, readableChapterAnnotationKinds(permissions))
1642
+ : store.listWorkChapterAnnotations(request.params.workId, readableChapterAnnotationKinds(permissions)));
1598
1643
  });
1599
1644
  app.post("/api/chapters/:chapterId/annotations", (request, response) => {
1600
1645
  const input = parse(z.object({
@@ -2173,9 +2218,16 @@ export function createRuntime(options) {
2173
2218
  }
2174
2219
  data(response, await ai.previewRelationshipSources(request.params.workId, input.scope, input.modelId));
2175
2220
  });
2176
- app.post("/api/works/:workId/tasks", (request, response) => {
2221
+ app.post("/api/works/:workId/tasks", async (request, response) => {
2177
2222
  const input = parse(analysisTaskSchema, request.body);
2178
- data(response, redactTaskCharacterNames(ai.createTask(request.params.workId, input), requestPermissions(request, request.params.workId)), 201);
2223
+ const permissions = requestPermissions(request, request.params.workId);
2224
+ const deniedModules = analysisTaskReadModules(input.taskType, input.scope).filter((module) => permissions[module] === "none");
2225
+ if (deniedModules.length > 0) {
2226
+ throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取创建分析任务所需资料模块的权限", {
2227
+ modules: deniedModules
2228
+ });
2229
+ }
2230
+ data(response, redactTaskCharacterNames(await ai.createTask(request.params.workId, input), permissions), 201);
2179
2231
  });
2180
2232
  app.post("/api/works/:workId/tasks/auto-run", (request, response) => {
2181
2233
  const permissions = requestPermissions(request, request.params.workId);
@@ -2223,7 +2275,7 @@ export function createRuntime(options) {
2223
2275
  allowAdminAccess: request.authMethod !== "api-key"
2224
2276
  } : undefined), requestPermissions(request)));
2225
2277
  });
2226
- app.post("/api/tasks/:taskId/rerun", (request, response) => {
2278
+ app.post("/api/tasks/:taskId/rerun", async (request, response) => {
2227
2279
  const input = parse(z.object({ modelId: identifier.optional() }).strict(), request.body ?? {});
2228
2280
  const task = store.getTask(request.params.taskId);
2229
2281
  const permissions = requestPermissions(request, String(task.workId));
@@ -2233,7 +2285,7 @@ export function createRuntime(options) {
2233
2285
  modules: deniedModules
2234
2286
  });
2235
2287
  }
2236
- data(response, redactTaskCharacterNames(ai.rerunTask(request.params.taskId, input.modelId), requestPermissions(request)), 201);
2288
+ data(response, redactTaskCharacterNames(await ai.rerunTask(request.params.taskId, input.modelId), requestPermissions(request)), 201);
2237
2289
  });
2238
2290
  app.post("/api/tasks/:taskId/cancel", (request, response) => data(response, redactTaskCharacterNames(ai.cancelTask(request.params.taskId), requestPermissions(request))));
2239
2291
  app.post("/api/tasks/:taskId/character-extraction/apply", (request, response) => {
@@ -2353,6 +2405,8 @@ export function createRuntime(options) {
2353
2405
  else
2354
2406
  ai.scheduleAutoRun(workId);
2355
2407
  }
2408
+ if (input.autoRunStabilityDelayMinutes !== undefined)
2409
+ ai.rescheduleChapterAnalysisTasks(workId);
2356
2410
  data(response, updated);
2357
2411
  });
2358
2412
  app.get("/api/works/:workId/ai-conversations", (request, response) => {
@@ -2680,6 +2734,8 @@ export function createRuntime(options) {
2680
2734
  ...(resolvedScope.characterIds ?? []),
2681
2735
  ...(resolvedScope.mentionCharacterIds ?? [])
2682
2736
  ])];
2737
+ const mentionRaceIds = [...new Set(resolvedScope.raceIds ?? [])];
2738
+ const mentionOrganizationIds = [...new Set(resolvedScope.organizationIds ?? [])];
2683
2739
  const begun = store.beginAiConversationStreamRequest({
2684
2740
  workId: request.params.workId,
2685
2741
  conversationId,
@@ -2690,9 +2746,11 @@ export function createRuntime(options) {
2690
2746
  content: input.instruction,
2691
2747
  citations,
2692
2748
  ...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
2693
- ...((modelId || mentionCharacterIds.length) ? { metadata: {
2749
+ ...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length) ? { metadata: {
2694
2750
  ...(modelId ? { modelId } : {}),
2695
- ...(mentionCharacterIds.length ? { mentionCharacterIds } : {})
2751
+ ...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
2752
+ ...(mentionRaceIds.length ? { mentionRaceIds } : {}),
2753
+ ...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {})
2696
2754
  } } : {})
2697
2755
  }
2698
2756
  });