@musnows/scriverse 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-protocol.js +22 -9
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai.js +756 -163
- package/dist/ai.js.map +1 -1
- package/dist/app.js +94 -14
- package/dist/app.js.map +1 -1
- package/dist/database.js +129 -2
- package/dist/database.js.map +1 -1
- package/dist/google-vertex-auth.js +155 -0
- package/dist/google-vertex-auth.js.map +1 -0
- package/dist/public/ai-mentions.js +10 -0
- package/dist/public/app.js +345 -42
- package/dist/public/display-labels.js +2 -1
- package/dist/public/index.html +30 -2
- package/dist/public/styles.css +78 -0
- package/dist/store.js +182 -4
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +21 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +8 -0
- package/dist/writing-progress-time.js.map +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -9,6 +9,7 @@ import { rm } from "node:fs/promises";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { pipeline } from "node:stream/promises";
|
|
11
11
|
import { z, ZodError } from "zod";
|
|
12
|
+
import { AI_PROVIDER_PROTOCOLS } from "./ai-protocol.js";
|
|
12
13
|
import { AttachmentStorage } from "./attachment-storage.js";
|
|
13
14
|
import { AiManager } from "./ai.js";
|
|
14
15
|
import { CredentialVault } from "./credential-vault.js";
|
|
@@ -16,9 +17,10 @@ import { Database } from "./database.js";
|
|
|
16
17
|
import { assertSafeDocxArchive } from "./docx-security.js";
|
|
17
18
|
import { DRAFT_SETTING_MODULES, TASK_TYPES } from "./domain.js";
|
|
18
19
|
import { AppError } from "./errors.js";
|
|
20
|
+
import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
19
21
|
import { HYBRID_SEARCH_TYPES } from "./hybrid-search.js";
|
|
20
22
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
21
|
-
import { attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
|
|
23
|
+
import { aiConversationTaskTypes, attachmentPermissionModules, Store, versionedEntityTypes } from "./store.js";
|
|
22
24
|
import { paginated, parsePagination } from "./pagination.js";
|
|
23
25
|
import { normalizeUploadFileName } from "./utils.js";
|
|
24
26
|
import { assertSafeAiEndpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
|
|
@@ -37,6 +39,7 @@ const identifier = z.string().trim().min(1).max(200);
|
|
|
37
39
|
const optionalStrings = z.array(z.string()).optional();
|
|
38
40
|
const jsonObject = z.record(z.string(), z.unknown());
|
|
39
41
|
const chapterTypeSchema = z.enum(["正文", "设定", "作者的话", "其他"]);
|
|
42
|
+
const aiConversationTaskTypeSchema = z.enum(aiConversationTaskTypes);
|
|
40
43
|
const versionedEntityTypeSchema = z.enum(versionedEntityTypes);
|
|
41
44
|
const attachmentPermissionModuleSchema = z.enum(attachmentPermissionModules);
|
|
42
45
|
const maximumImportedTextLength = 20_000_000;
|
|
@@ -289,16 +292,54 @@ const reviewSchema = z.object({
|
|
|
289
292
|
status: z.enum(["pending", "ignored", "fixing", "fixed", "exception"]).optional(),
|
|
290
293
|
resolutionNote: z.string().max(20_000).optional()
|
|
291
294
|
});
|
|
292
|
-
const
|
|
295
|
+
const providerBaseSchema = z.object({
|
|
293
296
|
name: nonEmpty.max(200),
|
|
294
297
|
baseUrl: z.string().url().refine((value) => value.startsWith("http://") || value.startsWith("https://"), "接口地址必须使用 HTTP 或 HTTPS"),
|
|
295
|
-
apiKey:
|
|
296
|
-
protocol: z.enum(
|
|
298
|
+
apiKey: z.string().trim().min(1).max(50_000),
|
|
299
|
+
protocol: z.enum(AI_PROVIDER_PROTOCOLS).optional(),
|
|
297
300
|
status: z.enum(["enabled", "disabled"]).optional(),
|
|
298
301
|
note: z.string().max(10_000).optional(),
|
|
299
302
|
concurrencyLimit: z.number().int().min(1).max(100).optional(),
|
|
300
303
|
rpmLimit: z.number().int().min(1).max(10_000).optional()
|
|
301
304
|
});
|
|
305
|
+
function refineProviderApiKey(value, ctx) {
|
|
306
|
+
if (value.protocol === "google-vertex" && value.baseUrl && !isOfficialGoogleVertexBaseUrl(value.baseUrl)) {
|
|
307
|
+
ctx.addIssue({
|
|
308
|
+
code: z.ZodIssueCode.custom,
|
|
309
|
+
path: ["baseUrl"],
|
|
310
|
+
message: "Google Vertex 接口地址必须使用官方 aiplatform.googleapis.com 或 *-aiplatform.googleapis.com 域名"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (!value.apiKey)
|
|
314
|
+
return;
|
|
315
|
+
const protocol = value.protocol ?? "openai-chat-completions";
|
|
316
|
+
if (protocol === "google-vertex") {
|
|
317
|
+
try {
|
|
318
|
+
parseGoogleServiceAccount(value.apiKey);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
ctx.addIssue({
|
|
322
|
+
code: z.ZodIssueCode.custom,
|
|
323
|
+
path: ["apiKey"],
|
|
324
|
+
message: error instanceof AppError ? error.message : "服务账号 JSON 无效"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (value.apiKey.length > 10_000) {
|
|
330
|
+
ctx.addIssue({
|
|
331
|
+
code: z.ZodIssueCode.custom,
|
|
332
|
+
path: ["apiKey"],
|
|
333
|
+
message: "API 密钥过长"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const providerSchema = providerBaseSchema.superRefine((value, ctx) => {
|
|
338
|
+
refineProviderApiKey(value, ctx);
|
|
339
|
+
});
|
|
340
|
+
const providerUpdateSchema = providerBaseSchema.partial().superRefine((value, ctx) => {
|
|
341
|
+
refineProviderApiKey(value, ctx);
|
|
342
|
+
});
|
|
302
343
|
const modelSchema = z.object({
|
|
303
344
|
displayName: nonEmpty.max(200),
|
|
304
345
|
modelId: nonEmpty.max(300),
|
|
@@ -393,14 +434,18 @@ const workAiSettingsSchema = z.object({
|
|
|
393
434
|
titleGenerationModelId: z.string().trim().max(200).optional()
|
|
394
435
|
}).strict();
|
|
395
436
|
const contextSchema = z.object({
|
|
396
|
-
type: z.enum(["none", "selection", "chapter", "volume", "book", "entities"]),
|
|
437
|
+
type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
|
|
397
438
|
chapterId: identifier.optional(),
|
|
398
439
|
volumeId: identifier.optional(),
|
|
399
440
|
selection: z.string().max(200_000).optional(),
|
|
400
441
|
chapterIds: z.array(identifier).max(20).optional(),
|
|
401
442
|
characterIds: optionalStrings,
|
|
443
|
+
mentionCharacterIds: optionalStrings,
|
|
402
444
|
settingIds: optionalStrings,
|
|
403
|
-
|
|
445
|
+
raceIds: optionalStrings,
|
|
446
|
+
organizationIds: optionalStrings,
|
|
447
|
+
includeBookSummary: z.boolean().optional(),
|
|
448
|
+
includeSettingInfo: z.boolean().optional()
|
|
404
449
|
});
|
|
405
450
|
const analysisTaskTypeSchema = z.enum(["structure", "chapter-analysis", "character-extraction", "character-summary", "character-identity-audit", "timeline-analysis", "worldview-analysis", "setting-extraction", "consistency-check", "report-update", "book-analysis"]);
|
|
406
451
|
const relationshipSourceRefSchema = z.object({
|
|
@@ -622,6 +667,18 @@ function redactAiCallContext(record, permissions) {
|
|
|
622
667
|
delete redactedScope.characterIds;
|
|
623
668
|
restricted = true;
|
|
624
669
|
}
|
|
670
|
+
if (permissions.characters === "none" && "mentionCharacterIds" in redactedScope) {
|
|
671
|
+
delete redactedScope.mentionCharacterIds;
|
|
672
|
+
restricted = true;
|
|
673
|
+
}
|
|
674
|
+
if (permissions.races === "none" && "raceIds" in redactedScope) {
|
|
675
|
+
delete redactedScope.raceIds;
|
|
676
|
+
restricted = true;
|
|
677
|
+
}
|
|
678
|
+
if (permissions.organizations === "none" && "organizationIds" in redactedScope) {
|
|
679
|
+
delete redactedScope.organizationIds;
|
|
680
|
+
restricted = true;
|
|
681
|
+
}
|
|
625
682
|
if (permissions.settings === "none" && "settingIds" in redactedScope) {
|
|
626
683
|
delete redactedScope.settingIds;
|
|
627
684
|
restricted = true;
|
|
@@ -670,10 +727,12 @@ function redactAiConversationMessage(item, permissions) {
|
|
|
670
727
|
}
|
|
671
728
|
/** 无正文读取权限时隐藏对话预览与消息正文,避免历史对话泄露章节原文。 */
|
|
672
729
|
function redactAiConversation(record, permissions) {
|
|
730
|
+
const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
|
|
731
|
+
const scopedRecord = redactAiCallContext(readableRecord, permissions);
|
|
673
732
|
if (permissions.prose !== "none")
|
|
674
|
-
return
|
|
733
|
+
return scopedRecord;
|
|
675
734
|
const result = {
|
|
676
|
-
...
|
|
735
|
+
...scopedRecord,
|
|
677
736
|
title: proseRestrictedPlaceholder
|
|
678
737
|
};
|
|
679
738
|
if (typeof result.preview === "string" && result.preview.length > 0) {
|
|
@@ -691,7 +750,7 @@ function redactAiConversation(record, permissions) {
|
|
|
691
750
|
}
|
|
692
751
|
return { ...result, restricted: true };
|
|
693
752
|
}
|
|
694
|
-
/** SSE 错误事件只暴露 AppError
|
|
753
|
+
/** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED 的 failure 已在 AI 层完成密钥脱敏。 */
|
|
695
754
|
export function publicAiStreamError(error) {
|
|
696
755
|
if (error instanceof AppError) {
|
|
697
756
|
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
@@ -701,7 +760,7 @@ export function publicAiStreamError(error) {
|
|
|
701
760
|
code: error.code,
|
|
702
761
|
message: error.message,
|
|
703
762
|
status: error.status,
|
|
704
|
-
...(error.status < 500 && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
763
|
+
...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
705
764
|
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
706
765
|
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
707
766
|
...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
|
|
@@ -843,7 +902,7 @@ export function createRuntime(options) {
|
|
|
843
902
|
bootId,
|
|
844
903
|
version: APP_VERSION,
|
|
845
904
|
protocol: "openai-chat-completions",
|
|
846
|
-
protocols: [
|
|
905
|
+
protocols: [...AI_PROVIDER_PROTOCOLS],
|
|
847
906
|
development: options.developmentServer === true
|
|
848
907
|
});
|
|
849
908
|
});
|
|
@@ -1880,8 +1939,11 @@ export function createRuntime(options) {
|
|
|
1880
1939
|
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
1881
1940
|
});
|
|
1882
1941
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
1883
|
-
const input = parse(z.object({
|
|
1884
|
-
|
|
1942
|
+
const input = parse(z.object({
|
|
1943
|
+
title: z.string().max(200).optional(),
|
|
1944
|
+
taskType: aiConversationTaskTypeSchema.optional()
|
|
1945
|
+
}).strict(), request.body ?? {});
|
|
1946
|
+
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
1885
1947
|
});
|
|
1886
1948
|
app.get("/api/ai-conversations/:conversationId", (request, response) => {
|
|
1887
1949
|
const pagination = parsePagination(request.query);
|
|
@@ -1897,6 +1959,24 @@ export function createRuntime(options) {
|
|
|
1897
1959
|
const permissions = requestPermissions(request, String(forked.workId));
|
|
1898
1960
|
data(response, redactAiConversation(forked, permissions), 201);
|
|
1899
1961
|
});
|
|
1962
|
+
app.patch("/api/ai-conversations/:conversationId/task-type", (request, response) => {
|
|
1963
|
+
const input = parse(z.object({ taskType: aiConversationTaskTypeSchema }).strict(), request.body);
|
|
1964
|
+
const updated = store.setAiConversationTaskType(request.params.conversationId, input.taskType);
|
|
1965
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1966
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1967
|
+
});
|
|
1968
|
+
app.patch("/api/ai-conversations/:conversationId/context-scope", (request, response) => {
|
|
1969
|
+
const input = parse(z.object({ scope: contextSchema }).strict(), request.body);
|
|
1970
|
+
const updated = store.setAiConversationContextScope(request.params.conversationId, input.scope);
|
|
1971
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1972
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1973
|
+
});
|
|
1974
|
+
app.patch("/api/ai-conversations/:conversationId/roleplay", (request, response) => {
|
|
1975
|
+
const input = parse(z.object({ characterId: identifier.nullable() }).strict(), request.body);
|
|
1976
|
+
const updated = store.setAiConversationRoleplayCharacter(request.params.conversationId, input.characterId);
|
|
1977
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1978
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1979
|
+
});
|
|
1900
1980
|
app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
|
|
1901
1981
|
const input = parse(z.object({
|
|
1902
1982
|
role: z.enum(["user", "assistant"]),
|
|
@@ -1963,7 +2043,7 @@ export function createRuntime(options) {
|
|
|
1963
2043
|
data(response, ai.createProvider(parse(providerSchema, request.body)), 201);
|
|
1964
2044
|
});
|
|
1965
2045
|
app.get("/api/providers/:providerId", (request, response) => data(response, ai.getProvider(request.params.providerId)));
|
|
1966
|
-
app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(
|
|
2046
|
+
app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(providerUpdateSchema, request.body))));
|
|
1967
2047
|
app.delete("/api/providers/:providerId", (request, response) => {
|
|
1968
2048
|
ai.deleteProvider(request.params.providerId);
|
|
1969
2049
|
noContent(response);
|