@musnows/scriverse 0.8.6 → 0.8.8
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.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai-model-pricing.js +297 -62
- package/dist/ai-model-pricing.js.map +1 -1
- package/dist/ai.js +543 -133
- package/dist/ai.js.map +1 -1
- package/dist/app.js +77 -11
- package/dist/app.js.map +1 -1
- package/dist/database.js +151 -3
- package/dist/database.js.map +1 -1
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +601 -74
- package/dist/public/index.html +23 -10
- package/dist/public/model-config.d.ts +3 -3
- package/dist/public/model-config.js +2 -1
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +109 -12
- package/dist/public/toast-layer.d.ts +4 -0
- package/dist/public/toast-layer.js +5 -0
- package/dist/roleplay-turn.js +98 -0
- package/dist/roleplay-turn.js.map +1 -0
- package/dist/security.js +33 -7
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +2 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +95 -12
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +36 -6
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -29,6 +29,7 @@ import { isOfficialGoogleVertexBaseUrl, parseGoogleServiceAccount } from "./goog
|
|
|
29
29
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, readableHybridSearchTypes } from "./hybrid-search.js";
|
|
30
30
|
import { applyImportFileHints, parseNovelText } from "./parser.js";
|
|
31
31
|
import { aiConversationTaskTypes, attachmentPermissionModules, RECYCLE_BIN_RETENTION_DAYS, Store, versionedEntityTypes, WORK_AGENT_TOOL_IDS } from "./store.js";
|
|
32
|
+
import { composeRoleplayStoredUserContent } from "./roleplay-turn.js";
|
|
32
33
|
import { paginated, parsePagination } from "./pagination.js";
|
|
33
34
|
import { normalizeUploadFileName } from "./utils.js";
|
|
34
35
|
import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
|
|
@@ -428,7 +429,7 @@ const modelSchema = z.object({
|
|
|
428
429
|
outputNote: z.string().max(10_000).optional(),
|
|
429
430
|
preset: jsonObject.optional(),
|
|
430
431
|
thinkingEnabled: z.boolean().optional(),
|
|
431
|
-
thinkingEffort: z.enum(["default", "low", "medium", "high", "xhigh", "max"]).optional(),
|
|
432
|
+
thinkingEffort: z.enum(["default", "auto", "low", "medium", "high", "xhigh", "max"]).optional(),
|
|
432
433
|
multimodalEnabled: z.boolean().optional(),
|
|
433
434
|
imageToolDefault: z.boolean().optional(),
|
|
434
435
|
enabled: z.boolean().optional(),
|
|
@@ -1004,7 +1005,16 @@ function redactAiConversation(record, permissions) {
|
|
|
1004
1005
|
result.agentTools = [
|
|
1005
1006
|
...(permissions.characters !== "none" ? ["recall_self"] : []),
|
|
1006
1007
|
...(permissions.characters !== "none" && permissions.relationships !== "none" ? ["recall_relationship"] : []),
|
|
1008
|
+
...(permissions.characters !== "none"
|
|
1009
|
+
&& (permissions.relationships !== "none" || permissions.organizations !== "none" || permissions.timeline !== "none")
|
|
1010
|
+
? ["recall_other"]
|
|
1011
|
+
: []),
|
|
1012
|
+
...((permissions.races !== "none" || permissions.organizations !== "none" || permissions.settings !== "none") ? ["recall_known"] : []),
|
|
1007
1013
|
...(permissions.prose !== "none" ? ["recall_story"] : []),
|
|
1014
|
+
...["settings", "characters", "races", "organizations", "timeline", "relationships", "outlines"]
|
|
1015
|
+
.some((module) => permissions[module] !== "none")
|
|
1016
|
+
? ["image"]
|
|
1017
|
+
: [],
|
|
1008
1018
|
"calculate_time"
|
|
1009
1019
|
];
|
|
1010
1020
|
}
|
|
@@ -1105,7 +1115,8 @@ export function createRuntime(options) {
|
|
|
1105
1115
|
const characterAvatarStorage = new AttachmentStorage(options.characterAvatarDirectory
|
|
1106
1116
|
?? (temporaryStorageRoot ? join(temporaryStorageRoot, "character-avatars") : join(dirname(options.databasePath), "character-avatars")), CHARACTER_AVATAR_IMAGE_MAX_BYTES);
|
|
1107
1117
|
mkdirSync(characterAvatarStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
|
|
1108
|
-
const
|
|
1118
|
+
const credentialVault = new CredentialVault(options.masterSecret);
|
|
1119
|
+
const auth = new UserAuthService(database, credentialVault);
|
|
1109
1120
|
const collaborationPresence = new CollaborationPresence(45_000, Date.now, 120_000, 50, { store: new PresenceStore(database) });
|
|
1110
1121
|
const publishCollaborativeChange = (workId, pageKey, options = {}) => {
|
|
1111
1122
|
const actor = currentRequestActor();
|
|
@@ -1197,7 +1208,6 @@ export function createRuntime(options) {
|
|
|
1197
1208
|
return lockedModelId ?? requestedModelId;
|
|
1198
1209
|
};
|
|
1199
1210
|
const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
|
|
1200
|
-
const credentialVault = new CredentialVault(options.masterSecret);
|
|
1201
1211
|
const backups = new S3BackupManager(database, credentialVault, store, attachmentStorage, {
|
|
1202
1212
|
...options.backupOptions,
|
|
1203
1213
|
characterAvatarStorage,
|
|
@@ -1235,7 +1245,8 @@ export function createRuntime(options) {
|
|
|
1235
1245
|
retryPolicy: options.aiRetryPolicy,
|
|
1236
1246
|
retrySleep: options.aiRetrySleep,
|
|
1237
1247
|
aiChatImageMaxBytes: uploadLimits.chatImageBytes,
|
|
1238
|
-
liteLlmPriceCache
|
|
1248
|
+
liteLlmPriceCache,
|
|
1249
|
+
allowPrivateAiEndpoints: options.security?.allowPrivateAiEndpoints === true
|
|
1239
1250
|
});
|
|
1240
1251
|
const app = express();
|
|
1241
1252
|
enforceCaseInsensitiveRouting(app);
|
|
@@ -1422,6 +1433,18 @@ export function createRuntime(options) {
|
|
|
1422
1433
|
throw new AppError(401, "SESSION_REQUIRED", "请使用网页会话管理 API Key");
|
|
1423
1434
|
data(response, auth.getApiKeyStatus(request.authUser.userId));
|
|
1424
1435
|
});
|
|
1436
|
+
app.post("/api/auth/api-key/reveal", (request, response) => {
|
|
1437
|
+
if (!request.authUser || request.authMethod !== "session")
|
|
1438
|
+
throw new AppError(401, "SESSION_REQUIRED", "请使用网页会话管理 API Key");
|
|
1439
|
+
parse(z.object({}).strict(), request.body ?? {});
|
|
1440
|
+
const userId = request.authUser.userId;
|
|
1441
|
+
const revealed = database.transaction(() => {
|
|
1442
|
+
const secret = auth.revealApiKey(userId);
|
|
1443
|
+
store.audit(null, "user.api-key-copied", "user", userId, { prefix: secret.prefix });
|
|
1444
|
+
return { apiKey: secret.apiKey };
|
|
1445
|
+
});
|
|
1446
|
+
data(response, revealed);
|
|
1447
|
+
});
|
|
1425
1448
|
app.post("/api/auth/api-key/reset", (request, response) => {
|
|
1426
1449
|
if (!request.authUser || request.authMethod !== "session")
|
|
1427
1450
|
throw new AppError(401, "SESSION_REQUIRED", "请使用网页会话管理 API Key");
|
|
@@ -1866,6 +1889,10 @@ export function createRuntime(options) {
|
|
|
1866
1889
|
data(response, store.createDraft(request.params.workId, parse(draftSchema, request.body)), 201);
|
|
1867
1890
|
});
|
|
1868
1891
|
app.get("/api/drafts/:draftId", (request, response) => data(response, store.getDraft(request.params.draftId)));
|
|
1892
|
+
app.patch("/api/drafts/:draftId/favorite", (request, response) => {
|
|
1893
|
+
const input = parse(z.object({ isFavorite: z.boolean() }).strict(), request.body);
|
|
1894
|
+
data(response, store.setDraftFavorite(request.params.draftId, input.isFavorite));
|
|
1895
|
+
});
|
|
1869
1896
|
app.patch("/api/drafts/:draftId", (request, response) => {
|
|
1870
1897
|
const { changeNote, expectedVersionNo, ...input } = parse(draftSchema.partial().extend({
|
|
1871
1898
|
changeNote: changeNoteSchema,
|
|
@@ -1889,6 +1916,12 @@ export function createRuntime(options) {
|
|
|
1889
1916
|
});
|
|
1890
1917
|
app.get("/api/works/:workId/settings/context", (request, response) => data(response, store.listSettings(request.params.workId, true)));
|
|
1891
1918
|
app.get("/api/settings/:settingId", (request, response) => data(response, store.getSetting(request.params.settingId)));
|
|
1919
|
+
app.patch("/api/settings/:settingId/favorite", (request, response) => {
|
|
1920
|
+
const input = parse(z.object({ isFavorite: z.boolean() }).strict(), request.body);
|
|
1921
|
+
const setting = store.setSettingFavorite(request.params.settingId, input.isFavorite);
|
|
1922
|
+
publishEntityChange(String(setting.workId), "setting", String(setting.id));
|
|
1923
|
+
data(response, setting);
|
|
1924
|
+
});
|
|
1892
1925
|
app.patch("/api/settings/:settingId", (request, response) => {
|
|
1893
1926
|
const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
1894
1927
|
const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
|
|
@@ -1976,6 +2009,12 @@ export function createRuntime(options) {
|
|
|
1976
2009
|
}
|
|
1977
2010
|
data(response, redactCharacterLinks(result.character, requestPermissions(request)));
|
|
1978
2011
|
});
|
|
2012
|
+
app.patch("/api/characters/:characterId/favorite", (request, response) => {
|
|
2013
|
+
const input = parse(z.object({ isFavorite: z.boolean() }).strict(), request.body);
|
|
2014
|
+
const character = store.setCharacterFavorite(request.params.characterId, input.isFavorite);
|
|
2015
|
+
publishEntityChange(String(character.workId), "character", String(character.id));
|
|
2016
|
+
data(response, redactCharacterLinks(character, requestPermissions(request)));
|
|
2017
|
+
});
|
|
1979
2018
|
app.patch("/api/characters/:characterId", (request, response) => {
|
|
1980
2019
|
const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
|
|
1981
2020
|
const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
|
|
@@ -2184,6 +2223,12 @@ export function createRuntime(options) {
|
|
|
2184
2223
|
app.get("/api/organizations/:organizationId", (request, response) => {
|
|
2185
2224
|
data(response, redactOrganizationMembers(store.getOrganization(request.params.organizationId), requestPermissions(request)));
|
|
2186
2225
|
});
|
|
2226
|
+
app.patch("/api/organizations/:organizationId/favorite", (request, response) => {
|
|
2227
|
+
const input = parse(z.object({ isFavorite: z.boolean() }).strict(), request.body);
|
|
2228
|
+
const organization = store.setOrganizationFavorite(request.params.organizationId, input.isFavorite);
|
|
2229
|
+
publishEntityChange(String(organization.workId), "organization", String(organization.id));
|
|
2230
|
+
data(response, redactOrganizationMembers(organization, requestPermissions(request)));
|
|
2231
|
+
});
|
|
2187
2232
|
app.patch("/api/organizations/:organizationId", (request, response) => {
|
|
2188
2233
|
const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
|
|
2189
2234
|
const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
|
|
@@ -2494,12 +2539,13 @@ export function createRuntime(options) {
|
|
|
2494
2539
|
parse(z.object({}).strict(), request.body ?? {});
|
|
2495
2540
|
const refreshed = await liteLlmPriceCache.refresh();
|
|
2496
2541
|
if (!refreshed) {
|
|
2497
|
-
throw new AppError(502, "
|
|
2542
|
+
throw new AppError(502, "MODEL_PRICE_REFRESH_FAILED", "模型价格刷新失败,历史缓存未改变");
|
|
2498
2543
|
}
|
|
2499
2544
|
data(response, {
|
|
2500
2545
|
refreshed: true,
|
|
2501
2546
|
pricingAvailable: liteLlmPriceCache.hasData(),
|
|
2502
|
-
modelCount: liteLlmPriceCache.getPriceTable().size
|
|
2547
|
+
modelCount: liteLlmPriceCache.getPriceTable().size,
|
|
2548
|
+
sources: liteLlmPriceCache.getSourceStatuses()
|
|
2503
2549
|
});
|
|
2504
2550
|
});
|
|
2505
2551
|
app.get("/api/platform/ai-conversations", (request, response) => {
|
|
@@ -2828,7 +2874,13 @@ export function createRuntime(options) {
|
|
|
2828
2874
|
});
|
|
2829
2875
|
app.post("/api/works/:workId/chat/stream", async (request, response) => {
|
|
2830
2876
|
const input = parse(z.object({
|
|
2831
|
-
instruction:
|
|
2877
|
+
instruction: z.string().max(100_000).default(""),
|
|
2878
|
+
sceneDirection: z.string().max(20_000).optional(),
|
|
2879
|
+
scenePin: z.object({
|
|
2880
|
+
location: z.string().max(200).optional(),
|
|
2881
|
+
present: z.string().max(500).optional(),
|
|
2882
|
+
timeLabel: z.string().max(200).optional()
|
|
2883
|
+
}).strict().optional(),
|
|
2832
2884
|
scope: contextSchema,
|
|
2833
2885
|
modelId: identifier.optional(),
|
|
2834
2886
|
parameters: jsonObject.optional(),
|
|
@@ -2849,7 +2901,6 @@ export function createRuntime(options) {
|
|
|
2849
2901
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2850
2902
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2851
2903
|
}
|
|
2852
|
-
const resolvedInstruction = instructionWithCitations(input.instruction, citations);
|
|
2853
2904
|
const existingRequest = store.findAiConversationStreamRequest(actorScope, request.params.workId, idempotencyKey);
|
|
2854
2905
|
if (existingRequest && input.conversationId && existingRequest.conversationId !== input.conversationId) {
|
|
2855
2906
|
throw new AppError(409, "IDEMPOTENCY_KEY_REUSED", "该请求标识已用于另一项 AI 对话请求");
|
|
@@ -2864,6 +2915,20 @@ export function createRuntime(options) {
|
|
|
2864
2915
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
2865
2916
|
}
|
|
2866
2917
|
const conversationId = String(conversation.id);
|
|
2918
|
+
const isRoleplay = Boolean(conversation.roleplayCharacter);
|
|
2919
|
+
const instructionText = input.instruction.trim();
|
|
2920
|
+
const sceneDirection = isRoleplay ? (input.sceneDirection ?? "").trim() : "";
|
|
2921
|
+
if (!instructionText && !sceneDirection) {
|
|
2922
|
+
throw new AppError(400, "INSTRUCTION_REQUIRED", isRoleplay ? "请输入角色台词或场景旁白" : "请输入指令");
|
|
2923
|
+
}
|
|
2924
|
+
if (!existingRequest && isRoleplay && input.scenePin !== undefined) {
|
|
2925
|
+
store.setAiConversationScenePin(conversationId, request.params.workId, input.scenePin);
|
|
2926
|
+
}
|
|
2927
|
+
const storedUserContent = isRoleplay
|
|
2928
|
+
? composeRoleplayStoredUserContent(sceneDirection, instructionText)
|
|
2929
|
+
: instructionText;
|
|
2930
|
+
const resolvedInstruction = instructionWithCitations(instructionText, citations);
|
|
2931
|
+
const mentionSource = [sceneDirection, resolvedInstruction].filter(Boolean).join("\n");
|
|
2867
2932
|
const modelId = resolveConversationModelId(request.params.workId, conversationId, input.modelId);
|
|
2868
2933
|
const permissions = requestPermissions(request, request.params.workId);
|
|
2869
2934
|
const controller = new AbortController();
|
|
@@ -2904,7 +2969,7 @@ export function createRuntime(options) {
|
|
|
2904
2969
|
workId: request.params.workId,
|
|
2905
2970
|
modelId,
|
|
2906
2971
|
scope: input.scope,
|
|
2907
|
-
instruction:
|
|
2972
|
+
instruction: mentionSource,
|
|
2908
2973
|
excludeConversationMessageId: input.currentMessageId
|
|
2909
2974
|
}, { ignoreWarning: input.ignoreContextWarning === true });
|
|
2910
2975
|
preparedConversation = redactAiConversation(store.getAiConversationSummary(conversationId), permissions);
|
|
@@ -2929,7 +2994,7 @@ export function createRuntime(options) {
|
|
|
2929
2994
|
: ai.resolveInstructionMentions({
|
|
2930
2995
|
workId: request.params.workId,
|
|
2931
2996
|
taskType: "chat",
|
|
2932
|
-
instruction:
|
|
2997
|
+
instruction: mentionSource,
|
|
2933
2998
|
scope: input.scope,
|
|
2934
2999
|
conversationId
|
|
2935
3000
|
});
|
|
@@ -2946,7 +3011,7 @@ export function createRuntime(options) {
|
|
|
2946
3011
|
idempotencyKey,
|
|
2947
3012
|
requestHash,
|
|
2948
3013
|
userMessage: {
|
|
2949
|
-
content:
|
|
3014
|
+
content: storedUserContent,
|
|
2950
3015
|
citations,
|
|
2951
3016
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
2952
3017
|
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length) ? { metadata: {
|
|
@@ -3004,6 +3069,7 @@ export function createRuntime(options) {
|
|
|
3004
3069
|
const suggestion = await ai.createStreamingChat({
|
|
3005
3070
|
workId: request.params.workId,
|
|
3006
3071
|
instruction: resolvedInstruction,
|
|
3072
|
+
...(sceneDirection ? { sceneDirection } : {}),
|
|
3007
3073
|
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
3008
3074
|
scope: input.scope,
|
|
3009
3075
|
signal: controller.signal,
|