@musnows/scriverse 0.6.3 → 0.6.5

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
@@ -9,6 +9,7 @@ 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
13
  import { AttachmentStorage } from "./attachment-storage.js";
13
14
  import { AiManager } from "./ai.js";
14
15
  import { CredentialVault } from "./credential-vault.js";
@@ -16,9 +17,10 @@ import { Database } from "./database.js";
16
17
  import { assertSafeDocxArchive } from "./docx-security.js";
17
18
  import { DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
18
19
  import { AppError } from "./errors.js";
20
+ import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
19
21
  import { HYBRID_SEARCH_TYPES } from "./hybrid-search.js";
20
22
  import { applyImportFileHints, parseNovelText } from "./parser.js";
21
- import { attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
23
+ import { aiConversationTaskTypes, attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
22
24
  import { paginated, parsePagination } from "./pagination.js";
23
25
  import { normalizeUploadFileName } from "./utils.js";
24
26
  import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
@@ -37,6 +39,7 @@ const identifier = z.string().trim().min(1).max(200);
37
39
  const optionalStrings = z.array(z.string()).optional();
38
40
  const jsonObject = z.record(z.string(), z.unknown());
39
41
  const chapterTypeSchema = z.enum(["正文", "设定", "作者的话", "其他"]);
42
+ const aiConversationTaskTypeSchema = z.enum(aiConversationTaskTypes);
40
43
  const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
41
44
  const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
42
45
  const maximumImportedTextLength = 20_000_000;
@@ -289,16 +292,54 @@ const reviewSchema = z.object({
289
292
  status: z.enum(["pending", "ignored", "fixing", "fixed", "exception"]).optional(),
290
293
  resolutionNote: z.string().max(20_000).optional()
291
294
  });
292
- const providerSchema = z.object({
295
+ const providerBaseSchema = z.object({
293
296
  name: nonEmpty.max(200),
294
297
  baseUrl: z.string().url().refine((value) => value.startsWith("http://") || value.startsWith("https://"), "接口地址必须使用 HTTP 或 HTTPS"),
295
- apiKey: nonEmpty.max(10_000),
296
- protocol: z.enum(["openai-chat-completions", "anthropic-messages"]).optional(),
298
+ apiKey: z.string().trim().min(1).max(50_000),
299
+ protocol: z.enum(AI_PROVIDER_PROTOCOLS).optional(),
297
300
  status: z.enum(["enabled", "disabled"]).optional(),
298
301
  note: z.string().max(10_000).optional(),
299
302
  concurrencyLimit: z.number().int().min(1).max(100).optional(),
300
303
  rpmLimit: z.number().int().min(1).max(10_000).optional()
301
304
  });
305
+ function refineProviderApiKey(value, ctx) {
306
+ if (value.protocol === "google-vertex" && value.baseUrl && !isOfficialGoogleVertexBaseUrl(value.baseUrl)) {
307
+ ctx.addIssue({
308
+ code: z.ZodIssueCode.custom,
309
+ path: ["baseUrl"],
310
+ message: "Google Vertex 接口地址必须使用官方 aiplatform.googleapis.com 或 *-aiplatform.googleapis.com 域名"
311
+ });
312
+ }
313
+ if (!value.apiKey)
314
+ return;
315
+ const protocol = value.protocol ?? "openai-chat-completions";
316
+ if (protocol === "google-vertex") {
317
+ try {
318
+ parseGoogleServiceAccount(value.apiKey);
319
+ }
320
+ catch (error) {
321
+ ctx.addIssue({
322
+ code: z.ZodIssueCode.custom,
323
+ path: ["apiKey"],
324
+ message: error instanceof AppError ? error.message : "服务账号 JSON 无效"
325
+ });
326
+ }
327
+ return;
328
+ }
329
+ if (value.apiKey.length > 10_000) {
330
+ ctx.addIssue({
331
+ code: z.ZodIssueCode.custom,
332
+ path: ["apiKey"],
333
+ message: "API 密钥过长"
334
+ });
335
+ }
336
+ }
337
+ const providerSchema = providerBaseSchema.superRefine((value, ctx) => {
338
+ refineProviderApiKey(value, ctx);
339
+ });
340
+ const providerUpdateSchema = providerBaseSchema.partial().superRefine((value, ctx) => {
341
+ refineProviderApiKey(value, ctx);
342
+ });
302
343
  const modelSchema = z.object({
303
344
  displayName: nonEmpty.max(200),
304
345
  modelId: nonEmpty.max(300),
@@ -390,17 +431,22 @@ const workAiSettingsSchema = z.object({
390
431
  agentToolCallLimit: z.number().int().min(5).max(48).optional(),
391
432
  agentToolCallGlobalMultiplier: z.number().int().min(1).max(6).optional(),
392
433
  agentTools: z.array(z.enum(["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"])).max(6).optional(),
434
+ alwaysIncludeSettingInfo: z.boolean().optional(),
393
435
  titleGenerationModelId: z.string().trim().max(200).optional()
394
436
  }).strict();
395
437
  const contextSchema = z.object({
396
- type: z.enum(["none", "selection", "chapter", "volume", "book", "entities"]),
438
+ type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
397
439
  chapterId: identifier.optional(),
398
440
  volumeId: identifier.optional(),
399
441
  selection: z.string().max(200_000).optional(),
400
442
  chapterIds: z.array(identifier).max(20).optional(),
401
443
  characterIds: optionalStrings,
444
+ mentionCharacterIds: optionalStrings,
402
445
  settingIds: optionalStrings,
403
- includeBookSummary: z.boolean().optional()
446
+ raceIds: optionalStrings,
447
+ organizationIds: optionalStrings,
448
+ includeBookSummary: z.boolean().optional(),
449
+ includeSettingInfo: z.boolean().optional()
404
450
  });
405
451
  const analysisTaskTypeSchema = z.enum(["structure", "chapter-analysis", "character-extraction", "character-summary", "character-identity-audit", "timeline-analysis", "worldview-analysis", "setting-extraction", "consistency-check", "report-update", "book-analysis"]);
406
452
  const relationshipSourceRefSchema = z.object({
@@ -622,6 +668,18 @@ function redactAiCallContext(record, permissions) {
622
668
  delete redactedScope.characterIds;
623
669
  restricted = true;
624
670
  }
671
+ if (permissions.characters === "none" && "mentionCharacterIds" in redactedScope) {
672
+ delete redactedScope.mentionCharacterIds;
673
+ restricted = true;
674
+ }
675
+ if (permissions.races === "none" && "raceIds" in redactedScope) {
676
+ delete redactedScope.raceIds;
677
+ restricted = true;
678
+ }
679
+ if (permissions.organizations === "none" && "organizationIds" in redactedScope) {
680
+ delete redactedScope.organizationIds;
681
+ restricted = true;
682
+ }
625
683
  if (permissions.settings === "none" && "settingIds" in redactedScope) {
626
684
  delete redactedScope.settingIds;
627
685
  restricted = true;
@@ -670,10 +728,12 @@ function redactAiConversationMessage(item, permissions) {
670
728
  }
671
729
  /** 无正文读取权限时隐藏对话预览与消息正文,避免历史对话泄露章节原文。 */
672
730
  function redactAiConversation(record, permissions) {
731
+ const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
732
+ const scopedRecord = redactAiCallContext(readableRecord, permissions);
673
733
  if (permissions.prose !== "none")
674
- return record;
734
+ return scopedRecord;
675
735
  const result = {
676
- ...record,
736
+ ...scopedRecord,
677
737
  title: proseRestrictedPlaceholder
678
738
  };
679
739
  if (typeof result.preview === "string" && result.preview.length > 0) {
@@ -691,7 +751,7 @@ function redactAiConversation(record, permissions) {
691
751
  }
692
752
  return { ...result, restricted: true };
693
753
  }
694
- /** SSE 错误事件只暴露 AppError 的公开信息,避免透传内部异常 message。 */
754
+ /** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED failure 已在 AI 层完成密钥脱敏。 */
695
755
  export function publicAiStreamError(error) {
696
756
  if (error instanceof AppError) {
697
757
  const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
@@ -701,7 +761,7 @@ export function publicAiStreamError(error) {
701
761
  code: error.code,
702
762
  message: error.message,
703
763
  status: error.status,
704
- ...(error.status < 500 && typeof details?.failure === "string" ? { failure: details.failure } : {}),
764
+ ...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
705
765
  ...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
706
766
  ...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
707
767
  ...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
@@ -843,7 +903,7 @@ export function createRuntime(options) {
843
903
  bootId,
844
904
  version: APP_VERSION,
845
905
  protocol: "openai-chat-completions",
846
- protocols: ["openai-chat-completions", "anthropic-messages"],
906
+ protocols: [...AI_PROVIDER_PROTOCOLS],
847
907
  development: options.developmentServer === true
848
908
  });
849
909
  });
@@ -1880,8 +1940,11 @@ export function createRuntime(options) {
1880
1940
  data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
1881
1941
  });
1882
1942
  app.post("/api/works/:workId/ai-conversations", (request, response) => {
1883
- const input = parse(z.object({ title: z.string().max(200).optional() }), request.body ?? {});
1884
- data(response, store.createAiConversation(request.params.workId, input.title), 201);
1943
+ const input = parse(z.object({
1944
+ title: z.string().max(200).optional(),
1945
+ taskType: aiConversationTaskTypeSchema.optional()
1946
+ }).strict(), request.body ?? {});
1947
+ data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
1885
1948
  });
1886
1949
  app.get("/api/ai-conversations/:conversationId", (request, response) => {
1887
1950
  const pagination = parsePagination(request.query);
@@ -1897,6 +1960,24 @@ export function createRuntime(options) {
1897
1960
  const permissions = requestPermissions(request, String(forked.workId));
1898
1961
  data(response, redactAiConversation(forked, permissions), 201);
1899
1962
  });
1963
+ app.patch("/api/ai-conversations/:conversationId/task-type", (request, response) => {
1964
+ const input = parse(z.object({ taskType: aiConversationTaskTypeSchema }).strict(), request.body);
1965
+ const updated = store.setAiConversationTaskType(request.params.conversationId, input.taskType);
1966
+ const permissions = requestPermissions(request, String(updated.workId));
1967
+ data(response, redactAiConversation(updated, permissions));
1968
+ });
1969
+ app.patch("/api/ai-conversations/:conversationId/context-scope", (request, response) => {
1970
+ const input = parse(z.object({ scope: contextSchema }).strict(), request.body);
1971
+ const updated = store.setAiConversationContextScope(request.params.conversationId, input.scope);
1972
+ const permissions = requestPermissions(request, String(updated.workId));
1973
+ data(response, redactAiConversation(updated, permissions));
1974
+ });
1975
+ app.patch("/api/ai-conversations/:conversationId/roleplay", (request, response) => {
1976
+ const input = parse(z.object({ characterId: identifier.nullable() }).strict(), request.body);
1977
+ const updated = store.setAiConversationRoleplayCharacter(request.params.conversationId, input.characterId);
1978
+ const permissions = requestPermissions(request, String(updated.workId));
1979
+ data(response, redactAiConversation(updated, permissions));
1980
+ });
1900
1981
  app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
1901
1982
  const input = parse(z.object({
1902
1983
  role: z.enum(["user", "assistant"]),
@@ -1963,7 +2044,7 @@ export function createRuntime(options) {
1963
2044
  data(response, ai.createProvider(parse(providerSchema, request.body)), 201);
1964
2045
  });
1965
2046
  app.get("/api/providers/:providerId", (request, response) => data(response, ai.getProvider(request.params.providerId)));
1966
- app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(providerSchema.partial(), request.body))));
2047
+ app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(providerUpdateSchema, request.body))));
1967
2048
  app.delete("/api/providers/:providerId", (request, response) => {
1968
2049
  ai.deleteProvider(request.params.providerId);
1969
2050
  noContent(response);