@musnows/scriverse 0.6.3 → 0.6.4

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),
@@ -393,14 +434,18 @@ const workAiSettingsSchema = z.object({
393
434
  titleGenerationModelId: z.string().trim().max(200).optional()
394
435
  }).strict();
395
436
  const contextSchema = z.object({
396
- type: z.enum(["none", "selection", "chapter", "volume", "book", "entities"]),
437
+ type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
397
438
  chapterId: identifier.optional(),
398
439
  volumeId: identifier.optional(),
399
440
  selection: z.string().max(200_000).optional(),
400
441
  chapterIds: z.array(identifier).max(20).optional(),
401
442
  characterIds: optionalStrings,
443
+ mentionCharacterIds: optionalStrings,
402
444
  settingIds: optionalStrings,
403
- includeBookSummary: z.boolean().optional()
445
+ raceIds: optionalStrings,
446
+ organizationIds: optionalStrings,
447
+ includeBookSummary: z.boolean().optional(),
448
+ includeSettingInfo: z.boolean().optional()
404
449
  });
405
450
  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
451
  const relationshipSourceRefSchema = z.object({
@@ -622,6 +667,18 @@ function redactAiCallContext(record, permissions) {
622
667
  delete redactedScope.characterIds;
623
668
  restricted = true;
624
669
  }
670
+ if (permissions.characters === "none" && "mentionCharacterIds" in redactedScope) {
671
+ delete redactedScope.mentionCharacterIds;
672
+ restricted = true;
673
+ }
674
+ if (permissions.races === "none" && "raceIds" in redactedScope) {
675
+ delete redactedScope.raceIds;
676
+ restricted = true;
677
+ }
678
+ if (permissions.organizations === "none" && "organizationIds" in redactedScope) {
679
+ delete redactedScope.organizationIds;
680
+ restricted = true;
681
+ }
625
682
  if (permissions.settings === "none" && "settingIds" in redactedScope) {
626
683
  delete redactedScope.settingIds;
627
684
  restricted = true;
@@ -670,10 +727,12 @@ function redactAiConversationMessage(item, permissions) {
670
727
  }
671
728
  /** 无正文读取权限时隐藏对话预览与消息正文,避免历史对话泄露章节原文。 */
672
729
  function redactAiConversation(record, permissions) {
730
+ const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
731
+ const scopedRecord = redactAiCallContext(readableRecord, permissions);
673
732
  if (permissions.prose !== "none")
674
- return record;
733
+ return scopedRecord;
675
734
  const result = {
676
- ...record,
735
+ ...scopedRecord,
677
736
  title: proseRestrictedPlaceholder
678
737
  };
679
738
  if (typeof result.preview === "string" && result.preview.length > 0) {
@@ -691,7 +750,7 @@ function redactAiConversation(record, permissions) {
691
750
  }
692
751
  return { ...result, restricted: true };
693
752
  }
694
- /** SSE 错误事件只暴露 AppError 的公开信息,避免透传内部异常 message。 */
753
+ /** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED failure 已在 AI 层完成密钥脱敏。 */
695
754
  export function publicAiStreamError(error) {
696
755
  if (error instanceof AppError) {
697
756
  const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
@@ -701,7 +760,7 @@ export function publicAiStreamError(error) {
701
760
  code: error.code,
702
761
  message: error.message,
703
762
  status: error.status,
704
- ...(error.status < 500 && typeof details?.failure === "string" ? { failure: details.failure } : {}),
763
+ ...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
705
764
  ...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
706
765
  ...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
707
766
  ...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
@@ -843,7 +902,7 @@ export function createRuntime(options) {
843
902
  bootId,
844
903
  version: APP_VERSION,
845
904
  protocol: "openai-chat-completions",
846
- protocols: ["openai-chat-completions", "anthropic-messages"],
905
+ protocols: [...AI_PROVIDER_PROTOCOLS],
847
906
  development: options.developmentServer === true
848
907
  });
849
908
  });
@@ -1880,8 +1939,11 @@ export function createRuntime(options) {
1880
1939
  data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
1881
1940
  });
1882
1941
  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);
1942
+ const input = parse(z.object({
1943
+ title: z.string().max(200).optional(),
1944
+ taskType: aiConversationTaskTypeSchema.optional()
1945
+ }).strict(), request.body ?? {});
1946
+ data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
1885
1947
  });
1886
1948
  app.get("/api/ai-conversations/:conversationId", (request, response) => {
1887
1949
  const pagination = parsePagination(request.query);
@@ -1897,6 +1959,24 @@ export function createRuntime(options) {
1897
1959
  const permissions = requestPermissions(request, String(forked.workId));
1898
1960
  data(response, redactAiConversation(forked, permissions), 201);
1899
1961
  });
1962
+ app.patch("/api/ai-conversations/:conversationId/task-type", (request, response) => {
1963
+ const input = parse(z.object({ taskType: aiConversationTaskTypeSchema }).strict(), request.body);
1964
+ const updated = store.setAiConversationTaskType(request.params.conversationId, input.taskType);
1965
+ const permissions = requestPermissions(request, String(updated.workId));
1966
+ data(response, redactAiConversation(updated, permissions));
1967
+ });
1968
+ app.patch("/api/ai-conversations/:conversationId/context-scope", (request, response) => {
1969
+ const input = parse(z.object({ scope: contextSchema }).strict(), request.body);
1970
+ const updated = store.setAiConversationContextScope(request.params.conversationId, input.scope);
1971
+ const permissions = requestPermissions(request, String(updated.workId));
1972
+ data(response, redactAiConversation(updated, permissions));
1973
+ });
1974
+ app.patch("/api/ai-conversations/:conversationId/roleplay", (request, response) => {
1975
+ const input = parse(z.object({ characterId: identifier.nullable() }).strict(), request.body);
1976
+ const updated = store.setAiConversationRoleplayCharacter(request.params.conversationId, input.characterId);
1977
+ const permissions = requestPermissions(request, String(updated.workId));
1978
+ data(response, redactAiConversation(updated, permissions));
1979
+ });
1900
1980
  app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
1901
1981
  const input = parse(z.object({
1902
1982
  role: z.enum(["user", "assistant"]),
@@ -1963,7 +2043,7 @@ export function createRuntime(options) {
1963
2043
  data(response, ai.createProvider(parse(providerSchema, request.body)), 201);
1964
2044
  });
1965
2045
  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))));
2046
+ app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(providerUpdateSchema, request.body))));
1967
2047
  app.delete("/api/providers/:providerId", (request, response) => {
1968
2048
  ai.deleteProvider(request.params.providerId);
1969
2049
  noContent(response);