@musnows/scriverse 0.8.5 → 0.8.7
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/README.md +1 -0
- package/dist/ai-model-pricing.js +530 -0
- package/dist/ai-model-pricing.js.map +1 -0
- package/dist/ai-protocol.js +335 -15
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-retry.js +1 -1
- package/dist/ai-retry.js.map +1 -1
- package/dist/ai.js +1083 -139
- package/dist/ai.js.map +1 -1
- package/dist/app.js +222 -25
- package/dist/app.js.map +1 -1
- package/dist/attachment-download.js +26 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/attachment-storage.js +8 -6
- package/dist/attachment-storage.js.map +1 -1
- package/dist/cli-contract.js +6 -4
- package/dist/cli-contract.js.map +1 -1
- package/dist/database.js +363 -4
- package/dist/database.js.map +1 -1
- package/dist/public/ai-image-attachments.d.ts +21 -0
- package/dist/public/ai-image-attachments.js +42 -0
- package/dist/public/ai-usage.d.ts +1 -0
- package/dist/public/ai-usage.js +11 -0
- package/dist/public/app.js +1030 -106
- package/dist/public/display-labels.d.ts +2 -1
- package/dist/public/display-labels.js +6 -6
- package/dist/public/index.html +42 -7
- package/dist/public/model-config.d.ts +6 -4
- package/dist/public/model-config.js +10 -2
- package/dist/public/styles.css +157 -6
- package/dist/s3-backup.js +21 -0
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +2 -1
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +6 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +550 -44
- package/dist/store.js.map +1 -1
- package/dist/upload-limits.js +10 -4
- package/dist/upload-limits.js.map +1 -1
- package/dist/user-auth.js +3 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +15 -0
- package/dist/writing-progress-time.js.map +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -9,12 +9,14 @@ 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, MAX_TOKENS_PARAMETERS } from "./ai-protocol.js";
|
|
12
|
+
import { AI_PROVIDER_PROTOCOL_OPTIONS, AI_PROVIDER_PROTOCOLS, AI_THINKING_TYPES, MAX_TOKENS_PARAMETERS } from "./ai-protocol.js";
|
|
13
13
|
import { aiConversationExportContentDisposition, exportAiConversationMarkdown } from "./ai-conversation-export.js";
|
|
14
14
|
import { DEFAULT_AI_CHAT_TAB_LIMIT } from "./ai-chat-tab-limit.js";
|
|
15
15
|
import { MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS, MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
16
16
|
import { AttachmentStorage } from "./attachment-storage.js";
|
|
17
|
+
import { attachmentDownloadFileName, inlineContentDisposition } from "./attachment-download.js";
|
|
17
18
|
import { AiManager } from "./ai.js";
|
|
19
|
+
import { LiteLlmPriceCache } from "./ai-model-pricing.js";
|
|
18
20
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
19
21
|
import { CHARACTER_EXTRACTION_MAX_ALIASES, CHARACTER_EXTRACTION_MAX_CANDIDATES, CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH, CHARACTER_EXTRACTION_MAX_NAME_LENGTH, CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH } from "./character-extraction.js";
|
|
20
22
|
import { CredentialVault } from "./credential-vault.js";
|
|
@@ -39,7 +41,7 @@ import { currentRequestActor, runWithRequestActor } from "./request-context.js";
|
|
|
39
41
|
import { S3BackupManager } from "./s3-backup.js";
|
|
40
42
|
import { APP_VERSION } from "./version.js";
|
|
41
43
|
import { ReleaseUpdateChecker } from "./release-update.js";
|
|
42
|
-
import { DEFAULT_IMAGE_UPLOAD_LIMITS, formatUploadLimit } from "./upload-limits.js";
|
|
44
|
+
import { CHARACTER_AVATAR_IMAGE_MAX_BYTES, DEFAULT_IMAGE_UPLOAD_LIMITS, formatUploadLimit } from "./upload-limits.js";
|
|
43
45
|
import { canReadWorkModule, canWriteWorkModule, chapterAnnotationPermissionModule, fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
|
|
44
46
|
import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, presencePageKinds } from "./collaboration-presence.js";
|
|
45
47
|
import { PresenceStore } from "./presence-store.js";
|
|
@@ -60,6 +62,15 @@ const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
|
60
62
|
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
61
63
|
const maximumImportedTextLength = 20_000_000;
|
|
62
64
|
const maximumKnowledgeSectionsLength = 4_000_000;
|
|
65
|
+
const aiChatAttachmentIngestOptions = {
|
|
66
|
+
allowedFormats: new Set(["png", "jpeg"]),
|
|
67
|
+
preserveFormat: true,
|
|
68
|
+
unsupportedMessage: "AI 对话图片附件仅支持 PNG、JPG、JPEG 图片"
|
|
69
|
+
};
|
|
70
|
+
const characterAvatarIngestOptions = {
|
|
71
|
+
allowedFormats: new Set(["png", "jpeg", "webp"]),
|
|
72
|
+
unsupportedMessage: "角色头像仅支持 PNG、JPEG 和 WebP 图片"
|
|
73
|
+
};
|
|
63
74
|
function stableJson(value) {
|
|
64
75
|
if (Array.isArray(value))
|
|
65
76
|
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
@@ -77,14 +88,20 @@ function assertImageUploadSize(byteLength, maximumBytes, message) {
|
|
|
77
88
|
return;
|
|
78
89
|
throw new AppError(413, "IMAGE_TOO_LARGE", message);
|
|
79
90
|
}
|
|
80
|
-
function uploadSizeError(pathname, limits) {
|
|
91
|
+
function uploadSizeError(pathname, limits, module = "") {
|
|
81
92
|
if (pathname === "/api/auth/avatar")
|
|
82
93
|
return { code: "IMAGE_TOO_LARGE", message: `头像图片不能超过 ${formatUploadLimit(limits.avatarBytes)}` };
|
|
94
|
+
if (/^\/api\/characters\/[^/]+\/avatar$/u.test(pathname)) {
|
|
95
|
+
return { code: "CHARACTER_AVATAR_TOO_LARGE", message: `角色头像不能超过 ${formatUploadLimit(CHARACTER_AVATAR_IMAGE_MAX_BYTES)}` };
|
|
96
|
+
}
|
|
83
97
|
if (/^\/api\/works\/[^/]+\/cover$/u.test(pathname)) {
|
|
84
98
|
return { code: "IMAGE_TOO_LARGE", message: `封面图片不能超过 ${formatUploadLimit(limits.coverBytes)}` };
|
|
85
99
|
}
|
|
86
100
|
if (/^\/api\/works\/[^/]+\/attachments$/u.test(pathname)) {
|
|
87
|
-
|
|
101
|
+
const maximumBytes = module === "ai-chat"
|
|
102
|
+
? limits.chatImageBytes
|
|
103
|
+
: limits.attachmentBytes;
|
|
104
|
+
return { code: "ATTACHMENT_TOO_LARGE", message: `图片附件不能超过 ${formatUploadLimit(maximumBytes)}` };
|
|
88
105
|
}
|
|
89
106
|
return null;
|
|
90
107
|
}
|
|
@@ -355,11 +372,14 @@ const providerBaseSchema = z.object({
|
|
|
355
372
|
baseUrl: z.string().url().refine((value) => value.startsWith("http://") || value.startsWith("https://"), "接口地址必须使用 HTTP 或 HTTPS"),
|
|
356
373
|
apiKey: z.string().trim().min(1).max(50_000),
|
|
357
374
|
protocol: z.enum(AI_PROVIDER_PROTOCOLS).optional(),
|
|
375
|
+
thinkingType: z.enum(AI_THINKING_TYPES).optional(),
|
|
358
376
|
maxTokensParameter: z.enum(MAX_TOKENS_PARAMETERS).optional(),
|
|
359
377
|
status: z.enum(["enabled", "disabled"]).optional(),
|
|
360
378
|
note: z.string().max(10_000).optional(),
|
|
361
379
|
concurrencyLimit: z.number().int().min(1).max(100).optional(),
|
|
362
|
-
rpmLimit: z.number().int().min(1).max(10_000).optional()
|
|
380
|
+
rpmLimit: z.number().int().min(1).max(10_000).optional(),
|
|
381
|
+
dailyTokenQuota: z.number().int().min(1, "Token 额度必须设置大于 0").max(2_000_000_000).nullable().optional(),
|
|
382
|
+
monthlyTokenQuota: z.number().int().min(1, "Token 额度必须设置大于 0").max(2_000_000_000).nullable().optional()
|
|
363
383
|
});
|
|
364
384
|
function refineProviderApiKey(value, ctx) {
|
|
365
385
|
if (value.protocol === "google-vertex" && value.baseUrl && !isOfficialGoogleVertexBaseUrl(value.baseUrl)) {
|
|
@@ -408,7 +428,7 @@ const modelSchema = z.object({
|
|
|
408
428
|
outputNote: z.string().max(10_000).optional(),
|
|
409
429
|
preset: jsonObject.optional(),
|
|
410
430
|
thinkingEnabled: z.boolean().optional(),
|
|
411
|
-
thinkingEffort: z.enum(["default", "low", "medium", "high", "xhigh", "max"]).optional(),
|
|
431
|
+
thinkingEffort: z.enum(["default", "auto", "low", "medium", "high", "xhigh", "max"]).optional(),
|
|
412
432
|
multimodalEnabled: z.boolean().optional(),
|
|
413
433
|
imageToolDefault: z.boolean().optional(),
|
|
414
434
|
enabled: z.boolean().optional(),
|
|
@@ -422,6 +442,13 @@ const aiPromptSchema = z.object({
|
|
|
422
442
|
const aiUsageQuerySchema = z.object({
|
|
423
443
|
timezoneOffset: z.coerce.number().int().min(-840).max(840).default(0)
|
|
424
444
|
}).strict();
|
|
445
|
+
const adminAiConversationQuerySchema = z.object({
|
|
446
|
+
page: z.string().optional(),
|
|
447
|
+
limit: z.string().optional(),
|
|
448
|
+
q: z.string().trim().max(200).optional(),
|
|
449
|
+
workId: identifier.optional(),
|
|
450
|
+
userId: identifier.optional()
|
|
451
|
+
}).strict();
|
|
425
452
|
const platformPageSizesSchema = z.object({
|
|
426
453
|
drafts: z.number().int().min(10).max(100).optional(),
|
|
427
454
|
settings: z.number().int().min(10).max(100).optional(),
|
|
@@ -544,7 +571,8 @@ const aiProcessStepSchema = z.discriminatedUnion("type", [
|
|
|
544
571
|
]);
|
|
545
572
|
const workAiSettingsSchema = z.object({
|
|
546
573
|
systemPrompt: z.string().max(100_000).optional(),
|
|
547
|
-
dailyTokenQuota: z.number().int().min(
|
|
574
|
+
dailyTokenQuota: z.number().int().min(1, "Token 额度必须设置大于 0").max(2_000_000_000).nullable().optional(),
|
|
575
|
+
monthlyTokenQuota: z.number().int().min(1, "Token 额度必须设置大于 0").max(2_000_000_000).nullable().optional(),
|
|
548
576
|
autoRunEnabled: z.boolean().optional(),
|
|
549
577
|
autoRunConcurrency: z.number().int().min(1).max(8).optional(),
|
|
550
578
|
autoRunBatchLimit: z.number().int().min(1).max(200).optional(),
|
|
@@ -1004,10 +1032,30 @@ export function publicAiStreamError(error) {
|
|
|
1004
1032
|
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
1005
1033
|
? error.details
|
|
1006
1034
|
: null;
|
|
1035
|
+
const publicQuotaDetails = details?.platformLimited === true
|
|
1036
|
+
? Object.fromEntries([
|
|
1037
|
+
"platformLimited",
|
|
1038
|
+
"limitScope",
|
|
1039
|
+
"limitPeriod",
|
|
1040
|
+
"workId",
|
|
1041
|
+
"providerId",
|
|
1042
|
+
"providerName",
|
|
1043
|
+
"dailyTokenQuota",
|
|
1044
|
+
"monthlyTokenQuota",
|
|
1045
|
+
"usedTokens",
|
|
1046
|
+
"remainingTokens",
|
|
1047
|
+
"estimatedInputTokens",
|
|
1048
|
+
"resetsAt",
|
|
1049
|
+
"timezone",
|
|
1050
|
+
"dayStartedAt",
|
|
1051
|
+
"monthStartedAt"
|
|
1052
|
+
].filter((key) => details[key] !== undefined).map((key) => [key, details[key]]))
|
|
1053
|
+
: undefined;
|
|
1007
1054
|
return {
|
|
1008
1055
|
code: error.code,
|
|
1009
1056
|
message: error.message,
|
|
1010
1057
|
status: error.status,
|
|
1058
|
+
...(publicQuotaDetails ? { details: publicQuotaDetails } : {}),
|
|
1011
1059
|
...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
1012
1060
|
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
1013
1061
|
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
@@ -1049,11 +1097,14 @@ export function createRuntime(options) {
|
|
|
1049
1097
|
});
|
|
1050
1098
|
const database = new Database(options.databasePath);
|
|
1051
1099
|
const bootId = randomUUID();
|
|
1052
|
-
const
|
|
1053
|
-
? mkdtempSync(join(tmpdir(), "scriverse-
|
|
1100
|
+
const temporaryStorageRoot = options.databasePath === ":memory:" && (!options.attachmentDirectory || !options.characterAvatarDirectory)
|
|
1101
|
+
? mkdtempSync(join(tmpdir(), "scriverse-storage-"))
|
|
1054
1102
|
: null;
|
|
1055
|
-
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ??
|
|
1103
|
+
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ?? temporaryStorageRoot ?? join(dirname(options.databasePath), "attachments"), uploadLimits.attachmentBytes);
|
|
1056
1104
|
mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
1105
|
+
const characterAvatarStorage = new AttachmentStorage(options.characterAvatarDirectory
|
|
1106
|
+
?? (temporaryStorageRoot ? join(temporaryStorageRoot, "character-avatars") : join(dirname(options.databasePath), "character-avatars")), CHARACTER_AVATAR_IMAGE_MAX_BYTES);
|
|
1107
|
+
mkdirSync(characterAvatarStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
1057
1108
|
const auth = new UserAuthService(database);
|
|
1058
1109
|
const collaborationPresence = new CollaborationPresence(45_000, Date.now, 120_000, 50, { store: new PresenceStore(database) });
|
|
1059
1110
|
const publishCollaborativeChange = (workId, pageKey, options = {}) => {
|
|
@@ -1113,6 +1164,13 @@ export function createRuntime(options) {
|
|
|
1113
1164
|
attachmentCleanupChain = cleanup.catch(() => undefined);
|
|
1114
1165
|
return cleanup;
|
|
1115
1166
|
};
|
|
1167
|
+
const cleanupCharacterAvatarFiles = async (storageKeys) => {
|
|
1168
|
+
for (const storageKey of new Set(storageKeys)) {
|
|
1169
|
+
if (store.characterAvatarStorageKeyInUse(storageKey))
|
|
1170
|
+
continue;
|
|
1171
|
+
await characterAvatarStorage.remove(storageKey);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1116
1174
|
const requestPermissions = (request, workId) => {
|
|
1117
1175
|
if (!request.authUser)
|
|
1118
1176
|
return fullWorkModulePermissions();
|
|
@@ -1121,19 +1179,28 @@ export function createRuntime(options) {
|
|
|
1121
1179
|
return fullWorkModulePermissions();
|
|
1122
1180
|
return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
|
|
1123
1181
|
};
|
|
1182
|
+
const assertRequestAiConversationOwner = (request, conversationId) => {
|
|
1183
|
+
if (request.authUser)
|
|
1184
|
+
store.assertAiConversationOwner(conversationId, request.authUser.userId);
|
|
1185
|
+
};
|
|
1124
1186
|
const resolveConversationModelId = (workId, conversationId, requestedModelId) => {
|
|
1125
1187
|
if (!conversationId)
|
|
1126
1188
|
return requestedModelId;
|
|
1127
1189
|
const lockedModelId = store.getAiConversationLockedModelId(conversationId, workId);
|
|
1190
|
+
const hasImageAttachments = store.getAiConversationHasImageAttachments(conversationId, workId);
|
|
1128
1191
|
if (lockedModelId && requestedModelId && lockedModelId !== requestedModelId) {
|
|
1129
1192
|
throw new AppError(409, "AI_CONVERSATION_MODEL_LOCKED", "当前对话已经锁定模型,请新建对话后再切换模型");
|
|
1130
1193
|
}
|
|
1194
|
+
if (hasImageAttachments && !lockedModelId && requestedModelId) {
|
|
1195
|
+
throw new AppError(409, "AI_CONVERSATION_IMAGE_MODEL_LOCKED", "当前对话链路包含图片但没有可继承的模型,请新建对话后再选择模型");
|
|
1196
|
+
}
|
|
1131
1197
|
return lockedModelId ?? requestedModelId;
|
|
1132
1198
|
};
|
|
1133
1199
|
const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
|
|
1134
1200
|
const credentialVault = new CredentialVault(options.masterSecret);
|
|
1135
1201
|
const backups = new S3BackupManager(database, credentialVault, store, attachmentStorage, {
|
|
1136
1202
|
...options.backupOptions,
|
|
1203
|
+
characterAvatarStorage,
|
|
1137
1204
|
masterKey: options.masterSecret,
|
|
1138
1205
|
validateEndpoint: options.backupOptions?.validateEndpoint
|
|
1139
1206
|
?? (options.security ? (url) => assertSafeS3Endpoint(url, options.security?.allowPrivateAiEndpoints) : undefined)
|
|
@@ -1143,6 +1210,11 @@ export function createRuntime(options) {
|
|
|
1143
1210
|
timeoutMs: options.releaseCheckTimeoutMs,
|
|
1144
1211
|
retries: options.releaseCheckRetries
|
|
1145
1212
|
});
|
|
1213
|
+
const liteLlmPriceCache = options.liteLlmPriceCache ?? new LiteLlmPriceCache({
|
|
1214
|
+
...(options.liteLlmPriceCachePath ? { cachePath: options.liteLlmPriceCachePath } : {})
|
|
1215
|
+
});
|
|
1216
|
+
if (!options.liteLlmPriceCache && options.liteLlmPriceCachePath)
|
|
1217
|
+
liteLlmPriceCache.start();
|
|
1146
1218
|
const ai = new AiManager(store, credentialVault, options.fetchImpl ?? fetch, options.developmentServer === true
|
|
1147
1219
|
? undefined
|
|
1148
1220
|
: options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
|
|
@@ -1161,7 +1233,9 @@ export function createRuntime(options) {
|
|
|
1161
1233
|
}, attachmentStorage, {
|
|
1162
1234
|
interactiveStreamIdleTimeoutMs: options.aiStreamIdleTimeoutMs ?? platformAiStreamIdleTimeoutMs,
|
|
1163
1235
|
retryPolicy: options.aiRetryPolicy,
|
|
1164
|
-
retrySleep: options.aiRetrySleep
|
|
1236
|
+
retrySleep: options.aiRetrySleep,
|
|
1237
|
+
aiChatImageMaxBytes: uploadLimits.chatImageBytes,
|
|
1238
|
+
liteLlmPriceCache
|
|
1165
1239
|
});
|
|
1166
1240
|
const app = express();
|
|
1167
1241
|
enforceCaseInsensitiveRouting(app);
|
|
@@ -1177,12 +1251,19 @@ export function createRuntime(options) {
|
|
|
1177
1251
|
storage: multer.memoryStorage(),
|
|
1178
1252
|
limits: { fileSize: uploadLimits.avatarBytes + 1, files: 1, fields: 1, fieldSize: 1024, parts: 2, headerPairs: 50 }
|
|
1179
1253
|
});
|
|
1254
|
+
const characterAvatarUpload = multer({
|
|
1255
|
+
storage: multer.diskStorage({
|
|
1256
|
+
destination: characterAvatarStorage.temporaryDirectory,
|
|
1257
|
+
filename: (_request, _file, callback) => callback(null, randomUUID())
|
|
1258
|
+
}),
|
|
1259
|
+
limits: { fileSize: CHARACTER_AVATAR_IMAGE_MAX_BYTES + 1, files: 1, fields: 0, fieldSize: 1024, parts: 2, headerPairs: 50 }
|
|
1260
|
+
});
|
|
1180
1261
|
const attachmentUpload = multer({
|
|
1181
1262
|
storage: multer.diskStorage({
|
|
1182
1263
|
destination: attachmentStorage.temporaryDirectory,
|
|
1183
1264
|
filename: (_request, _file, callback) => callback(null, randomUUID())
|
|
1184
1265
|
}),
|
|
1185
|
-
limits: { fileSize: uploadLimits.attachmentBytes + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
1266
|
+
limits: { fileSize: Math.max(uploadLimits.attachmentBytes, uploadLimits.chatImageBytes) + 1, files: 1, fields: 4, fieldSize: 16 * 1024, parts: 5, headerPairs: 100 }
|
|
1186
1267
|
});
|
|
1187
1268
|
app.disable("x-powered-by");
|
|
1188
1269
|
const trustProxy = resolveTrustProxySetting(options.security?.trustProxy);
|
|
@@ -1396,7 +1477,9 @@ export function createRuntime(options) {
|
|
|
1396
1477
|
if (request.authUser)
|
|
1397
1478
|
auth.assertDeletedWorkAccess(request.authUser, request.params.workId, request.authMethod !== "api-key");
|
|
1398
1479
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1480
|
+
const characterAvatarStorageKeys = store.listCharacterAvatarStorageKeysForWork(request.params.workId);
|
|
1399
1481
|
store.permanentlyDeleteWork(request.params.workId, input.expectedVersionNo);
|
|
1482
|
+
await cleanupCharacterAvatarFiles(characterAvatarStorageKeys);
|
|
1400
1483
|
await cleanupAttachments();
|
|
1401
1484
|
noContent(response);
|
|
1402
1485
|
});
|
|
@@ -1548,11 +1631,11 @@ export function createRuntime(options) {
|
|
|
1548
1631
|
data(response, store.replaceWorkText(request.params.workId, parse(globalReplaceSchema, request.body)));
|
|
1549
1632
|
});
|
|
1550
1633
|
app.post("/api/works/:workId/volumes", (request, response) => {
|
|
1551
|
-
const input = parse(z.object({ title: nonEmpty.max(200), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional() }), request.body);
|
|
1634
|
+
const input = parse(z.object({ title: nonEmpty.max(200), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), storyOrder: z.number().int().min(0).max(1_000_000).optional() }).strict(), request.body);
|
|
1552
1635
|
data(response, store.createVolume(request.params.workId, input), 201);
|
|
1553
1636
|
});
|
|
1554
1637
|
app.patch("/api/volumes/:volumeId", (request, response) => {
|
|
1555
|
-
const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional(), expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
|
|
1638
|
+
const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional(), storyOrder: z.number().int().min(0).max(1_000_000).optional(), expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
|
|
1556
1639
|
const { expectedVersionNo, changeNote, ...volumeInput } = input;
|
|
1557
1640
|
data(response, store.updateVolume(request.params.volumeId, volumeInput, expectedVersionNo, "manual", null, changeNote));
|
|
1558
1641
|
});
|
|
@@ -1839,6 +1922,60 @@ export function createRuntime(options) {
|
|
|
1839
1922
|
app.get("/api/characters/:characterId", (request, response) => {
|
|
1840
1923
|
data(response, redactCharacterLinks(store.getCharacter(request.params.characterId), requestPermissions(request)));
|
|
1841
1924
|
});
|
|
1925
|
+
app.put("/api/characters/:characterId/avatar", characterAvatarUpload.single("file"), async (request, response) => {
|
|
1926
|
+
if (!request.file)
|
|
1927
|
+
throw new AppError(400, "FILE_REQUIRED", "请选择 PNG、JPEG 或 WebP 角色头像");
|
|
1928
|
+
const characterId = String(request.params.characterId);
|
|
1929
|
+
let stored = null;
|
|
1930
|
+
try {
|
|
1931
|
+
stored = await characterAvatarStorage.ingest(request.file.path, characterAvatarIngestOptions);
|
|
1932
|
+
const result = store.setCharacterAvatar(characterId, {
|
|
1933
|
+
mimeType: stored.storedMimeType,
|
|
1934
|
+
byteLength: stored.storedByteLength,
|
|
1935
|
+
sha256: stored.storedSha256,
|
|
1936
|
+
storageKey: stored.storageKey,
|
|
1937
|
+
width: stored.width,
|
|
1938
|
+
height: stored.height
|
|
1939
|
+
});
|
|
1940
|
+
if (result.previousStorageKey
|
|
1941
|
+
&& result.previousStorageKey !== stored.storageKey
|
|
1942
|
+
&& !store.characterAvatarStorageKeyInUse(result.previousStorageKey)) {
|
|
1943
|
+
await characterAvatarStorage.remove(result.previousStorageKey);
|
|
1944
|
+
}
|
|
1945
|
+
data(response, redactCharacterLinks(result.character, requestPermissions(request)));
|
|
1946
|
+
}
|
|
1947
|
+
catch (error) {
|
|
1948
|
+
if (error instanceof AppError && error.code === "ATTACHMENT_TOO_LARGE") {
|
|
1949
|
+
throw new AppError(413, "CHARACTER_AVATAR_TOO_LARGE", `角色头像不能超过 ${formatUploadLimit(CHARACTER_AVATAR_IMAGE_MAX_BYTES)}`);
|
|
1950
|
+
}
|
|
1951
|
+
if (stored && !store.characterAvatarStorageKeyInUse(stored.storageKey)) {
|
|
1952
|
+
await characterAvatarStorage.remove(stored.storageKey);
|
|
1953
|
+
}
|
|
1954
|
+
throw error;
|
|
1955
|
+
}
|
|
1956
|
+
finally {
|
|
1957
|
+
await rm(request.file.path, { force: true });
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
app.get("/api/characters/:characterId/avatar", async (request, response) => {
|
|
1961
|
+
const avatar = store.getCharacterAvatar(request.params.characterId);
|
|
1962
|
+
if (!avatar)
|
|
1963
|
+
throw new AppError(404, "CHARACTER_AVATAR_NOT_FOUND", "角色头像不存在");
|
|
1964
|
+
const content = await characterAvatarStorage.read(avatar.storageKey);
|
|
1965
|
+
response.setHeader("Content-Type", avatar.mimeType);
|
|
1966
|
+
response.setHeader("Content-Length", String(content.byteLength));
|
|
1967
|
+
response.setHeader("ETag", `\"${avatar.sha256}\"`);
|
|
1968
|
+
response.setHeader("Cache-Control", "private, max-age=31536000, immutable");
|
|
1969
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1970
|
+
response.send(content);
|
|
1971
|
+
});
|
|
1972
|
+
app.delete("/api/characters/:characterId/avatar", async (request, response) => {
|
|
1973
|
+
const result = store.deleteCharacterAvatar(request.params.characterId);
|
|
1974
|
+
if (result.storageKey && !store.characterAvatarStorageKeyInUse(result.storageKey)) {
|
|
1975
|
+
await characterAvatarStorage.remove(result.storageKey);
|
|
1976
|
+
}
|
|
1977
|
+
data(response, redactCharacterLinks(result.character, requestPermissions(request)));
|
|
1978
|
+
});
|
|
1842
1979
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
1843
1980
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
1844
1981
|
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
@@ -1856,10 +1993,14 @@ export function createRuntime(options) {
|
|
|
1856
1993
|
const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
|
|
1857
1994
|
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
1858
1995
|
});
|
|
1859
|
-
app.delete("/api/characters/:characterId", (request, response) => {
|
|
1996
|
+
app.delete("/api/characters/:characterId", async (request, response) => {
|
|
1860
1997
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1861
1998
|
const character = store.getCharacter(request.params.characterId);
|
|
1999
|
+
const avatar = store.getCharacterAvatar(request.params.characterId);
|
|
1862
2000
|
store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
|
|
2001
|
+
if (avatar && !store.characterAvatarStorageKeyInUse(avatar.storageKey)) {
|
|
2002
|
+
await characterAvatarStorage.remove(avatar.storageKey);
|
|
2003
|
+
}
|
|
1863
2004
|
publishEntityChange(String(character.workId), "character", String(character.id), deletedPageChange);
|
|
1864
2005
|
noContent(response);
|
|
1865
2006
|
});
|
|
@@ -1929,9 +2070,18 @@ export function createRuntime(options) {
|
|
|
1929
2070
|
if (!request.file)
|
|
1930
2071
|
throw new AppError(400, "FILE_REQUIRED", "请选择要上传的图片附件");
|
|
1931
2072
|
const accessModule = parse(attachmentPermissionModuleSchema, request.query.module ?? "settings");
|
|
2073
|
+
if (!canWriteWorkModule(requestPermissions(request, String(request.params.workId)), accessModule)) {
|
|
2074
|
+
throw new AppError(403, "WORK_MODULE_WRITE_DENIED", "你没有编辑该资料模块的权限");
|
|
2075
|
+
}
|
|
2076
|
+
const maximumUploadBytes = accessModule === "ai-chat" ? uploadLimits.chatImageBytes : uploadLimits.attachmentBytes;
|
|
1932
2077
|
let storageKey = null;
|
|
1933
2078
|
try {
|
|
1934
|
-
|
|
2079
|
+
if (request.file.size > maximumUploadBytes) {
|
|
2080
|
+
throw new AppError(413, "ATTACHMENT_TOO_LARGE", `图片附件不能超过 ${formatUploadLimit(maximumUploadBytes)}`);
|
|
2081
|
+
}
|
|
2082
|
+
const stored = await attachmentStorage.ingest(request.file.path, accessModule === "ai-chat"
|
|
2083
|
+
? { ...aiChatAttachmentIngestOptions, maximumUploadBytes }
|
|
2084
|
+
: undefined);
|
|
1935
2085
|
storageKey = stored.storageKey;
|
|
1936
2086
|
const result = store.createAttachment(String(request.params.workId), {
|
|
1937
2087
|
originalName: normalizeUploadFileName(request.file.originalname),
|
|
@@ -1958,8 +2108,10 @@ export function createRuntime(options) {
|
|
|
1958
2108
|
throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取该附件所属资料模块的权限");
|
|
1959
2109
|
}
|
|
1960
2110
|
const content = await attachmentStorage.read(String(attachment.storageKey));
|
|
2111
|
+
const fileName = attachmentDownloadFileName(store.getAttachmentDownloadContextName(String(attachment.id)), String(attachment.originalName));
|
|
1961
2112
|
response.setHeader("Content-Type", String(attachment.storedMimeType));
|
|
1962
2113
|
response.setHeader("Content-Length", String(attachment.storedByteLength));
|
|
2114
|
+
response.setHeader("Content-Disposition", inlineContentDisposition(fileName));
|
|
1963
2115
|
response.setHeader("ETag", `"${String(attachment.storedSha256)}"`);
|
|
1964
2116
|
response.setHeader("Cache-Control", "private, max-age=31536000, immutable");
|
|
1965
2117
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -2315,6 +2467,7 @@ export function createRuntime(options) {
|
|
|
2315
2467
|
const pagination = parsePagination(request.query);
|
|
2316
2468
|
data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
|
|
2317
2469
|
});
|
|
2470
|
+
app.get("/api/platform/ai/protocols", (_request, response) => data(response, AI_PROVIDER_PROTOCOL_OPTIONS));
|
|
2318
2471
|
app.post("/api/platform/ai/providers", (request, response) => data(response, ai.createProvider(parse(providerSchema, request.body)), 201));
|
|
2319
2472
|
app.get("/api/platform/ai/models", (request, response) => {
|
|
2320
2473
|
const pagination = parsePagination(request.query);
|
|
@@ -2333,6 +2486,32 @@ export function createRuntime(options) {
|
|
|
2333
2486
|
const query = parse(aiUsageQuerySchema, request.query);
|
|
2334
2487
|
data(response, ai.getPlatformTokenUsage(query.timezoneOffset));
|
|
2335
2488
|
});
|
|
2489
|
+
app.post("/api/platform/ai/usage/pricing/refresh", async (request, response) => {
|
|
2490
|
+
if (!request.authUser)
|
|
2491
|
+
throw new AppError(401, "AUTH_REQUIRED", "请先登录");
|
|
2492
|
+
if (request.authUser.role !== "admin")
|
|
2493
|
+
throw new AppError(403, "ADMIN_REQUIRED", "该操作仅限系统管理员");
|
|
2494
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
2495
|
+
const refreshed = await liteLlmPriceCache.refresh();
|
|
2496
|
+
if (!refreshed) {
|
|
2497
|
+
throw new AppError(502, "MODEL_PRICE_REFRESH_FAILED", "模型价格刷新失败,历史缓存未改变");
|
|
2498
|
+
}
|
|
2499
|
+
data(response, {
|
|
2500
|
+
refreshed: true,
|
|
2501
|
+
pricingAvailable: liteLlmPriceCache.hasData(),
|
|
2502
|
+
modelCount: liteLlmPriceCache.getPriceTable().size,
|
|
2503
|
+
sources: liteLlmPriceCache.getSourceStatuses()
|
|
2504
|
+
});
|
|
2505
|
+
});
|
|
2506
|
+
app.get("/api/platform/ai-conversations", (request, response) => {
|
|
2507
|
+
const query = parse(adminAiConversationQuerySchema, request.query);
|
|
2508
|
+
const pagination = parsePagination(request.query) ?? { page: 1, limit: 30, offset: 0 };
|
|
2509
|
+
data(response, store.listAdminAiConversationsPage(pagination, {
|
|
2510
|
+
query: query.q,
|
|
2511
|
+
workId: query.workId,
|
|
2512
|
+
userId: query.userId
|
|
2513
|
+
}));
|
|
2514
|
+
});
|
|
2336
2515
|
app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
|
|
2337
2516
|
app.get("/api/platform/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
|
|
2338
2517
|
app.patch("/api/platform/ui-settings", (request, response) => {
|
|
@@ -2423,7 +2602,7 @@ export function createRuntime(options) {
|
|
|
2423
2602
|
limit: request.query.limit ?? "20"
|
|
2424
2603
|
}) ?? { page: 1, limit: 20, offset: 0 };
|
|
2425
2604
|
const permissions = requestPermissions(request, request.params.workId);
|
|
2426
|
-
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
2605
|
+
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination, request.authUser?.userId), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
2427
2606
|
});
|
|
2428
2607
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
2429
2608
|
const input = parse(z.object({
|
|
@@ -2432,6 +2611,10 @@ export function createRuntime(options) {
|
|
|
2432
2611
|
}).strict(), request.body ?? {});
|
|
2433
2612
|
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
2434
2613
|
});
|
|
2614
|
+
app.use("/api/ai-conversations/:conversationId", (request, _response, next) => {
|
|
2615
|
+
assertRequestAiConversationOwner(request, request.params.conversationId);
|
|
2616
|
+
next();
|
|
2617
|
+
});
|
|
2435
2618
|
app.get("/api/ai-conversations/:conversationId", (request, response) => {
|
|
2436
2619
|
const pagination = parsePagination(request.query);
|
|
2437
2620
|
const focusMessageId = request.query.messageId === undefined
|
|
@@ -2580,6 +2763,10 @@ export function createRuntime(options) {
|
|
|
2580
2763
|
const pagination = parsePagination(request.query);
|
|
2581
2764
|
data(response, pagination ? ai.listModelsPage(request.params.providerId, pagination) : ai.listModels(request.params.providerId));
|
|
2582
2765
|
});
|
|
2766
|
+
app.post("/api/providers/:providerId/models/import", async (request, response) => {
|
|
2767
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
2768
|
+
data(response, await ai.importProviderModels(request.params.providerId));
|
|
2769
|
+
});
|
|
2583
2770
|
app.post("/api/providers/:providerId/models", (request, response) => data(response, ai.createModel(request.params.providerId, parse(modelSchema, request.body)), 201));
|
|
2584
2771
|
app.post("/api/models/:modelId/test", async (request, response) => {
|
|
2585
2772
|
parse(z.object({}).strict(), request.body ?? {});
|
|
@@ -2627,6 +2814,8 @@ export function createRuntime(options) {
|
|
|
2627
2814
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2628
2815
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2629
2816
|
}
|
|
2817
|
+
if (input.conversationId)
|
|
2818
|
+
assertRequestAiConversationOwner(request, input.conversationId);
|
|
2630
2819
|
const modelId = resolveConversationModelId(request.params.workId, input.conversationId, input.modelId);
|
|
2631
2820
|
data(response, redactSuggestion(await ai.createSuggestion({
|
|
2632
2821
|
workId: request.params.workId,
|
|
@@ -2645,6 +2834,7 @@ export function createRuntime(options) {
|
|
|
2645
2834
|
modelId: identifier.optional(),
|
|
2646
2835
|
parameters: jsonObject.optional(),
|
|
2647
2836
|
citations: aiCitationsSchema.optional(),
|
|
2837
|
+
imageAttachmentIds: z.array(identifier).max(4).optional(),
|
|
2648
2838
|
conversationId: identifier.optional(),
|
|
2649
2839
|
currentMessageId: identifier.optional(),
|
|
2650
2840
|
ignoreContextWarning: z.boolean().optional()
|
|
@@ -2670,6 +2860,7 @@ export function createRuntime(options) {
|
|
|
2670
2860
|
: input.conversationId
|
|
2671
2861
|
? store.getAiConversationSummary(input.conversationId)
|
|
2672
2862
|
: store.createAiConversation(request.params.workId);
|
|
2863
|
+
assertRequestAiConversationOwner(request, String(conversation.id));
|
|
2673
2864
|
if (String(conversation.workId) !== request.params.workId) {
|
|
2674
2865
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
2675
2866
|
}
|
|
@@ -2686,6 +2877,7 @@ export function createRuntime(options) {
|
|
|
2686
2877
|
let lastStreamLeaseTouchAt = Date.now();
|
|
2687
2878
|
let preparedContext = null;
|
|
2688
2879
|
let preparedConversation = null;
|
|
2880
|
+
let preparedChatImageAttachments = [];
|
|
2689
2881
|
const startStream = () => {
|
|
2690
2882
|
if (response.headersSent)
|
|
2691
2883
|
return;
|
|
@@ -2706,6 +2898,7 @@ export function createRuntime(options) {
|
|
|
2706
2898
|
};
|
|
2707
2899
|
try {
|
|
2708
2900
|
if (!existingRequest) {
|
|
2901
|
+
preparedChatImageAttachments = await ai.prepareChatImageAttachments(request.params.workId, modelId, input.imageAttachmentIds ?? [], permissions);
|
|
2709
2902
|
store.assertAiConversationStreamAvailable(conversationId);
|
|
2710
2903
|
preparedContext = await ai.prepareConversationContext({
|
|
2711
2904
|
conversationId,
|
|
@@ -2757,11 +2950,12 @@ export function createRuntime(options) {
|
|
|
2757
2950
|
content: input.instruction,
|
|
2758
2951
|
citations,
|
|
2759
2952
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
2760
|
-
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length) ? { metadata: {
|
|
2953
|
+
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length) ? { metadata: {
|
|
2761
2954
|
...(modelId ? { modelId } : {}),
|
|
2762
2955
|
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
2763
2956
|
...(mentionRaceIds.length ? { mentionRaceIds } : {}),
|
|
2764
|
-
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {})
|
|
2957
|
+
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {}),
|
|
2958
|
+
...(input.imageAttachmentIds?.length ? { chatImageAttachmentIds: [...new Set(input.imageAttachmentIds)] } : {})
|
|
2765
2959
|
} } : {})
|
|
2766
2960
|
}
|
|
2767
2961
|
});
|
|
@@ -2821,7 +3015,8 @@ export function createRuntime(options) {
|
|
|
2821
3015
|
excludeConversationMessageId: currentMessageId,
|
|
2822
3016
|
...(currentMessageId ? { assistantMessageRequestId: `assistant:${currentMessageId}` } : {}),
|
|
2823
3017
|
...(modelId ? { modelId } : {}),
|
|
2824
|
-
...(input.parameters ? { parameters: input.parameters } : {})
|
|
3018
|
+
...(input.parameters ? { parameters: input.parameters } : {}),
|
|
3019
|
+
...(preparedChatImageAttachments.length ? { imageAttachments: preparedChatImageAttachments } : {})
|
|
2825
3020
|
}, (delta) => sendEvent("delta", { delta }));
|
|
2826
3021
|
const assistantMessageId = typeof suggestion.conversationMessage === "object" && suggestion.conversationMessage !== null
|
|
2827
3022
|
? String(suggestion.conversationMessage.id ?? "")
|
|
@@ -2929,7 +3124,8 @@ export function createRuntime(options) {
|
|
|
2929
3124
|
data(response, await ai.searchWork(request.params.workId, query.q, {
|
|
2930
3125
|
type: query.type,
|
|
2931
3126
|
limit: query.limit,
|
|
2932
|
-
allowedTypes: readableHybridSearchTypes(permissions)
|
|
3127
|
+
allowedTypes: readableHybridSearchTypes(permissions),
|
|
3128
|
+
conversationOwnerUserId: request.authUser?.userId
|
|
2933
3129
|
}));
|
|
2934
3130
|
});
|
|
2935
3131
|
app.head("/api/works/:workId/export", (request, response) => {
|
|
@@ -3056,7 +3252,7 @@ export function createRuntime(options) {
|
|
|
3056
3252
|
if (error instanceof multer.MulterError) {
|
|
3057
3253
|
logger.warn("http.request.upload_rejected", { ...commonFields, uploadCode: error.code });
|
|
3058
3254
|
if (error.code === "LIMIT_FILE_SIZE") {
|
|
3059
|
-
const sizeError = uploadSizeError(request.path, uploadLimits);
|
|
3255
|
+
const sizeError = uploadSizeError(request.path, uploadLimits, String(request.query.module ?? ""));
|
|
3060
3256
|
if (sizeError) {
|
|
3061
3257
|
response.status(413).json({ error: sizeError });
|
|
3062
3258
|
return;
|
|
@@ -3112,6 +3308,7 @@ export function createRuntime(options) {
|
|
|
3112
3308
|
stopping = true;
|
|
3113
3309
|
logger.info("runtime.closing");
|
|
3114
3310
|
backups.dispose();
|
|
3311
|
+
liteLlmPriceCache.dispose();
|
|
3115
3312
|
ai.dispose();
|
|
3116
3313
|
const cancelledStreamRequests = store.cancelActiveAiConversationStreamRequests();
|
|
3117
3314
|
if (cancelledStreamRequests > 0)
|
|
@@ -3122,8 +3319,8 @@ export function createRuntime(options) {
|
|
|
3122
3319
|
await backups.waitForIdle(RUNTIME_BACKUP_IDLE_TIMEOUT_MS);
|
|
3123
3320
|
collaborationPresence.close();
|
|
3124
3321
|
database.close();
|
|
3125
|
-
if (
|
|
3126
|
-
rmSync(
|
|
3322
|
+
if (temporaryStorageRoot)
|
|
3323
|
+
rmSync(temporaryStorageRoot, { recursive: true, force: true });
|
|
3127
3324
|
closed = true;
|
|
3128
3325
|
logger.info("runtime.closed");
|
|
3129
3326
|
}
|
|
@@ -3138,6 +3335,6 @@ export function createRuntime(options) {
|
|
|
3138
3335
|
})();
|
|
3139
3336
|
return closePromise;
|
|
3140
3337
|
};
|
|
3141
|
-
return { app, database, store, ai, backups, auth, attachmentStorage, cleanupAttachments, close };
|
|
3338
|
+
return { app, database, store, ai, liteLlmPriceCache, backups, auth, attachmentStorage, characterAvatarStorage, cleanupAttachments, close };
|
|
3142
3339
|
}
|
|
3143
3340
|
//# sourceMappingURL=app.js.map
|