@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.
- package/README.md +5 -0
- package/dist/ai.js +248 -250
- package/dist/ai.js.map +1 -1
- package/dist/app.js +194 -40
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +28 -12
- package/dist/attachment-storage.js.map +1 -1
- package/dist/backup-encryption.js +129 -0
- package/dist/backup-encryption.js.map +1 -0
- package/dist/collaboration-presence.js +200 -18
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +215 -4
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +1 -1
- package/dist/docx-export.js.map +1 -1
- package/dist/domain.js +18 -0
- package/dist/domain.js.map +1 -1
- package/dist/image-metadata.js +17 -2
- package/dist/image-metadata.js.map +1 -1
- package/dist/presence-store.js +160 -0
- package/dist/presence-store.js.map +1 -0
- package/dist/public/ai-context-meter.js +27 -0
- package/dist/public/ai-mentions.js +14 -0
- package/dist/public/app.js +1018 -292
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/index.html +44 -22
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- package/dist/public/relationship-graph.js +24 -4
- package/dist/public/s3-backup-ui.d.ts +10 -0
- package/dist/public/s3-backup-ui.js +29 -0
- package/dist/public/setting-filters.d.ts +16 -0
- package/dist/public/setting-filters.js +19 -0
- package/dist/public/styles.css +39 -9
- package/dist/s3-backup.js +197 -12
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +82 -20
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +36 -18
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +77 -18
- package/dist/store.js.map +1 -1
- package/dist/upload-limits.js +35 -0
- package/dist/upload-limits.js.map +1 -0
- package/dist/user-auth.js +11 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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.
|
|
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")
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
2131
|
-
?
|
|
2132
|
-
:
|
|
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 =
|
|
2147
|
-
|
|
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:
|
|
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(
|
|
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",
|
|
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
|
}
|