@musnows/scriverse 0.8.4 → 0.8.6
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 +295 -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-stream-timeout.js +9 -6
- package/dist/ai-stream-timeout.js.map +1 -1
- package/dist/ai.js +1103 -142
- package/dist/ai.js.map +1 -1
- package/dist/app.js +230 -28
- 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 +352 -3
- 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 +1125 -150
- 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 +3 -1
- package/dist/public/model-config.js +9 -2
- package/dist/public/styles.css +120 -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 +10 -2
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +562 -47
- 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
|
-
import {
|
|
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)) {
|
|
@@ -416,11 +436,19 @@ const modelSchema = z.object({
|
|
|
416
436
|
});
|
|
417
437
|
const aiPromptSchema = z.object({
|
|
418
438
|
systemPrompt: z.string().max(100_000).optional(),
|
|
419
|
-
imageToolModelId: identifier.nullable().optional()
|
|
439
|
+
imageToolModelId: identifier.nullable().optional(),
|
|
440
|
+
streamIdleTimeoutSeconds: z.number().int().min(MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS).max(MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS).optional()
|
|
420
441
|
});
|
|
421
442
|
const aiUsageQuerySchema = z.object({
|
|
422
443
|
timezoneOffset: z.coerce.number().int().min(-840).max(840).default(0)
|
|
423
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();
|
|
424
452
|
const platformPageSizesSchema = z.object({
|
|
425
453
|
drafts: z.number().int().min(10).max(100).optional(),
|
|
426
454
|
settings: z.number().int().min(10).max(100).optional(),
|
|
@@ -543,7 +571,8 @@ const aiProcessStepSchema = z.discriminatedUnion("type", [
|
|
|
543
571
|
]);
|
|
544
572
|
const workAiSettingsSchema = z.object({
|
|
545
573
|
systemPrompt: z.string().max(100_000).optional(),
|
|
546
|
-
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(),
|
|
547
576
|
autoRunEnabled: z.boolean().optional(),
|
|
548
577
|
autoRunConcurrency: z.number().int().min(1).max(8).optional(),
|
|
549
578
|
autoRunBatchLimit: z.number().int().min(1).max(200).optional(),
|
|
@@ -975,6 +1004,7 @@ function redactAiConversation(record, permissions) {
|
|
|
975
1004
|
result.agentTools = [
|
|
976
1005
|
...(permissions.characters !== "none" ? ["recall_self"] : []),
|
|
977
1006
|
...(permissions.characters !== "none" && permissions.relationships !== "none" ? ["recall_relationship"] : []),
|
|
1007
|
+
...(permissions.prose !== "none" ? ["recall_story"] : []),
|
|
978
1008
|
"calculate_time"
|
|
979
1009
|
];
|
|
980
1010
|
}
|
|
@@ -1002,10 +1032,30 @@ export function publicAiStreamError(error) {
|
|
|
1002
1032
|
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
1003
1033
|
? error.details
|
|
1004
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;
|
|
1005
1054
|
return {
|
|
1006
1055
|
code: error.code,
|
|
1007
1056
|
message: error.message,
|
|
1008
1057
|
status: error.status,
|
|
1058
|
+
...(publicQuotaDetails ? { details: publicQuotaDetails } : {}),
|
|
1009
1059
|
...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
1010
1060
|
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
1011
1061
|
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
@@ -1047,11 +1097,14 @@ export function createRuntime(options) {
|
|
|
1047
1097
|
});
|
|
1048
1098
|
const database = new Database(options.databasePath);
|
|
1049
1099
|
const bootId = randomUUID();
|
|
1050
|
-
const
|
|
1051
|
-
? mkdtempSync(join(tmpdir(), "scriverse-
|
|
1100
|
+
const temporaryStorageRoot = options.databasePath === ":memory:" && (!options.attachmentDirectory || !options.characterAvatarDirectory)
|
|
1101
|
+
? mkdtempSync(join(tmpdir(), "scriverse-storage-"))
|
|
1052
1102
|
: null;
|
|
1053
|
-
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ??
|
|
1103
|
+
const attachmentStorage = new AttachmentStorage(options.attachmentDirectory ?? temporaryStorageRoot ?? join(dirname(options.databasePath), "attachments"), uploadLimits.attachmentBytes);
|
|
1054
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 });
|
|
1055
1108
|
const auth = new UserAuthService(database);
|
|
1056
1109
|
const collaborationPresence = new CollaborationPresence(45_000, Date.now, 120_000, 50, { store: new PresenceStore(database) });
|
|
1057
1110
|
const publishCollaborativeChange = (workId, pageKey, options = {}) => {
|
|
@@ -1083,6 +1136,8 @@ export function createRuntime(options) {
|
|
|
1083
1136
|
? auth.listUsers().find((user) => user.status === "active") ?? null
|
|
1084
1137
|
: null;
|
|
1085
1138
|
const store = new Store(database);
|
|
1139
|
+
const platformAiSettings = store.getPlatformAiSettings();
|
|
1140
|
+
const platformAiStreamIdleTimeoutMs = normalizeAiStreamIdleTimeoutSeconds(Number(platformAiSettings.streamIdleTimeoutSeconds)) * 1_000;
|
|
1086
1141
|
let attachmentCleanupChain = Promise.resolve();
|
|
1087
1142
|
const cleanupAttachments = () => {
|
|
1088
1143
|
const cleanup = attachmentCleanupChain.then(async () => {
|
|
@@ -1109,6 +1164,13 @@ export function createRuntime(options) {
|
|
|
1109
1164
|
attachmentCleanupChain = cleanup.catch(() => undefined);
|
|
1110
1165
|
return cleanup;
|
|
1111
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
|
+
};
|
|
1112
1174
|
const requestPermissions = (request, workId) => {
|
|
1113
1175
|
if (!request.authUser)
|
|
1114
1176
|
return fullWorkModulePermissions();
|
|
@@ -1117,19 +1179,28 @@ export function createRuntime(options) {
|
|
|
1117
1179
|
return fullWorkModulePermissions();
|
|
1118
1180
|
return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
|
|
1119
1181
|
};
|
|
1182
|
+
const assertRequestAiConversationOwner = (request, conversationId) => {
|
|
1183
|
+
if (request.authUser)
|
|
1184
|
+
store.assertAiConversationOwner(conversationId, request.authUser.userId);
|
|
1185
|
+
};
|
|
1120
1186
|
const resolveConversationModelId = (workId, conversationId, requestedModelId) => {
|
|
1121
1187
|
if (!conversationId)
|
|
1122
1188
|
return requestedModelId;
|
|
1123
1189
|
const lockedModelId = store.getAiConversationLockedModelId(conversationId, workId);
|
|
1190
|
+
const hasImageAttachments = store.getAiConversationHasImageAttachments(conversationId, workId);
|
|
1124
1191
|
if (lockedModelId && requestedModelId && lockedModelId !== requestedModelId) {
|
|
1125
1192
|
throw new AppError(409, "AI_CONVERSATION_MODEL_LOCKED", "当前对话已经锁定模型,请新建对话后再切换模型");
|
|
1126
1193
|
}
|
|
1194
|
+
if (hasImageAttachments && !lockedModelId && requestedModelId) {
|
|
1195
|
+
throw new AppError(409, "AI_CONVERSATION_IMAGE_MODEL_LOCKED", "当前对话链路包含图片但没有可继承的模型,请新建对话后再选择模型");
|
|
1196
|
+
}
|
|
1127
1197
|
return lockedModelId ?? requestedModelId;
|
|
1128
1198
|
};
|
|
1129
1199
|
const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
|
|
1130
1200
|
const credentialVault = new CredentialVault(options.masterSecret);
|
|
1131
1201
|
const backups = new S3BackupManager(database, credentialVault, store, attachmentStorage, {
|
|
1132
1202
|
...options.backupOptions,
|
|
1203
|
+
characterAvatarStorage,
|
|
1133
1204
|
masterKey: options.masterSecret,
|
|
1134
1205
|
validateEndpoint: options.backupOptions?.validateEndpoint
|
|
1135
1206
|
?? (options.security ? (url) => assertSafeS3Endpoint(url, options.security?.allowPrivateAiEndpoints) : undefined)
|
|
@@ -1139,6 +1210,11 @@ export function createRuntime(options) {
|
|
|
1139
1210
|
timeoutMs: options.releaseCheckTimeoutMs,
|
|
1140
1211
|
retries: options.releaseCheckRetries
|
|
1141
1212
|
});
|
|
1213
|
+
const liteLlmPriceCache = options.liteLlmPriceCache ?? new LiteLlmPriceCache({
|
|
1214
|
+
...(options.liteLlmPriceCachePath ? { cachePath: options.liteLlmPriceCachePath } : {})
|
|
1215
|
+
});
|
|
1216
|
+
if (!options.liteLlmPriceCache && options.liteLlmPriceCachePath)
|
|
1217
|
+
liteLlmPriceCache.start();
|
|
1142
1218
|
const ai = new AiManager(store, credentialVault, options.fetchImpl ?? fetch, options.developmentServer === true
|
|
1143
1219
|
? undefined
|
|
1144
1220
|
: options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined, (task, actor) => {
|
|
@@ -1155,9 +1231,11 @@ export function createRuntime(options) {
|
|
|
1155
1231
|
write: ["ai-analysis"]
|
|
1156
1232
|
}, false, actor?.allowAdminAccess ?? false);
|
|
1157
1233
|
}, attachmentStorage, {
|
|
1158
|
-
interactiveStreamIdleTimeoutMs: options.aiStreamIdleTimeoutMs ??
|
|
1234
|
+
interactiveStreamIdleTimeoutMs: options.aiStreamIdleTimeoutMs ?? platformAiStreamIdleTimeoutMs,
|
|
1159
1235
|
retryPolicy: options.aiRetryPolicy,
|
|
1160
|
-
retrySleep: options.aiRetrySleep
|
|
1236
|
+
retrySleep: options.aiRetrySleep,
|
|
1237
|
+
aiChatImageMaxBytes: uploadLimits.chatImageBytes,
|
|
1238
|
+
liteLlmPriceCache
|
|
1161
1239
|
});
|
|
1162
1240
|
const app = express();
|
|
1163
1241
|
enforceCaseInsensitiveRouting(app);
|
|
@@ -1173,12 +1251,19 @@ export function createRuntime(options) {
|
|
|
1173
1251
|
storage: multer.memoryStorage(),
|
|
1174
1252
|
limits: { fileSize: uploadLimits.avatarBytes + 1, files: 1, fields: 1, fieldSize: 1024, parts: 2, headerPairs: 50 }
|
|
1175
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
|
+
});
|
|
1176
1261
|
const attachmentUpload = multer({
|
|
1177
1262
|
storage: multer.diskStorage({
|
|
1178
1263
|
destination: attachmentStorage.temporaryDirectory,
|
|
1179
1264
|
filename: (_request, _file, callback) => callback(null, randomUUID())
|
|
1180
1265
|
}),
|
|
1181
|
-
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 }
|
|
1182
1267
|
});
|
|
1183
1268
|
app.disable("x-powered-by");
|
|
1184
1269
|
const trustProxy = resolveTrustProxySetting(options.security?.trustProxy);
|
|
@@ -1392,7 +1477,9 @@ export function createRuntime(options) {
|
|
|
1392
1477
|
if (request.authUser)
|
|
1393
1478
|
auth.assertDeletedWorkAccess(request.authUser, request.params.workId, request.authMethod !== "api-key");
|
|
1394
1479
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1480
|
+
const characterAvatarStorageKeys = store.listCharacterAvatarStorageKeysForWork(request.params.workId);
|
|
1395
1481
|
store.permanentlyDeleteWork(request.params.workId, input.expectedVersionNo);
|
|
1482
|
+
await cleanupCharacterAvatarFiles(characterAvatarStorageKeys);
|
|
1396
1483
|
await cleanupAttachments();
|
|
1397
1484
|
noContent(response);
|
|
1398
1485
|
});
|
|
@@ -1544,11 +1631,11 @@ export function createRuntime(options) {
|
|
|
1544
1631
|
data(response, store.replaceWorkText(request.params.workId, parse(globalReplaceSchema, request.body)));
|
|
1545
1632
|
});
|
|
1546
1633
|
app.post("/api/works/:workId/volumes", (request, response) => {
|
|
1547
|
-
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);
|
|
1548
1635
|
data(response, store.createVolume(request.params.workId, input), 201);
|
|
1549
1636
|
});
|
|
1550
1637
|
app.patch("/api/volumes/:volumeId", (request, response) => {
|
|
1551
|
-
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);
|
|
1552
1639
|
const { expectedVersionNo, changeNote, ...volumeInput } = input;
|
|
1553
1640
|
data(response, store.updateVolume(request.params.volumeId, volumeInput, expectedVersionNo, "manual", null, changeNote));
|
|
1554
1641
|
});
|
|
@@ -1835,6 +1922,60 @@ export function createRuntime(options) {
|
|
|
1835
1922
|
app.get("/api/characters/:characterId", (request, response) => {
|
|
1836
1923
|
data(response, redactCharacterLinks(store.getCharacter(request.params.characterId), requestPermissions(request)));
|
|
1837
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
|
+
});
|
|
1838
1979
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
1839
1980
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
1840
1981
|
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
@@ -1852,10 +1993,14 @@ export function createRuntime(options) {
|
|
|
1852
1993
|
const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
|
|
1853
1994
|
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
1854
1995
|
});
|
|
1855
|
-
app.delete("/api/characters/:characterId", (request, response) => {
|
|
1996
|
+
app.delete("/api/characters/:characterId", async (request, response) => {
|
|
1856
1997
|
const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
|
|
1857
1998
|
const character = store.getCharacter(request.params.characterId);
|
|
1999
|
+
const avatar = store.getCharacterAvatar(request.params.characterId);
|
|
1858
2000
|
store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
|
|
2001
|
+
if (avatar && !store.characterAvatarStorageKeyInUse(avatar.storageKey)) {
|
|
2002
|
+
await characterAvatarStorage.remove(avatar.storageKey);
|
|
2003
|
+
}
|
|
1859
2004
|
publishEntityChange(String(character.workId), "character", String(character.id), deletedPageChange);
|
|
1860
2005
|
noContent(response);
|
|
1861
2006
|
});
|
|
@@ -1925,9 +2070,18 @@ export function createRuntime(options) {
|
|
|
1925
2070
|
if (!request.file)
|
|
1926
2071
|
throw new AppError(400, "FILE_REQUIRED", "请选择要上传的图片附件");
|
|
1927
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;
|
|
1928
2077
|
let storageKey = null;
|
|
1929
2078
|
try {
|
|
1930
|
-
|
|
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);
|
|
1931
2085
|
storageKey = stored.storageKey;
|
|
1932
2086
|
const result = store.createAttachment(String(request.params.workId), {
|
|
1933
2087
|
originalName: normalizeUploadFileName(request.file.originalname),
|
|
@@ -1954,8 +2108,10 @@ export function createRuntime(options) {
|
|
|
1954
2108
|
throw new AppError(403, "WORK_MODULE_READ_DENIED", "你没有读取该附件所属资料模块的权限");
|
|
1955
2109
|
}
|
|
1956
2110
|
const content = await attachmentStorage.read(String(attachment.storageKey));
|
|
2111
|
+
const fileName = attachmentDownloadFileName(store.getAttachmentDownloadContextName(String(attachment.id)), String(attachment.originalName));
|
|
1957
2112
|
response.setHeader("Content-Type", String(attachment.storedMimeType));
|
|
1958
2113
|
response.setHeader("Content-Length", String(attachment.storedByteLength));
|
|
2114
|
+
response.setHeader("Content-Disposition", inlineContentDisposition(fileName));
|
|
1959
2115
|
response.setHeader("ETag", `"${String(attachment.storedSha256)}"`);
|
|
1960
2116
|
response.setHeader("Cache-Control", "private, max-age=31536000, immutable");
|
|
1961
2117
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -2311,6 +2467,7 @@ export function createRuntime(options) {
|
|
|
2311
2467
|
const pagination = parsePagination(request.query);
|
|
2312
2468
|
data(response, pagination ? ai.listProvidersPage(pagination) : ai.listProviders());
|
|
2313
2469
|
});
|
|
2470
|
+
app.get("/api/platform/ai/protocols", (_request, response) => data(response, AI_PROVIDER_PROTOCOL_OPTIONS));
|
|
2314
2471
|
app.post("/api/platform/ai/providers", (request, response) => data(response, ai.createProvider(parse(providerSchema, request.body)), 201));
|
|
2315
2472
|
app.get("/api/platform/ai/models", (request, response) => {
|
|
2316
2473
|
const pagination = parsePagination(request.query);
|
|
@@ -2321,12 +2478,39 @@ export function createRuntime(options) {
|
|
|
2321
2478
|
const input = parse(aiPromptSchema, request.body);
|
|
2322
2479
|
if (input.imageToolModelId)
|
|
2323
2480
|
ai.assertImageToolModelAvailable(input.imageToolModelId);
|
|
2324
|
-
|
|
2481
|
+
const settings = store.updatePlatformAiSettings(input);
|
|
2482
|
+
ai.setInteractiveStreamIdleTimeoutSeconds(Number(settings.streamIdleTimeoutSeconds));
|
|
2483
|
+
data(response, settings);
|
|
2325
2484
|
});
|
|
2326
2485
|
app.get("/api/platform/ai/usage", (request, response) => {
|
|
2327
2486
|
const query = parse(aiUsageQuerySchema, request.query);
|
|
2328
2487
|
data(response, ai.getPlatformTokenUsage(query.timezoneOffset));
|
|
2329
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, "LITELLM_PRICE_REFRESH_FAILED", "LiteLLM 模型价格刷新失败,历史缓存未改变");
|
|
2498
|
+
}
|
|
2499
|
+
data(response, {
|
|
2500
|
+
refreshed: true,
|
|
2501
|
+
pricingAvailable: liteLlmPriceCache.hasData(),
|
|
2502
|
+
modelCount: liteLlmPriceCache.getPriceTable().size
|
|
2503
|
+
});
|
|
2504
|
+
});
|
|
2505
|
+
app.get("/api/platform/ai-conversations", (request, response) => {
|
|
2506
|
+
const query = parse(adminAiConversationQuerySchema, request.query);
|
|
2507
|
+
const pagination = parsePagination(request.query) ?? { page: 1, limit: 30, offset: 0 };
|
|
2508
|
+
data(response, store.listAdminAiConversationsPage(pagination, {
|
|
2509
|
+
query: query.q,
|
|
2510
|
+
workId: query.workId,
|
|
2511
|
+
userId: query.userId
|
|
2512
|
+
}));
|
|
2513
|
+
});
|
|
2330
2514
|
app.get("/api/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
|
|
2331
2515
|
app.get("/api/platform/ui-settings", (_request, response) => data(response, store.getPlatformUiSettings()));
|
|
2332
2516
|
app.patch("/api/platform/ui-settings", (request, response) => {
|
|
@@ -2417,7 +2601,7 @@ export function createRuntime(options) {
|
|
|
2417
2601
|
limit: request.query.limit ?? "20"
|
|
2418
2602
|
}) ?? { page: 1, limit: 20, offset: 0 };
|
|
2419
2603
|
const permissions = requestPermissions(request, request.params.workId);
|
|
2420
|
-
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
2604
|
+
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination, request.authUser?.userId), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
2421
2605
|
});
|
|
2422
2606
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
2423
2607
|
const input = parse(z.object({
|
|
@@ -2426,6 +2610,10 @@ export function createRuntime(options) {
|
|
|
2426
2610
|
}).strict(), request.body ?? {});
|
|
2427
2611
|
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
2428
2612
|
});
|
|
2613
|
+
app.use("/api/ai-conversations/:conversationId", (request, _response, next) => {
|
|
2614
|
+
assertRequestAiConversationOwner(request, request.params.conversationId);
|
|
2615
|
+
next();
|
|
2616
|
+
});
|
|
2429
2617
|
app.get("/api/ai-conversations/:conversationId", (request, response) => {
|
|
2430
2618
|
const pagination = parsePagination(request.query);
|
|
2431
2619
|
const focusMessageId = request.query.messageId === undefined
|
|
@@ -2574,6 +2762,10 @@ export function createRuntime(options) {
|
|
|
2574
2762
|
const pagination = parsePagination(request.query);
|
|
2575
2763
|
data(response, pagination ? ai.listModelsPage(request.params.providerId, pagination) : ai.listModels(request.params.providerId));
|
|
2576
2764
|
});
|
|
2765
|
+
app.post("/api/providers/:providerId/models/import", async (request, response) => {
|
|
2766
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
2767
|
+
data(response, await ai.importProviderModels(request.params.providerId));
|
|
2768
|
+
});
|
|
2577
2769
|
app.post("/api/providers/:providerId/models", (request, response) => data(response, ai.createModel(request.params.providerId, parse(modelSchema, request.body)), 201));
|
|
2578
2770
|
app.post("/api/models/:modelId/test", async (request, response) => {
|
|
2579
2771
|
parse(z.object({}).strict(), request.body ?? {});
|
|
@@ -2621,6 +2813,8 @@ export function createRuntime(options) {
|
|
|
2621
2813
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2622
2814
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2623
2815
|
}
|
|
2816
|
+
if (input.conversationId)
|
|
2817
|
+
assertRequestAiConversationOwner(request, input.conversationId);
|
|
2624
2818
|
const modelId = resolveConversationModelId(request.params.workId, input.conversationId, input.modelId);
|
|
2625
2819
|
data(response, redactSuggestion(await ai.createSuggestion({
|
|
2626
2820
|
workId: request.params.workId,
|
|
@@ -2639,6 +2833,7 @@ export function createRuntime(options) {
|
|
|
2639
2833
|
modelId: identifier.optional(),
|
|
2640
2834
|
parameters: jsonObject.optional(),
|
|
2641
2835
|
citations: aiCitationsSchema.optional(),
|
|
2836
|
+
imageAttachmentIds: z.array(identifier).max(4).optional(),
|
|
2642
2837
|
conversationId: identifier.optional(),
|
|
2643
2838
|
currentMessageId: identifier.optional(),
|
|
2644
2839
|
ignoreContextWarning: z.boolean().optional()
|
|
@@ -2664,6 +2859,7 @@ export function createRuntime(options) {
|
|
|
2664
2859
|
: input.conversationId
|
|
2665
2860
|
? store.getAiConversationSummary(input.conversationId)
|
|
2666
2861
|
: store.createAiConversation(request.params.workId);
|
|
2862
|
+
assertRequestAiConversationOwner(request, String(conversation.id));
|
|
2667
2863
|
if (String(conversation.workId) !== request.params.workId) {
|
|
2668
2864
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
2669
2865
|
}
|
|
@@ -2680,6 +2876,7 @@ export function createRuntime(options) {
|
|
|
2680
2876
|
let lastStreamLeaseTouchAt = Date.now();
|
|
2681
2877
|
let preparedContext = null;
|
|
2682
2878
|
let preparedConversation = null;
|
|
2879
|
+
let preparedChatImageAttachments = [];
|
|
2683
2880
|
const startStream = () => {
|
|
2684
2881
|
if (response.headersSent)
|
|
2685
2882
|
return;
|
|
@@ -2700,6 +2897,7 @@ export function createRuntime(options) {
|
|
|
2700
2897
|
};
|
|
2701
2898
|
try {
|
|
2702
2899
|
if (!existingRequest) {
|
|
2900
|
+
preparedChatImageAttachments = await ai.prepareChatImageAttachments(request.params.workId, modelId, input.imageAttachmentIds ?? [], permissions);
|
|
2703
2901
|
store.assertAiConversationStreamAvailable(conversationId);
|
|
2704
2902
|
preparedContext = await ai.prepareConversationContext({
|
|
2705
2903
|
conversationId,
|
|
@@ -2751,11 +2949,12 @@ export function createRuntime(options) {
|
|
|
2751
2949
|
content: input.instruction,
|
|
2752
2950
|
citations,
|
|
2753
2951
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
2754
|
-
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length) ? { metadata: {
|
|
2952
|
+
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length) ? { metadata: {
|
|
2755
2953
|
...(modelId ? { modelId } : {}),
|
|
2756
2954
|
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
2757
2955
|
...(mentionRaceIds.length ? { mentionRaceIds } : {}),
|
|
2758
|
-
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {})
|
|
2956
|
+
...(mentionOrganizationIds.length ? { mentionOrganizationIds } : {}),
|
|
2957
|
+
...(input.imageAttachmentIds?.length ? { chatImageAttachmentIds: [...new Set(input.imageAttachmentIds)] } : {})
|
|
2759
2958
|
} } : {})
|
|
2760
2959
|
}
|
|
2761
2960
|
});
|
|
@@ -2815,7 +3014,8 @@ export function createRuntime(options) {
|
|
|
2815
3014
|
excludeConversationMessageId: currentMessageId,
|
|
2816
3015
|
...(currentMessageId ? { assistantMessageRequestId: `assistant:${currentMessageId}` } : {}),
|
|
2817
3016
|
...(modelId ? { modelId } : {}),
|
|
2818
|
-
...(input.parameters ? { parameters: input.parameters } : {})
|
|
3017
|
+
...(input.parameters ? { parameters: input.parameters } : {}),
|
|
3018
|
+
...(preparedChatImageAttachments.length ? { imageAttachments: preparedChatImageAttachments } : {})
|
|
2819
3019
|
}, (delta) => sendEvent("delta", { delta }));
|
|
2820
3020
|
const assistantMessageId = typeof suggestion.conversationMessage === "object" && suggestion.conversationMessage !== null
|
|
2821
3021
|
? String(suggestion.conversationMessage.id ?? "")
|
|
@@ -2923,7 +3123,8 @@ export function createRuntime(options) {
|
|
|
2923
3123
|
data(response, await ai.searchWork(request.params.workId, query.q, {
|
|
2924
3124
|
type: query.type,
|
|
2925
3125
|
limit: query.limit,
|
|
2926
|
-
allowedTypes: readableHybridSearchTypes(permissions)
|
|
3126
|
+
allowedTypes: readableHybridSearchTypes(permissions),
|
|
3127
|
+
conversationOwnerUserId: request.authUser?.userId
|
|
2927
3128
|
}));
|
|
2928
3129
|
});
|
|
2929
3130
|
app.head("/api/works/:workId/export", (request, response) => {
|
|
@@ -3050,7 +3251,7 @@ export function createRuntime(options) {
|
|
|
3050
3251
|
if (error instanceof multer.MulterError) {
|
|
3051
3252
|
logger.warn("http.request.upload_rejected", { ...commonFields, uploadCode: error.code });
|
|
3052
3253
|
if (error.code === "LIMIT_FILE_SIZE") {
|
|
3053
|
-
const sizeError = uploadSizeError(request.path, uploadLimits);
|
|
3254
|
+
const sizeError = uploadSizeError(request.path, uploadLimits, String(request.query.module ?? ""));
|
|
3054
3255
|
if (sizeError) {
|
|
3055
3256
|
response.status(413).json({ error: sizeError });
|
|
3056
3257
|
return;
|
|
@@ -3106,6 +3307,7 @@ export function createRuntime(options) {
|
|
|
3106
3307
|
stopping = true;
|
|
3107
3308
|
logger.info("runtime.closing");
|
|
3108
3309
|
backups.dispose();
|
|
3310
|
+
liteLlmPriceCache.dispose();
|
|
3109
3311
|
ai.dispose();
|
|
3110
3312
|
const cancelledStreamRequests = store.cancelActiveAiConversationStreamRequests();
|
|
3111
3313
|
if (cancelledStreamRequests > 0)
|
|
@@ -3116,8 +3318,8 @@ export function createRuntime(options) {
|
|
|
3116
3318
|
await backups.waitForIdle(RUNTIME_BACKUP_IDLE_TIMEOUT_MS);
|
|
3117
3319
|
collaborationPresence.close();
|
|
3118
3320
|
database.close();
|
|
3119
|
-
if (
|
|
3120
|
-
rmSync(
|
|
3321
|
+
if (temporaryStorageRoot)
|
|
3322
|
+
rmSync(temporaryStorageRoot, { recursive: true, force: true });
|
|
3121
3323
|
closed = true;
|
|
3122
3324
|
logger.info("runtime.closed");
|
|
3123
3325
|
}
|
|
@@ -3132,6 +3334,6 @@ export function createRuntime(options) {
|
|
|
3132
3334
|
})();
|
|
3133
3335
|
return closePromise;
|
|
3134
3336
|
};
|
|
3135
|
-
return { app, database, store, ai, backups, auth, attachmentStorage, cleanupAttachments, close };
|
|
3337
|
+
return { app, database, store, ai, liteLlmPriceCache, backups, auth, attachmentStorage, characterAvatarStorage, cleanupAttachments, close };
|
|
3136
3338
|
}
|
|
3137
3339
|
//# sourceMappingURL=app.js.map
|