@musnows/scriverse 0.7.1 → 0.7.3

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 (49) hide show
  1. package/README.md +5 -0
  2. package/dist/ai.js +248 -250
  3. package/dist/ai.js.map +1 -1
  4. package/dist/app.js +194 -40
  5. package/dist/app.js.map +1 -1
  6. package/dist/attachment-storage.js +28 -12
  7. package/dist/attachment-storage.js.map +1 -1
  8. package/dist/backup-encryption.js +129 -0
  9. package/dist/backup-encryption.js.map +1 -0
  10. package/dist/collaboration-presence.js +200 -18
  11. package/dist/collaboration-presence.js.map +1 -1
  12. package/dist/database.js +215 -4
  13. package/dist/database.js.map +1 -1
  14. package/dist/docx-export.js +1 -1
  15. package/dist/docx-export.js.map +1 -1
  16. package/dist/domain.js +18 -0
  17. package/dist/domain.js.map +1 -1
  18. package/dist/image-metadata.js +17 -2
  19. package/dist/image-metadata.js.map +1 -1
  20. package/dist/presence-store.js +160 -0
  21. package/dist/presence-store.js.map +1 -0
  22. package/dist/public/ai-context-meter.js +27 -0
  23. package/dist/public/ai-mentions.js +14 -0
  24. package/dist/public/app.js +1018 -292
  25. package/dist/public/background-task-center.js +1 -1
  26. package/dist/public/chapter-editor-virtualization.js +52 -0
  27. package/dist/public/index.html +44 -22
  28. package/dist/public/presence-client-id.d.ts +10 -0
  29. package/dist/public/presence-client-id.js +31 -0
  30. package/dist/public/relationship-graph.js +24 -4
  31. package/dist/public/s3-backup-ui.d.ts +10 -0
  32. package/dist/public/s3-backup-ui.js +29 -0
  33. package/dist/public/setting-filters.d.ts +16 -0
  34. package/dist/public/setting-filters.js +19 -0
  35. package/dist/public/styles.css +39 -9
  36. package/dist/s3-backup.js +197 -12
  37. package/dist/s3-backup.js.map +1 -1
  38. package/dist/security.js +82 -20
  39. package/dist/security.js.map +1 -1
  40. package/dist/server-runtime.js +36 -18
  41. package/dist/server-runtime.js.map +1 -1
  42. package/dist/store.js +77 -18
  43. package/dist/store.js.map +1 -1
  44. package/dist/upload-limits.js +35 -0
  45. package/dist/upload-limits.js.map +1 -0
  46. package/dist/user-auth.js +11 -1
  47. package/dist/user-auth.js.map +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +3 -2
package/dist/store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DRAFT_SETTING_MODULES } from "./domain.js";
2
2
  import { createHash } from "node:crypto";
3
- import { PLATFORM_AI_WORK_ID } from "./database.js";
3
+ import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID } from "./database.js";
4
4
  import { exportWorkDocx } from "./docx-export.js";
5
5
  import { AppError, notFound } from "./errors.js";
6
6
  import { accountReference, logger } from "./logger.js";
@@ -10,6 +10,7 @@ import { canWriteWorkModule, classifyWorkModulePermissions, emptyWorkModulePermi
10
10
  import { countWords, documentShortSearchTerms, id, json, normalizeDocumentSearchText, normalizeParagraphSpacing, now, splitDocumentParagraphs } from "./utils.js";
11
11
  import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
12
12
  import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
13
+ const WORK_LIST_BATCH_SIZE = 500;
13
14
  export const attachmentPermissionModules = ["prose", "drafts", "settings", "characters", "races", "organizations"];
14
15
  export const WORK_AGENT_TOOL_IDS = [
15
16
  "story_index",
@@ -37,6 +38,7 @@ export function normalizeWorkAgentTools(value) {
37
38
  }
38
39
  return WORK_AGENT_TOOL_IDS.filter((toolId) => enabled.has(toolId));
39
40
  }
41
+ const galaxyFrameRates = [24, 30, 60, 90, 120, 144, 165, 240];
40
42
  const defaultPlatformPageSizes = {
41
43
  drafts: 30,
42
44
  settings: 30,
@@ -78,7 +80,7 @@ function platformPageSizes(value) {
78
80
  }
79
81
  function galaxyFrameRate(value) {
80
82
  const candidate = Number(value);
81
- return candidate === 24 || candidate === 60 ? candidate : 30;
83
+ return galaxyFrameRates.includes(candidate) ? candidate : 30;
82
84
  }
83
85
  function settingsFromInput(settingsMarkdown, settings, fallback = []) {
84
86
  if (settingsMarkdown !== undefined)
@@ -265,7 +267,7 @@ export class Store {
265
267
  db;
266
268
  constructor(db) {
267
269
  this.db = db;
268
- this.backfillEntityVersionBaselines();
270
+ this.migrateEntityVersionBaselines();
269
271
  }
270
272
  currentEntityVersionNo(type, entityId) {
271
273
  const row = this.db.get("SELECT MAX(version_no) AS version_no FROM entity_versions WHERE entity_type = ? AND entity_id = ?", type, entityId);
@@ -489,6 +491,26 @@ export class Store {
489
491
  }
490
492
  });
491
493
  }
494
+ migrateEntityVersionBaselines() {
495
+ const migrationSql = "SELECT 1 AS present FROM schema_migrations WHERE version = ?";
496
+ if (this.db.get(migrationSql, ENTITY_VERSION_BASELINE_MIGRATION_VERSION))
497
+ return;
498
+ this.db.transaction(() => {
499
+ if (this.db.get(migrationSql, ENTITY_VERSION_BASELINE_MIGRATION_VERSION))
500
+ return;
501
+ if (!this.db.get("SELECT 1 AS present FROM entity_versions LIMIT 1")) {
502
+ this.backfillEntityVersionBaselines();
503
+ }
504
+ const integrity = this.db.all("PRAGMA integrity_check");
505
+ if (integrity.some((row) => row.integrity_check !== "ok")) {
506
+ throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
507
+ }
508
+ const foreignKeys = this.db.all("PRAGMA foreign_key_check");
509
+ if (foreignKeys.length > 0)
510
+ throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
511
+ this.db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", ENTITY_VERSION_BASELINE_MIGRATION_VERSION, now());
512
+ });
513
+ }
492
514
  listEntityVersions(type, entityId) {
493
515
  const rows = this.db.all(`SELECT version.*, user.display_name AS actor_display_name, user.username AS actor_username
494
516
  FROM entity_versions version LEFT JOIN users user ON user.id = version.created_by_user_id
@@ -661,11 +683,11 @@ export class Store {
661
683
  listWorks() {
662
684
  const actor = currentRequestActor();
663
685
  if (!actor || (actor.role === "admin" && actor.authentication !== "api-key")) {
664
- return this.db.all("SELECT * FROM works WHERE COALESCE(is_internal, 0) = 0 ORDER BY updated_at DESC").map((row) => this.mapWork(row));
686
+ return this.mapWorks(this.db.all("SELECT * FROM works WHERE COALESCE(is_internal, 0) = 0 ORDER BY updated_at DESC"));
665
687
  }
666
- return this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
688
+ return this.mapWorks(this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
667
689
  WHERE COALESCE(work.is_internal, 0) = 0 AND (work.owner_user_id = ? OR membership.user_id = ?)
668
- ORDER BY work.updated_at DESC`, actor.userId, actor.userId).map((row) => this.mapWork(row));
690
+ ORDER BY work.updated_at DESC`, actor.userId, actor.userId));
669
691
  }
670
692
  listWorksPage(pagination) {
671
693
  const actor = currentRequestActor();
@@ -675,7 +697,7 @@ export class Store {
675
697
  : this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
676
698
  WHERE COALESCE(work.is_internal, 0) = 0 AND (work.owner_user_id = ? OR membership.user_id = ?)
677
699
  ORDER BY work.updated_at DESC${page.sql}`, actor.userId, actor.userId, ...page.params);
678
- return paginated(rows.map((row) => this.mapWork(row)), pagination);
700
+ return paginated(this.mapWorks(rows), pagination);
679
701
  }
680
702
  getWork(workId) {
681
703
  const row = this.db.get("SELECT * FROM works WHERE id = ?", workId);
@@ -966,7 +988,7 @@ export class Store {
966
988
  if (!row)
967
989
  return null;
968
990
  const mimeType = requiredString(row, "mime_type");
969
- if (mimeType !== "image/jpeg" && mimeType !== "image/png" && mimeType !== "image/webp") {
991
+ if (mimeType !== "image/jpeg" && mimeType !== "image/png" && mimeType !== "image/webp" && mimeType !== "image/gif") {
970
992
  throw new AppError(500, "INVALID_COVER_MIME", "作品封面类型无效");
971
993
  }
972
994
  return {
@@ -2124,12 +2146,45 @@ export class Store {
2124
2146
  }
2125
2147
  this.notifyAnalysisTaskQueued(workId);
2126
2148
  }
2127
- mapWork(row) {
2149
+ mapWorks(rows) {
2150
+ if (rows.length === 0)
2151
+ return [];
2152
+ const actor = currentRequestActor();
2153
+ const workIds = [...new Set(rows.map((row) => requiredString(row, "id")))];
2154
+ const batch = {
2155
+ memberships: new Map(),
2156
+ counts: new Map(),
2157
+ covers: new Map()
2158
+ };
2159
+ for (let offset = 0; offset < workIds.length; offset += WORK_LIST_BATCH_SIZE) {
2160
+ const batchIds = workIds.slice(offset, offset + WORK_LIST_BATCH_SIZE);
2161
+ const placeholders = batchIds.map(() => "?").join(", ");
2162
+ if (actor) {
2163
+ const memberships = this.db.all(`SELECT work_id, role, permissions_json FROM work_memberships
2164
+ WHERE user_id = ? AND work_id IN (${placeholders})`, actor.userId, ...batchIds);
2165
+ for (const membership of memberships) {
2166
+ batch.memberships.set(requiredString(membership, "work_id"), membership);
2167
+ }
2168
+ }
2169
+ const counts = this.db.all(`SELECT work_id, COUNT(*) AS chapter_count, COALESCE(SUM(word_count), 0) AS word_count
2170
+ FROM chapters WHERE work_id IN (${placeholders}) AND deleted_at IS NULL GROUP BY work_id`, ...batchIds);
2171
+ for (const count of counts)
2172
+ batch.counts.set(requiredString(count, "work_id"), count);
2173
+ const covers = this.db.all(`SELECT work_id, updated_at FROM work_covers WHERE work_id IN (${placeholders})`, ...batchIds);
2174
+ for (const cover of covers)
2175
+ batch.covers.set(requiredString(cover, "work_id"), cover);
2176
+ }
2177
+ return rows.map((row) => this.mapWork(row, batch));
2178
+ }
2179
+ mapWork(row, batch) {
2128
2180
  const actor = currentRequestActor();
2181
+ const workId = requiredString(row, "id");
2129
2182
  const ownerUserId = optionalString(row, "owner_user_id");
2130
- const membership = actor
2131
- ? this.db.get("SELECT role, permissions_json FROM work_memberships WHERE work_id = ? AND user_id = ?", requiredString(row, "id"), actor.userId)
2132
- : undefined;
2183
+ const membership = batch
2184
+ ? batch.memberships.get(workId)
2185
+ : actor
2186
+ ? this.db.get("SELECT role, permissions_json FROM work_memberships WHERE work_id = ? AND user_id = ?", workId, actor.userId)
2187
+ : undefined;
2133
2188
  const membershipRole = String(membership?.role ?? "");
2134
2189
  const ownerAccess = ownerUserId === actor?.userId;
2135
2190
  const adminAccess = actor?.role === "admin" && actor.authentication !== "api-key";
@@ -2143,19 +2198,23 @@ export class Store {
2143
2198
  : adminAccess
2144
2199
  ? "admin"
2145
2200
  : membershipRole ? classifyWorkModulePermissions(modulePermissions) : null;
2146
- const count = this.db.get("SELECT COUNT(*) AS chapter_count, COALESCE(SUM(word_count), 0) AS word_count FROM chapters WHERE work_id = ? AND deleted_at IS NULL", requiredString(row, "id"));
2147
- const cover = this.db.get("SELECT updated_at FROM work_covers WHERE work_id = ?", requiredString(row, "id"));
2201
+ const count = batch
2202
+ ? batch.counts.get(workId)
2203
+ : this.db.get("SELECT COUNT(*) AS chapter_count, COALESCE(SUM(word_count), 0) AS word_count FROM chapters WHERE work_id = ? AND deleted_at IS NULL", workId);
2204
+ const cover = batch
2205
+ ? batch.covers.get(workId)
2206
+ : this.db.get("SELECT updated_at FROM work_covers WHERE work_id = ?", workId);
2148
2207
  return {
2149
- id: requiredString(row, "id"),
2208
+ id: workId,
2150
2209
  title: requiredString(row, "title"),
2151
2210
  author: requiredString(row, "author"),
2152
2211
  description: requiredString(row, "description"),
2153
2212
  language: requiredString(row, "language"),
2154
2213
  coverUrl: cover
2155
- ? `/api/works/${encodeURIComponent(requiredString(row, "id"))}/cover?v=${encodeURIComponent(requiredString(cover, "updated_at"))}`
2214
+ ? `/api/works/${encodeURIComponent(workId)}/cover?v=${encodeURIComponent(requiredString(cover, "updated_at"))}`
2156
2215
  : optionalString(row, "cover_url"),
2157
2216
  tags: json(requiredString(row, "tags_json"), []),
2158
- versionNo: numberValue(row, "version_no") || this.currentEntityVersionNo("work", requiredString(row, "id")),
2217
+ versionNo: numberValue(row, "version_no") || this.currentEntityVersionNo("work", workId),
2159
2218
  ownerUserId,
2160
2219
  accessRole,
2161
2220
  modulePermissions,
@@ -5202,7 +5261,7 @@ export class Store {
5202
5261
  }
5203
5262
  updateTask(taskId, input) {
5204
5263
  const current = this.getTask(taskId);
5205
- const terminal = ["completed", "partial", "review", "expired", "cancelled"];
5264
+ const terminal = ["completed", "partial", "failed", "review", "expired", "cancelled"];
5206
5265
  if (terminal.includes(String(current.status)) && input.status !== current.status) {
5207
5266
  throw new AppError(409, "INVALID_TASK_TRANSITION", "终态任务不能再变更状态");
5208
5267
  }