@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/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 auth = new UserAuthService(database);
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: nonEmpty.max(100_000),
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: resolvedInstruction,
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: resolvedInstruction,
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: input.instruction,
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,