@musnows/scriverse 1.0.11 → 1.1.0

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.
Files changed (43) hide show
  1. package/README.en.md +6 -3
  2. package/README.md +6 -3
  3. package/dist/ai-stream-steer.js +96 -0
  4. package/dist/ai-stream-steer.js.map +1 -0
  5. package/dist/ai-write-plans.js +44 -38
  6. package/dist/ai-write-plans.js.map +1 -1
  7. package/dist/ai.js +241 -128
  8. package/dist/ai.js.map +1 -1
  9. package/dist/app.js +134 -14
  10. package/dist/app.js.map +1 -1
  11. package/dist/attachment-storage.js +3 -0
  12. package/dist/attachment-storage.js.map +1 -1
  13. package/dist/chapter-annotation-note.js +14 -0
  14. package/dist/chapter-annotation-note.js.map +1 -0
  15. package/dist/character-structured-fields.js +7 -0
  16. package/dist/character-structured-fields.js.map +1 -0
  17. package/dist/cli-contract.js +1 -1
  18. package/dist/cli-contract.js.map +1 -1
  19. package/dist/database.js +29 -2
  20. package/dist/database.js.map +1 -1
  21. package/dist/im.js +2 -1
  22. package/dist/im.js.map +1 -1
  23. package/dist/public/ai-prompt-keyboard.js +5 -1
  24. package/dist/public/ai-prompt-queue.js +226 -0
  25. package/dist/public/ai-send-mode.js +31 -0
  26. package/dist/public/app.js +689 -74
  27. package/dist/public/chapter-editor-scroll.d.ts +37 -0
  28. package/dist/public/chapter-editor-scroll.js +52 -0
  29. package/dist/public/character-profile.d.ts +7 -0
  30. package/dist/public/character-profile.js +123 -5
  31. package/dist/public/index.html +31 -10
  32. package/dist/public/styles.css +75 -8
  33. package/dist/security.js +22 -3
  34. package/dist/security.js.map +1 -1
  35. package/dist/server-runtime.js +2 -0
  36. package/dist/server-runtime.js.map +1 -1
  37. package/dist/store.js +7 -6
  38. package/dist/store.js.map +1 -1
  39. package/dist/user-auth.js +125 -15
  40. package/dist/user-auth.js.map +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -14,6 +14,7 @@ import { AI_PROVIDER_PROTOCOL_OPTIONS, AI_PROVIDER_PROTOCOLS, AI_THINKING_TYPES,
14
14
  import { MAX_AI_ANALYSIS_TIMEOUT_SECONDS, MIN_AI_ANALYSIS_TIMEOUT_SECONDS } from "./ai-analysis-timeout.js";
15
15
  import { aiConversationExportContentDisposition, exportAiConversationMarkdown } from "./ai-conversation-export.js";
16
16
  import { DEFAULT_AI_CHAT_TAB_LIMIT } from "./ai-chat-tab-limit.js";
17
+ import { DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH } from "./chapter-annotation-note.js";
17
18
  import { MAX_AI_STREAM_IDLE_TIMEOUT_SECONDS, MIN_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
18
19
  import { AttachmentStorage } from "./attachment-storage.js";
19
20
  import { attachmentDownloadFileName, inlineContentDisposition } from "./attachment-download.js";
@@ -22,6 +23,7 @@ import { LiteLlmPriceCache } from "./ai-model-pricing.js";
22
23
  import { MIN_AGENT_TOOL_CALL_LIMIT, resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
23
24
  import { SEMANTIC_SOURCE_TYPES } from "./semantic-search.js";
24
25
  import { CHARACTER_EXTRACTION_MAX_ALIASES, CHARACTER_EXTRACTION_MAX_CANDIDATES, CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH, CHARACTER_EXTRACTION_MAX_NAME_LENGTH, CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH } from "./character-extraction.js";
26
+ import { characterAttributesInputSchema, characterStateInputSchema } from "./character-structured-fields.js";
25
27
  import { AiWritePlanManager, AI_USER_QUESTION_STATUSES, AI_WRITE_PLAN_STATUSES, aiWriteToolDescriptions, aiWriteToolLabels, aiWriteToolsUpdateSchema, answerAiUserQuestionSchema, resolveAiWritePlanMaxOperations } from "./ai-write-plans.js";
26
28
  import { CredentialVault } from "./credential-vault.js";
27
29
  import { Database } from "./database.js";
@@ -39,7 +41,7 @@ import { composeRoleplayStoredUserContent } from "./roleplay-turn.js";
39
41
  import { ROLEPLAY_MEMORY_CATEGORIES, ROLEPLAY_MEMORY_CERTAINTY, ROLEPLAY_MEMORY_IMPORTANCE, ROLEPLAY_MEMORY_STATUSES } from "./roleplay-memory.js";
40
42
  import { paginated, parsePagination } from "./pagination.js";
41
43
  import { normalizeUploadFileName } from "./utils.js";
42
- import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, normalizeApiPath, resolveTrustProxySetting, verifySetupToken } from "./security.js";
44
+ import { assertSafeAiEndpoint, assertSafeS3Endpoint, createApiRateLimitMiddleware, createAuthenticationRateLimitMiddleware, createBasicAuthMiddleware, createCaptchaRateLimitMiddleware, createExpensiveApiRateLimitMiddleware, createSameOriginMiddleware, createSecurityHeadersMiddleware, createUploadRateLimitMiddleware, enforceCaseInsensitiveRouting, isRegistrationEnabled, normalizeApiPath, resolveRegistrationMode, resolveTrustProxySetting, verifySetupToken } from "./security.js";
43
45
  import { ImageCaptchaService } from "./image-captcha.js";
44
46
  import { ImService } from "./im.js";
45
47
  import { ImOrchestrator } from "./im-orchestrator.js";
@@ -146,6 +148,7 @@ const registrationSchema = z.object({
146
148
  password: passwordSchema,
147
149
  passwordConfirmation: passwordSchema,
148
150
  setupToken: z.string().max(500).optional(),
151
+ inviteCode: z.string().trim().max(32).optional(),
149
152
  ...captchaFields
150
153
  }).strict().refine((input) => input.password === input.passwordConfirmation, {
151
154
  path: ["passwordConfirmation"],
@@ -164,6 +167,20 @@ const desktopLoginSchema = z.object({
164
167
  clientVersion: z.string().trim().min(1).max(80),
165
168
  ...captchaFields
166
169
  }).strict();
170
+ const desktopRegistrationSchema = z.object({
171
+ username: usernameSchema,
172
+ password: passwordSchema,
173
+ passwordConfirmation: passwordSchema,
174
+ setupToken: z.string().max(500).optional(),
175
+ inviteCode: z.string().trim().max(32).optional(),
176
+ desktopId: z.string().uuid(),
177
+ profileId: z.string().uuid(),
178
+ clientVersion: z.string().trim().min(1).max(80),
179
+ ...captchaFields
180
+ }).strict().refine((input) => input.password === input.passwordConfirmation, {
181
+ path: ["passwordConfirmation"],
182
+ message: "两次输入的密码不一致"
183
+ });
167
184
  const userUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), status: z.enum(["active", "disabled"]).optional() }).strict();
168
185
  const memberRoleValueSchema = z.enum(["editor", "settings-editor", "viewer"]);
169
186
  const moduleAccessSchema = z.enum(["none", "read", "write"]);
@@ -299,9 +316,9 @@ const characterSchema = z.object({
299
316
  aliases: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
300
317
  raceId: identifier.nullable().optional(),
301
318
  organizationIds: z.array(identifier).max(100).optional(),
302
- attributes: jsonObject.optional(),
319
+ attributes: characterAttributesInputSchema.optional(),
303
320
  profile: jsonObject.optional(),
304
- currentState: jsonObject.optional(),
321
+ currentState: characterStateInputSchema.optional(),
305
322
  lockedFields: optionalStrings,
306
323
  firstChapterId: identifier.nullable().optional()
307
324
  }).strict();
@@ -1266,6 +1283,7 @@ function redactVersionSnapshots(value, mapper) {
1266
1283
  export function createRuntime(options) {
1267
1284
  const uploadLimits = options.uploadLimits ?? DEFAULT_IMAGE_UPLOAD_LIMITS;
1268
1285
  const aiChatTabLimit = options.aiChatTabLimit ?? DEFAULT_AI_CHAT_TAB_LIMIT;
1286
+ const chapterAnnotationNoteMaxLength = options.chapterAnnotationNoteMaxLength ?? DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH;
1269
1287
  const requestedAiStreamHeartbeatIntervalMs = Number(options.aiStreamHeartbeatIntervalMs);
1270
1288
  const aiStreamHeartbeatIntervalMs = Number.isFinite(requestedAiStreamHeartbeatIntervalMs)
1271
1289
  ? Math.max(1, Math.trunc(requestedAiStreamHeartbeatIntervalMs))
@@ -1437,7 +1455,7 @@ export function createRuntime(options) {
1437
1455
  auth,
1438
1456
  resolveAnalysisTask: (workId, input) => ai.resolveTaskInput(workId, input),
1439
1457
  startAnalysisTask: (workId, input) => store.createTask(workId, input)
1440
- });
1458
+ }, { chapterAnnotationNoteMaxLength });
1441
1459
  ai.attachWritePlanManager(aiWritePlanManager);
1442
1460
  const app = express();
1443
1461
  enforceCaseInsensitiveRouting(app);
@@ -1487,6 +1505,7 @@ export function createRuntime(options) {
1487
1505
  protocols: [...AI_PROVIDER_PROTOCOLS],
1488
1506
  development: options.developmentServer === true,
1489
1507
  aiChatTabLimit,
1508
+ chapterAnnotationNoteMaxLength,
1490
1509
  uploadLimits
1491
1510
  });
1492
1511
  });
@@ -1502,15 +1521,38 @@ export function createRuntime(options) {
1502
1521
  app.use(createSameOriginMiddleware());
1503
1522
  app.use("/api/sync/works", express.json({ limit: "3mb" }));
1504
1523
  app.use(express.json({ limit: "2mb" }));
1524
+ const registrationMode = resolveRegistrationMode(options.security);
1525
+ const registrationEnabled = isRegistrationEnabled(registrationMode);
1526
+ const registrationPolicy = (setupRequired) => ({
1527
+ registrationMode,
1528
+ registrationOpen: registrationEnabled,
1529
+ inviteRequired: registrationMode === "invite" && !setupRequired
1530
+ });
1531
+ const resolvedInviteCode = (inviteCode, setupRequired) => {
1532
+ if (registrationMode !== "invite" || setupRequired)
1533
+ return undefined;
1534
+ const trimmed = inviteCode?.trim() ?? "";
1535
+ if (!trimmed)
1536
+ throw new AppError(400, "INVITE_CODE_REQUIRED", "当前部署需要邀请码才能注册");
1537
+ return trimmed;
1538
+ };
1505
1539
  app.get("/api/auth/session", (request, response) => {
1506
1540
  const desktopSession = auth.authenticateDesktop(request);
1507
1541
  const session = desktopSession ? null : auth.authenticate(request);
1508
- const registrationOpen = options.security?.allowRegistration === true;
1509
1542
  const setupRequired = !auth.hasUsers();
1510
1543
  const setupTokenRequired = setupRequired && Boolean(options.security?.setupToken);
1544
+ const policy = registrationPolicy(setupRequired);
1511
1545
  const developmentUser = getDevelopmentUser();
1512
1546
  if (!session && developmentUser) {
1513
- data(response, { authenticated: true, user: developmentUser, csrfToken: null, bootId, setupRequired: false, setupTokenRequired: false, registrationOpen });
1547
+ data(response, {
1548
+ authenticated: true,
1549
+ user: developmentUser,
1550
+ csrfToken: null,
1551
+ bootId,
1552
+ setupRequired: false,
1553
+ setupTokenRequired: false,
1554
+ ...registrationPolicy(false)
1555
+ });
1514
1556
  return;
1515
1557
  }
1516
1558
  const interactiveSession = desktopSession ?? session;
@@ -1522,25 +1564,33 @@ export function createRuntime(options) {
1522
1564
  bootId,
1523
1565
  setupRequired: false,
1524
1566
  setupTokenRequired: false,
1525
- registrationOpen
1567
+ ...registrationPolicy(false)
1526
1568
  }
1527
- : { authenticated: false, user: null, csrfToken: null, bootId, setupRequired, setupTokenRequired, registrationOpen });
1569
+ : { authenticated: false, user: null, csrfToken: null, bootId, setupRequired, setupTokenRequired, ...policy });
1528
1570
  });
1529
1571
  app.get("/api/auth/captcha", (_request, response) => {
1530
1572
  data(response, captcha.create());
1531
1573
  });
1532
1574
  app.post("/api/auth/register", (request, response) => {
1533
- if (options.security?.allowRegistration !== true) {
1575
+ if (!registrationEnabled) {
1534
1576
  throw new AppError(403, "REGISTRATION_DISABLED", "当前部署已关闭新用户注册");
1535
1577
  }
1536
1578
  const input = parse(registrationSchema, request.body);
1537
1579
  captcha.consume(input.captchaId, input.captchaAnswer);
1538
- if (!auth.hasUsers() && !verifySetupToken(options.security?.setupToken, input.setupToken)) {
1580
+ const setupRequired = !auth.hasUsers();
1581
+ if (setupRequired && !verifySetupToken(options.security?.setupToken, input.setupToken)) {
1539
1582
  throw new AppError(403, "SETUP_TOKEN_INVALID", "初始化令牌无效或未配置");
1540
1583
  }
1541
- const result = auth.register({ username: input.username, password: input.password });
1584
+ const result = auth.register({
1585
+ username: input.username,
1586
+ password: input.password,
1587
+ inviteCode: resolvedInviteCode(input.inviteCode, setupRequired)
1588
+ });
1542
1589
  setSessionCookie(response, result.token, request.secure);
1543
- runWithRequestActor(result.session.user, () => store.audit(null, "user.registered", "user", result.session.user.userId, { role: result.session.user.role }));
1590
+ runWithRequestActor(result.session.user, () => store.audit(null, "user.registered", "user", result.session.user.userId, {
1591
+ role: result.session.user.role,
1592
+ ...(result.inviteId ? { inviteId: result.inviteId } : {})
1593
+ }));
1544
1594
  logger.info("auth.registration.succeeded", { actorRef: accountReference(result.session.user.userId) });
1545
1595
  data(response, { user: result.session.user, csrfToken: result.session.csrfToken }, 201);
1546
1596
  });
@@ -1567,6 +1617,34 @@ export function createRuntime(options) {
1567
1617
  logger.info("auth.desktop_login.succeeded", { actorRef: accountReference(result.session.user.userId) });
1568
1618
  data(response, { token: result.token, expiresAt: result.session.expiresAt, user: result.session.user });
1569
1619
  });
1620
+ app.post("/api/desktop/auth/register", (request, response) => {
1621
+ if (!registrationEnabled) {
1622
+ throw new AppError(403, "REGISTRATION_DISABLED", "当前部署已关闭新用户注册");
1623
+ }
1624
+ const input = parse(desktopRegistrationSchema, request.body);
1625
+ captcha.consume(input.captchaId, input.captchaAnswer);
1626
+ const setupRequired = !auth.hasUsers();
1627
+ if (setupRequired && !verifySetupToken(options.security?.setupToken, input.setupToken)) {
1628
+ throw new AppError(403, "SETUP_TOKEN_INVALID", "初始化令牌无效或未配置");
1629
+ }
1630
+ const result = auth.registerDesktop({
1631
+ username: input.username,
1632
+ password: input.password,
1633
+ inviteCode: resolvedInviteCode(input.inviteCode, setupRequired),
1634
+ desktopId: input.desktopId,
1635
+ profileId: input.profileId,
1636
+ clientVersion: input.clientVersion
1637
+ });
1638
+ imOrchestrator.disconnectUser(result.session.user.userId);
1639
+ response.setHeader("Cache-Control", "no-store");
1640
+ runWithRequestActor(result.session.user, () => store.audit(null, "user.registered", "user", result.session.user.userId, {
1641
+ role: result.session.user.role,
1642
+ source: "desktop",
1643
+ ...(result.inviteId ? { inviteId: result.inviteId } : {})
1644
+ }));
1645
+ logger.info("auth.desktop_registration.succeeded", { actorRef: accountReference(result.session.user.userId) });
1646
+ data(response, { token: result.token, expiresAt: result.session.expiresAt, user: result.session.user }, 201);
1647
+ });
1570
1648
  app.use(createUserSessionMiddleware(auth, {
1571
1649
  disabled: options.disableUserAuth === true,
1572
1650
  resolveBypassUser: getDevelopmentUser
@@ -1714,6 +1792,28 @@ export function createRuntime(options) {
1714
1792
  store.audit(null, "user.updated", "user", updated.userId, { role: updated.role, status: updated.status });
1715
1793
  data(response, updated);
1716
1794
  });
1795
+ app.get("/api/registration-invites", (request, response) => {
1796
+ if (!request.authUser)
1797
+ throw new AppError(401, "AUTH_REQUIRED", "请先登录");
1798
+ if (request.authUser.role !== "admin")
1799
+ throw new AppError(403, "ADMIN_REQUIRED", "该操作仅限系统管理员");
1800
+ data(response, {
1801
+ registrationMode,
1802
+ registrationOpen: registrationEnabled,
1803
+ items: auth.listRegistrationInvites()
1804
+ });
1805
+ });
1806
+ app.post("/api/registration-invites", (request, response) => {
1807
+ if (!request.authUser)
1808
+ throw new AppError(401, "AUTH_REQUIRED", "请先登录");
1809
+ if (request.authUser.role !== "admin")
1810
+ throw new AppError(403, "ADMIN_REQUIRED", "该操作仅限系统管理员");
1811
+ parse(z.object({}).strict(), request.body ?? {});
1812
+ const created = auth.createRegistrationInvite(request.authUser);
1813
+ store.audit(null, "registration-invite.created", "registration-invite", created.id, {});
1814
+ logger.info("auth.registration_invite.created", { actorRef: accountReference(request.authUser.userId) });
1815
+ data(response, created, 201);
1816
+ });
1717
1817
  const requireImUser = (request) => {
1718
1818
  if (!request.authUser || !hasInteractiveSession(request)) {
1719
1819
  throw new AppError(401, "SESSION_REQUIRED", "请使用交互式会话访问 IM");
@@ -2404,12 +2504,12 @@ export function createRuntime(options) {
2404
2504
  kind: z.enum(["note", "todo"]),
2405
2505
  startLine: z.number().int().positive(),
2406
2506
  endLine: z.number().int().positive(),
2407
- note: z.string().trim().min(1).max(2000)
2507
+ note: z.string().trim().min(1).max(chapterAnnotationNoteMaxLength)
2408
2508
  }).strict().refine((value) => value.endLine >= value.startLine, { message: "结束行不能早于开始行", path: ["endLine"] }), request.body);
2409
2509
  data(response, store.createChapterAnnotation(request.params.chapterId, input), 201);
2410
2510
  });
2411
2511
  app.patch("/api/chapter-annotations/:annotationId", (request, response) => {
2412
- const input = parse(z.object({ note: z.string().trim().min(1).max(2000).optional(), status: z.enum(["open", "resolved"]).optional(), expectedVersionNo: expectedVersionNoSchema }).strict().refine((value) => value.note !== undefined || value.status !== undefined, { message: "至少需要修改一项" }), request.body);
2512
+ const input = parse(z.object({ note: z.string().trim().min(1).max(chapterAnnotationNoteMaxLength).optional(), status: z.enum(["open", "resolved"]).optional(), expectedVersionNo: expectedVersionNoSchema }).strict().refine((value) => value.note !== undefined || value.status !== undefined, { message: "至少需要修改一项" }), request.body);
2413
2513
  const { expectedVersionNo, ...update } = input;
2414
2514
  data(response, store.updateChapterAnnotation(request.params.annotationId, update, expectedVersionNo));
2415
2515
  });
@@ -3475,6 +3575,19 @@ export function createRuntime(options) {
3475
3575
  const permissions = requestPermissions(request, String(conversation.workId));
3476
3576
  data(response, redactAiConversationMessage(message, permissions), 201);
3477
3577
  });
3578
+ app.post("/api/ai-conversations/:conversationId/steer", (request, response) => {
3579
+ const input = parse(z.object({
3580
+ content: nonEmpty.max(100_000)
3581
+ }).strict(), request.body ?? {});
3582
+ const conversation = store.getAiConversationSummary(request.params.conversationId);
3583
+ const workId = String(conversation.workId);
3584
+ const permissions = requestPermissions(request, workId);
3585
+ if (!canWriteWorkModule(permissions, "ai-chat")) {
3586
+ throw new AppError(403, "WORK_MODULE_WRITE_DENIED", "你没有使用创作助手的权限");
3587
+ }
3588
+ const entry = ai.enqueueSteer(workId, request.params.conversationId, input.content);
3589
+ data(response, { ...entry, status: "pending" }, 202);
3590
+ });
3478
3591
  app.post("/api/ai-conversations/:conversationId/context/prepare", async (request, response) => {
3479
3592
  const input = parse(z.object({
3480
3593
  modelId: identifier.optional(),
@@ -3972,6 +4085,7 @@ export function createRuntime(options) {
3972
4085
  if (!response.writableEnded && !response.destroyed)
3973
4086
  response.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
3974
4087
  };
4088
+ let releaseSteer = () => undefined;
3975
4089
  try {
3976
4090
  if (!existingRequest) {
3977
4091
  preparedChatImageAttachments = await ai.prepareChatImageAttachments(request.params.workId, modelId, input.imageAttachmentIds ?? [], permissions);
@@ -4092,6 +4206,7 @@ export function createRuntime(options) {
4092
4206
  questionId: pendingQuestion.id
4093
4207
  });
4094
4208
  }
4209
+ releaseSteer = ai.steerMailbox.begin(conversationId);
4095
4210
  const result = await ai.createStreamingChat({
4096
4211
  workId: request.params.workId,
4097
4212
  instruction: resolvedInstruction,
@@ -4103,6 +4218,10 @@ export function createRuntime(options) {
4103
4218
  onToolCall: (toolCall, round) => sendEvent("tool_call", { ...toolCall, round }),
4104
4219
  onProcessStep: (step) => sendEvent("process_step", step),
4105
4220
  onContextCompacted: (event) => sendEvent("context_compacted", event),
4221
+ onSteer: (event) => sendEvent("steer", {
4222
+ message: redactAiConversationMessage(event.message, permissions),
4223
+ applied: true
4224
+ }),
4106
4225
  conversationId,
4107
4226
  excludeConversationMessageId: currentMessageId,
4108
4227
  ...(currentMessageId ? { assistantMessageRequestId: `assistant:${currentMessageId}` } : {}),
@@ -4159,6 +4278,7 @@ export function createRuntime(options) {
4159
4278
  }
4160
4279
  }
4161
4280
  finally {
4281
+ releaseSteer();
4162
4282
  stopStreamHeartbeat();
4163
4283
  if (streamRequestId && !streamRequestFinished && !stopping) {
4164
4284
  store.finishAiConversationStreamRequest(streamRequestId, "cancelled", "stream_closed");