@musnows/scriverse 0.6.9 → 0.6.11

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
@@ -12,6 +12,7 @@ import { z, ZodError } from "zod";
12
12
  import { AI_PROVIDER_PROTOCOLS } from "./ai-protocol.js";
13
13
  import { AttachmentStorage } from "./attachment-storage.js";
14
14
  import { AiManager } from "./ai.js";
15
+ import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
15
16
  import { CredentialVault } from "./credential-vault.js";
16
17
  import { Database } from "./database.js";
17
18
  import { assertSafeDocxArchive } from "./docx-security.js";
@@ -23,13 +24,14 @@ import { applyImportFileHints, parseNovelText } from "./parser.js";
23
24
  import { aiConversationTaskTypes, attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
24
25
  import { paginated, parsePagination } from "./pagination.js";
25
26
  import { normalizeUploadFileName } from "./utils.js";
26
- import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
27
+ import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
27
28
  import { ImageCaptchaService } from "./image-captcha.js";
28
29
  import { assertSafeImportedPlainText, decodeUtf8ImportedText } from "./import-security.js";
29
30
  import { InvalidRasterImageError, readRasterImageMetadata } from "./image-metadata.js";
30
31
  import { createRequestLoggingMiddleware, sanitizeRequestPath } from "./http-logging.js";
31
32
  import { accountReference, logger, sanitizeError } from "./logger.js";
32
33
  import { currentRequestActor, runWithRequestActor } from "./request-context.js";
34
+ import { S3BackupManager } from "./s3-backup.js";
33
35
  import { APP_VERSION } from "./version.js";
34
36
  import { ReleaseUpdateChecker } from "./release-update.js";
35
37
  import { canReadWorkModule, canWriteWorkModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
@@ -385,6 +387,50 @@ const platformUiSettingsSchema = z.object({
385
387
  }).strict().refine((input) => input.toastPosition !== undefined || input.pageSizes !== undefined, {
386
388
  message: "至少需要提供一项界面设置"
387
389
  });
390
+ const s3EndpointSchema = z.string().trim().url().max(2_000).superRefine((value, context) => {
391
+ let url;
392
+ try {
393
+ url = new URL(value);
394
+ }
395
+ catch {
396
+ return;
397
+ }
398
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
399
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "S3 服务地址必须使用 HTTP 或 HTTPS" });
400
+ }
401
+ if (url.username || url.password || url.search || url.hash) {
402
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "S3 服务地址不能包含凭据、查询参数或片段" });
403
+ }
404
+ });
405
+ const s3BasePathSchema = z.string().trim().max(512).refine((value) => {
406
+ if (/[\u0000-\u001f\u007f]/u.test(value))
407
+ return false;
408
+ return value.split("/").filter(Boolean).every((segment) => segment !== "." && segment !== "..");
409
+ }, "S3 子目录不能包含控制字符或相对路径段");
410
+ const s3BucketSchema = z.string().trim().min(1).max(255).refine((value) => !/[\/\\\s\u0000-\u001f\u007f]/u.test(value), "S3 桶名称不能包含空白、斜杠或控制字符");
411
+ const s3ScheduleTimeSchema = z.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/u, "备份触发时间必须使用 HH:mm 格式");
412
+ const s3BackupTargetBaseSchema = z.object({
413
+ name: nonEmpty.max(100),
414
+ endpoint: s3EndpointSchema,
415
+ region: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/u, "S3 区域格式无效").optional(),
416
+ bucket: s3BucketSchema,
417
+ basePath: s3BasePathSchema.optional(),
418
+ accessKeyId: z.string().trim().min(1).max(512),
419
+ secretAccessKey: z.string().min(1).max(2_048),
420
+ forcePathStyle: z.boolean().optional(),
421
+ enabled: z.boolean().optional(),
422
+ backupImages: z.boolean().optional(),
423
+ scheduleTime: s3ScheduleTimeSchema.optional(),
424
+ retentionCount: z.number().int().min(1).max(365).optional()
425
+ }).strict();
426
+ const s3BackupTargetUpdateSchema = s3BackupTargetBaseSchema.partial().refine((input) => Object.keys(input).length > 0, "至少需要提供一项 S3 备份配置");
427
+ const s3BackupRunSchema = z.object({
428
+ targetIds: z.array(identifier).min(1).max(100).refine((items) => new Set(items).size === items.length, "S3 备份目标不能重复").optional()
429
+ }).strict();
430
+ const s3BackupRunQuerySchema = z.object({
431
+ afterSequence: z.coerce.number().int().min(0).optional(),
432
+ limit: z.coerce.number().int().min(1).max(100).optional()
433
+ }).strict();
388
434
  const aiToolCallResultSchema = z.object({
389
435
  id: z.string().min(1).max(300),
390
436
  name: z.string().min(1).max(200),
@@ -435,7 +481,7 @@ const workAiSettingsSchema = z.object({
435
481
  autoRunFailureThreshold: z.number().int().min(1).max(10).optional(),
436
482
  bookSummaryContextPercent: z.number().int().min(1).max(90).optional(),
437
483
  contextCompactThreshold: z.number().int().min(50).max(90).optional(),
438
- agentToolCallLimit: z.number().int().min(5).max(48).optional(),
484
+ agentToolCallLimit: z.number().int().min(5).optional(),
439
485
  agentToolCallGlobalMultiplier: z.number().int().min(1).max(6).optional(),
440
486
  agentTools: z.array(z.enum(["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"])).max(7).optional(),
441
487
  alwaysIncludeSettingInfo: z.boolean().optional(),
@@ -861,12 +907,19 @@ export function createRuntime(options) {
861
907
  return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
862
908
  };
863
909
  const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
910
+ const credentialVault = new CredentialVault(options.masterSecret);
911
+ const backups = new S3BackupManager(database, credentialVault, store, attachmentStorage, {
912
+ ...options.backupOptions,
913
+ masterKey: options.masterSecret,
914
+ validateEndpoint: options.backupOptions?.validateEndpoint
915
+ ?? (options.security ? (url) => assertSafeS3Endpoint(url, options.security?.allowPrivateAiEndpoints) : undefined)
916
+ });
864
917
  const releaseUpdateChecker = new ReleaseUpdateChecker(APP_VERSION, options.releaseFetchImpl ?? fetch, {
865
918
  intervalMs: options.releaseCheckIntervalMs,
866
919
  timeoutMs: options.releaseCheckTimeoutMs,
867
920
  retries: options.releaseCheckRetries
868
921
  });
869
- const ai = new AiManager(store, new CredentialVault(options.masterSecret), options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
922
+ const ai = new AiManager(store, credentialVault, options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
870
923
  const requiredModules = analysisTaskReadModules(task.taskType, task.scope);
871
924
  const creator = actor ? null : database.get("SELECT created_by_user_id FROM analysis_tasks WHERE id = ?", String(task.id));
872
925
  const userId = actor?.userId ?? (typeof creator?.created_by_user_id === "string" ? creator.created_by_user_id : null);
@@ -1913,6 +1966,27 @@ export function createRuntime(options) {
1913
1966
  app.patch("/api/platform/ui-settings", (request, response) => {
1914
1967
  data(response, store.updatePlatformUiSettings(parse(platformUiSettingsSchema, request.body)));
1915
1968
  });
1969
+ app.get("/api/platform/backups/targets", (_request, response) => data(response, backups.listTargets()));
1970
+ app.post("/api/platform/backups/targets", (request, response) => {
1971
+ data(response, backups.createTarget(parse(s3BackupTargetBaseSchema, request.body)), 201);
1972
+ });
1973
+ app.patch("/api/platform/backups/targets/:targetId", (request, response) => {
1974
+ data(response, backups.updateTarget(request.params.targetId, parse(s3BackupTargetUpdateSchema, request.body)));
1975
+ });
1976
+ app.delete("/api/platform/backups/targets/:targetId", (request, response) => {
1977
+ backups.deleteTarget(request.params.targetId);
1978
+ noContent(response);
1979
+ });
1980
+ app.get("/api/platform/backups/runs", (request, response) => {
1981
+ data(response, backups.listRuns(parse(s3BackupRunQuerySchema, request.query)));
1982
+ });
1983
+ app.post("/api/platform/backups/run", (request, response) => {
1984
+ const input = parse(s3BackupRunSchema, request.body ?? {});
1985
+ const queued = input.targetIds
1986
+ ? backups.enqueueTargets(input.targetIds, "manual")
1987
+ : backups.enqueueEnabledTargets("manual");
1988
+ data(response, { ...queued, queuedAt: new Date().toISOString() }, 202);
1989
+ });
1916
1990
  app.get("/api/works/:workId/ai-settings", (request, response) => data(response, store.getWorkAiSettings(request.params.workId)));
1917
1991
  app.get("/api/works/:workId/ai-settings/usage", (request, response) => {
1918
1992
  const query = parse(aiUsageQuerySchema, request.query);
@@ -1940,6 +2014,10 @@ export function createRuntime(options) {
1940
2014
  app.patch("/api/works/:workId/ai-settings", (request, response) => {
1941
2015
  const workId = request.params.workId;
1942
2016
  const input = parse(workAiSettingsSchema, request.body);
2017
+ const maximumAgentToolCallLimit = resolveMaxAgentToolCallLimit();
2018
+ if (input.agentToolCallLimit !== undefined && input.agentToolCallLimit > maximumAgentToolCallLimit) {
2019
+ throw new AppError(400, "AGENT_TOOL_CALL_LIMIT_TOO_HIGH", `Agent 工具调用上限不能超过 ${maximumAgentToolCallLimit} 次`);
2020
+ }
1943
2021
  if (input.titleGenerationModelId)
1944
2022
  ai.assertModelAvailable(input.titleGenerationModelId);
1945
2023
  if (input.imageToolModelId)
@@ -2445,9 +2523,11 @@ export function createRuntime(options) {
2445
2523
  logger.error("http.request.unhandled_error", commonFields);
2446
2524
  response.status(500).json({ error: { code: "INTERNAL_ERROR", message: "服务器内部错误" } });
2447
2525
  });
2526
+ backups.startScheduler();
2448
2527
  logger.info("runtime.ready", { serveUi: options.serveUi ?? true });
2449
- return { app, database, store, ai, auth, attachmentStorage, cleanupAttachments, close: () => {
2528
+ return { app, database, store, ai, backups, auth, attachmentStorage, cleanupAttachments, close: () => {
2450
2529
  logger.info("runtime.closing");
2530
+ backups.dispose();
2451
2531
  ai.dispose();
2452
2532
  database.close();
2453
2533
  if (temporaryAttachmentRoot)