@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/ai-tool-results.js +13 -1
- package/dist/ai-tool-results.js.map +1 -1
- package/dist/ai.js +4 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +84 -4
- package/dist/app.js.map +1 -1
- package/dist/database.js +167 -7
- package/dist/database.js.map +1 -1
- package/dist/logger.js +2 -2
- package/dist/logger.js.map +1 -1
- package/dist/public/app.js +342 -5
- package/dist/public/global-search.js +12 -3
- package/dist/public/index.html +46 -2
- package/dist/public/s3-backup-ui.d.ts +17 -0
- package/dist/public/s3-backup-ui.js +28 -0
- package/dist/public/styles.css +84 -1
- package/dist/s3-backup.js +654 -0
- package/dist/s3-backup.js.map +1 -0
- package/dist/security.js +25 -0
- package/dist/security.js.map +1 -1
- package/dist/store.js +7 -3
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -1
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).
|
|
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,
|
|
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)
|