@musnows/scriverse 0.8.7 → 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.js +532 -131
- package/dist/ai.js.map +1 -1
- package/dist/app.js +73 -8
- package/dist/app.js.map +1 -1
- package/dist/database.js +94 -2
- package/dist/database.js.map +1 -1
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +448 -72
- package/dist/public/index.html +23 -10
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +66 -15
- 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";
|
|
@@ -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);
|
|
@@ -2829,7 +2874,13 @@ export function createRuntime(options) {
|
|
|
2829
2874
|
});
|
|
2830
2875
|
app.post("/api/works/:workId/chat/stream", async (request, response) => {
|
|
2831
2876
|
const input = parse(z.object({
|
|
2832
|
-
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(),
|
|
2833
2884
|
scope: contextSchema,
|
|
2834
2885
|
modelId: identifier.optional(),
|
|
2835
2886
|
parameters: jsonObject.optional(),
|
|
@@ -2850,7 +2901,6 @@ export function createRuntime(options) {
|
|
|
2850
2901
|
if (store.getChapter(citation.chapterId).workId !== request.params.workId)
|
|
2851
2902
|
throw new AppError(400, "CITATION_WORK_MISMATCH", "引用章节不属于当前作品");
|
|
2852
2903
|
}
|
|
2853
|
-
const resolvedInstruction = instructionWithCitations(input.instruction, citations);
|
|
2854
2904
|
const existingRequest = store.findAiConversationStreamRequest(actorScope, request.params.workId, idempotencyKey);
|
|
2855
2905
|
if (existingRequest && input.conversationId && existingRequest.conversationId !== input.conversationId) {
|
|
2856
2906
|
throw new AppError(409, "IDEMPOTENCY_KEY_REUSED", "该请求标识已用于另一项 AI 对话请求");
|
|
@@ -2865,6 +2915,20 @@ export function createRuntime(options) {
|
|
|
2865
2915
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
2866
2916
|
}
|
|
2867
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");
|
|
2868
2932
|
const modelId = resolveConversationModelId(request.params.workId, conversationId, input.modelId);
|
|
2869
2933
|
const permissions = requestPermissions(request, request.params.workId);
|
|
2870
2934
|
const controller = new AbortController();
|
|
@@ -2905,7 +2969,7 @@ export function createRuntime(options) {
|
|
|
2905
2969
|
workId: request.params.workId,
|
|
2906
2970
|
modelId,
|
|
2907
2971
|
scope: input.scope,
|
|
2908
|
-
instruction:
|
|
2972
|
+
instruction: mentionSource,
|
|
2909
2973
|
excludeConversationMessageId: input.currentMessageId
|
|
2910
2974
|
}, { ignoreWarning: input.ignoreContextWarning === true });
|
|
2911
2975
|
preparedConversation = redactAiConversation(store.getAiConversationSummary(conversationId), permissions);
|
|
@@ -2930,7 +2994,7 @@ export function createRuntime(options) {
|
|
|
2930
2994
|
: ai.resolveInstructionMentions({
|
|
2931
2995
|
workId: request.params.workId,
|
|
2932
2996
|
taskType: "chat",
|
|
2933
|
-
instruction:
|
|
2997
|
+
instruction: mentionSource,
|
|
2934
2998
|
scope: input.scope,
|
|
2935
2999
|
conversationId
|
|
2936
3000
|
});
|
|
@@ -2947,7 +3011,7 @@ export function createRuntime(options) {
|
|
|
2947
3011
|
idempotencyKey,
|
|
2948
3012
|
requestHash,
|
|
2949
3013
|
userMessage: {
|
|
2950
|
-
content:
|
|
3014
|
+
content: storedUserContent,
|
|
2951
3015
|
citations,
|
|
2952
3016
|
...(input.currentMessageId ? { existingMessageId: input.currentMessageId } : {}),
|
|
2953
3017
|
...((modelId || mentionCharacterIds.length || mentionRaceIds.length || mentionOrganizationIds.length || input.imageAttachmentIds?.length) ? { metadata: {
|
|
@@ -3005,6 +3069,7 @@ export function createRuntime(options) {
|
|
|
3005
3069
|
const suggestion = await ai.createStreamingChat({
|
|
3006
3070
|
workId: request.params.workId,
|
|
3007
3071
|
instruction: resolvedInstruction,
|
|
3072
|
+
...(sceneDirection ? { sceneDirection } : {}),
|
|
3008
3073
|
// 仍由生成路径基于原始范围持久化累计注入,保证预解析不会吞掉本轮自动命中。
|
|
3009
3074
|
scope: input.scope,
|
|
3010
3075
|
signal: controller.signal,
|