@musnows/scriverse 0.6.3 → 0.6.5
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 +758 -164
- package/dist/ai.js.map +1 -1
- package/dist/app.js +95 -14
- package/dist/app.js.map +1 -1
- package/dist/database.js +146 -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 +15 -1
- package/dist/public/app.js +339 -45
- package/dist/public/display-labels.js +2 -1
- package/dist/public/index.html +27 -3
- package/dist/public/styles.css +59 -2
- package/dist/store.js +189 -7
- 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),
|
|
@@ -390,17 +431,22 @@ const workAiSettingsSchema = z.object({
|
|
|
390
431
|
agentToolCallLimit: z.number().int().min(5).max(48).optional(),
|
|
391
432
|
agentToolCallGlobalMultiplier: z.number().int().min(1).max(6).optional(),
|
|
392
433
|
agentTools: z.array(z.enum(["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"])).max(6).optional(),
|
|
434
|
+
alwaysIncludeSettingInfo: z.boolean().optional(),
|
|
393
435
|
titleGenerationModelId: z.string().trim().max(200).optional()
|
|
394
436
|
}).strict();
|
|
395
437
|
const contextSchema = z.object({
|
|
396
|
-
type: z.enum(["none", "selection", "chapter", "volume", "book", "entities"]),
|
|
438
|
+
type: z.enum(["none", "selection", "chapter", "volume", "book", "settings-catalog", "entities"]),
|
|
397
439
|
chapterId: identifier.optional(),
|
|
398
440
|
volumeId: identifier.optional(),
|
|
399
441
|
selection: z.string().max(200_000).optional(),
|
|
400
442
|
chapterIds: z.array(identifier).max(20).optional(),
|
|
401
443
|
characterIds: optionalStrings,
|
|
444
|
+
mentionCharacterIds: optionalStrings,
|
|
402
445
|
settingIds: optionalStrings,
|
|
403
|
-
|
|
446
|
+
raceIds: optionalStrings,
|
|
447
|
+
organizationIds: optionalStrings,
|
|
448
|
+
includeBookSummary: z.boolean().optional(),
|
|
449
|
+
includeSettingInfo: z.boolean().optional()
|
|
404
450
|
});
|
|
405
451
|
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
452
|
const relationshipSourceRefSchema = z.object({
|
|
@@ -622,6 +668,18 @@ function redactAiCallContext(record, permissions) {
|
|
|
622
668
|
delete redactedScope.characterIds;
|
|
623
669
|
restricted = true;
|
|
624
670
|
}
|
|
671
|
+
if (permissions.characters === "none" && "mentionCharacterIds" in redactedScope) {
|
|
672
|
+
delete redactedScope.mentionCharacterIds;
|
|
673
|
+
restricted = true;
|
|
674
|
+
}
|
|
675
|
+
if (permissions.races === "none" && "raceIds" in redactedScope) {
|
|
676
|
+
delete redactedScope.raceIds;
|
|
677
|
+
restricted = true;
|
|
678
|
+
}
|
|
679
|
+
if (permissions.organizations === "none" && "organizationIds" in redactedScope) {
|
|
680
|
+
delete redactedScope.organizationIds;
|
|
681
|
+
restricted = true;
|
|
682
|
+
}
|
|
625
683
|
if (permissions.settings === "none" && "settingIds" in redactedScope) {
|
|
626
684
|
delete redactedScope.settingIds;
|
|
627
685
|
restricted = true;
|
|
@@ -670,10 +728,12 @@ function redactAiConversationMessage(item, permissions) {
|
|
|
670
728
|
}
|
|
671
729
|
/** 无正文读取权限时隐藏对话预览与消息正文,避免历史对话泄露章节原文。 */
|
|
672
730
|
function redactAiConversation(record, permissions) {
|
|
731
|
+
const readableRecord = permissions.characters === "none" ? { ...record, roleplayCharacter: null } : record;
|
|
732
|
+
const scopedRecord = redactAiCallContext(readableRecord, permissions);
|
|
673
733
|
if (permissions.prose !== "none")
|
|
674
|
-
return
|
|
734
|
+
return scopedRecord;
|
|
675
735
|
const result = {
|
|
676
|
-
...
|
|
736
|
+
...scopedRecord,
|
|
677
737
|
title: proseRestrictedPlaceholder
|
|
678
738
|
};
|
|
679
739
|
if (typeof result.preview === "string" && result.preview.length > 0) {
|
|
@@ -691,7 +751,7 @@ function redactAiConversation(record, permissions) {
|
|
|
691
751
|
}
|
|
692
752
|
return { ...result, restricted: true };
|
|
693
753
|
}
|
|
694
|
-
/** SSE 错误事件只暴露 AppError
|
|
754
|
+
/** SSE 错误事件只暴露 AppError 的公开信息;AI_CALL_FAILED 的 failure 已在 AI 层完成密钥脱敏。 */
|
|
695
755
|
export function publicAiStreamError(error) {
|
|
696
756
|
if (error instanceof AppError) {
|
|
697
757
|
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
@@ -701,7 +761,7 @@ export function publicAiStreamError(error) {
|
|
|
701
761
|
code: error.code,
|
|
702
762
|
message: error.message,
|
|
703
763
|
status: error.status,
|
|
704
|
-
...(error.status < 500 && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
764
|
+
...((error.status < 500 || error.code === "AI_CALL_FAILED") && typeof details?.failure === "string" ? { failure: details.failure } : {}),
|
|
705
765
|
...(typeof details?.callId === "string" ? { callId: details.callId } : {}),
|
|
706
766
|
...(typeof details?.providerName === "string" ? { providerName: details.providerName } : {}),
|
|
707
767
|
...(typeof details?.providerId === "string" ? { providerId: details.providerId } : {}),
|
|
@@ -843,7 +903,7 @@ export function createRuntime(options) {
|
|
|
843
903
|
bootId,
|
|
844
904
|
version: APP_VERSION,
|
|
845
905
|
protocol: "openai-chat-completions",
|
|
846
|
-
protocols: [
|
|
906
|
+
protocols: [...AI_PROVIDER_PROTOCOLS],
|
|
847
907
|
development: options.developmentServer === true
|
|
848
908
|
});
|
|
849
909
|
});
|
|
@@ -1880,8 +1940,11 @@ export function createRuntime(options) {
|
|
|
1880
1940
|
data(response, mapRecords(store.listAiConversationsPage(request.params.workId, pagination), (conversation) => (redactAiConversation(conversation, permissions))));
|
|
1881
1941
|
});
|
|
1882
1942
|
app.post("/api/works/:workId/ai-conversations", (request, response) => {
|
|
1883
|
-
const input = parse(z.object({
|
|
1884
|
-
|
|
1943
|
+
const input = parse(z.object({
|
|
1944
|
+
title: z.string().max(200).optional(),
|
|
1945
|
+
taskType: aiConversationTaskTypeSchema.optional()
|
|
1946
|
+
}).strict(), request.body ?? {});
|
|
1947
|
+
data(response, store.createAiConversation(request.params.workId, input.title, input.taskType), 201);
|
|
1885
1948
|
});
|
|
1886
1949
|
app.get("/api/ai-conversations/:conversationId", (request, response) => {
|
|
1887
1950
|
const pagination = parsePagination(request.query);
|
|
@@ -1897,6 +1960,24 @@ export function createRuntime(options) {
|
|
|
1897
1960
|
const permissions = requestPermissions(request, String(forked.workId));
|
|
1898
1961
|
data(response, redactAiConversation(forked, permissions), 201);
|
|
1899
1962
|
});
|
|
1963
|
+
app.patch("/api/ai-conversations/:conversationId/task-type", (request, response) => {
|
|
1964
|
+
const input = parse(z.object({ taskType: aiConversationTaskTypeSchema }).strict(), request.body);
|
|
1965
|
+
const updated = store.setAiConversationTaskType(request.params.conversationId, input.taskType);
|
|
1966
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1967
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1968
|
+
});
|
|
1969
|
+
app.patch("/api/ai-conversations/:conversationId/context-scope", (request, response) => {
|
|
1970
|
+
const input = parse(z.object({ scope: contextSchema }).strict(), request.body);
|
|
1971
|
+
const updated = store.setAiConversationContextScope(request.params.conversationId, input.scope);
|
|
1972
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1973
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1974
|
+
});
|
|
1975
|
+
app.patch("/api/ai-conversations/:conversationId/roleplay", (request, response) => {
|
|
1976
|
+
const input = parse(z.object({ characterId: identifier.nullable() }).strict(), request.body);
|
|
1977
|
+
const updated = store.setAiConversationRoleplayCharacter(request.params.conversationId, input.characterId);
|
|
1978
|
+
const permissions = requestPermissions(request, String(updated.workId));
|
|
1979
|
+
data(response, redactAiConversation(updated, permissions));
|
|
1980
|
+
});
|
|
1900
1981
|
app.post("/api/ai-conversations/:conversationId/messages", (request, response) => {
|
|
1901
1982
|
const input = parse(z.object({
|
|
1902
1983
|
role: z.enum(["user", "assistant"]),
|
|
@@ -1963,7 +2044,7 @@ export function createRuntime(options) {
|
|
|
1963
2044
|
data(response, ai.createProvider(parse(providerSchema, request.body)), 201);
|
|
1964
2045
|
});
|
|
1965
2046
|
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(
|
|
2047
|
+
app.patch("/api/providers/:providerId", (request, response) => data(response, ai.updateProvider(request.params.providerId, parse(providerUpdateSchema, request.body))));
|
|
1967
2048
|
app.delete("/api/providers/:providerId", (request, response) => {
|
|
1968
2049
|
ai.deleteProvider(request.params.providerId);
|
|
1969
2050
|
noContent(response);
|