@musnows/scriverse 0.7.2 → 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 +184 -39
- 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 +162 -3
- 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 +1017 -291
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/index.html +43 -21
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- 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 +37 -7
- 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 +75 -17
- 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/database.js
CHANGED
|
@@ -4,7 +4,9 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
import { logger, sanitizeError } from "./logger.js";
|
|
5
5
|
import { documentShortSearchTerms, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
|
|
6
6
|
export const PLATFORM_AI_WORK_ID = "__scriverse_platform_ai__";
|
|
7
|
-
|
|
7
|
+
// 版本 81 用于列表查询索引;版本 82 由 Store 写入实体版本基线标记;版本 83 创建协作状态表;版本 84 创建备份加密表;版本 85 持久化协作变更动作;版本 86 扩展直接图片上传格式。
|
|
8
|
+
export const ENTITY_VERSION_BASELINE_MIGRATION_VERSION = 82;
|
|
9
|
+
export const DATABASE_SCHEMA_VERSION = 86;
|
|
8
10
|
export const SQLITE_IOERR_SHMSIZE = 4874;
|
|
9
11
|
export function isSqliteDiskIoError(error) {
|
|
10
12
|
if (!(error instanceof Error))
|
|
@@ -631,7 +633,7 @@ export class Database {
|
|
|
631
633
|
|
|
632
634
|
CREATE TABLE IF NOT EXISTS work_covers (
|
|
633
635
|
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
634
|
-
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp')),
|
|
636
|
+
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp', 'image/gif')),
|
|
635
637
|
content BLOB NOT NULL,
|
|
636
638
|
byte_length INTEGER NOT NULL,
|
|
637
639
|
sha256 TEXT NOT NULL,
|
|
@@ -723,6 +725,8 @@ export class Database {
|
|
|
723
725
|
CREATE INDEX IF NOT EXISTS idx_volumes_work ON volumes(work_id, sort_order);
|
|
724
726
|
CREATE INDEX IF NOT EXISTS idx_chapters_work ON chapters(work_id, volume_id, sort_order);
|
|
725
727
|
CREATE INDEX IF NOT EXISTS idx_versions_chapter ON chapter_versions(chapter_id, version_no DESC);
|
|
728
|
+
CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC);
|
|
729
|
+
CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC);
|
|
726
730
|
CREATE INDEX IF NOT EXISTS idx_settings_work ON settings(work_id, category);
|
|
727
731
|
CREATE INDEX IF NOT EXISTS idx_drafts_work ON drafts(work_id, draft_type, updated_at DESC);
|
|
728
732
|
CREATE INDEX IF NOT EXISTS idx_characters_work ON characters(work_id, name);
|
|
@@ -733,8 +737,10 @@ export class Database {
|
|
|
733
737
|
CREATE INDEX IF NOT EXISTS idx_character_merges_work ON character_merges(work_id, created_at DESC);
|
|
734
738
|
CREATE INDEX IF NOT EXISTS idx_tasks_work ON analysis_tasks(work_id, status);
|
|
735
739
|
CREATE INDEX IF NOT EXISTS idx_calls_work ON ai_calls(work_id, created_at DESC);
|
|
740
|
+
CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC);
|
|
736
741
|
CREATE INDEX IF NOT EXISTS idx_ai_conversations_work ON ai_conversations(work_id, updated_at DESC);
|
|
737
742
|
CREATE INDEX IF NOT EXISTS idx_ai_conversation_messages ON ai_conversation_messages(conversation_id, created_at);
|
|
743
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_work_created ON audit_logs(work_id, created_at DESC);
|
|
738
744
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_character_names_primary ON character_names(character_id) WHERE kind = 'primary';
|
|
739
745
|
CREATE INDEX IF NOT EXISTS idx_character_names_character ON character_names(character_id, sort_order);
|
|
740
746
|
CREATE INDEX IF NOT EXISTS idx_character_versions_character ON character_versions(character_id, version_no DESC);
|
|
@@ -1269,7 +1275,7 @@ export class Database {
|
|
|
1269
1275
|
}
|
|
1270
1276
|
this.run(`CREATE TABLE IF NOT EXISTS user_avatars (
|
|
1271
1277
|
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
1272
|
-
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp')),
|
|
1278
|
+
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif')),
|
|
1273
1279
|
content BLOB NOT NULL,
|
|
1274
1280
|
byte_length INTEGER NOT NULL,
|
|
1275
1281
|
sha256 TEXT NOT NULL,
|
|
@@ -3175,6 +3181,159 @@ export class Database {
|
|
|
3175
3181
|
if (foreignKeys.length > 0)
|
|
3176
3182
|
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3177
3183
|
}
|
|
3184
|
+
if (!applied.has(80)) {
|
|
3185
|
+
this.transaction(() => {
|
|
3186
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_audit_logs_work_created ON audit_logs(work_id, created_at DESC)");
|
|
3187
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (80, ?)", new Date().toISOString());
|
|
3188
|
+
});
|
|
3189
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3190
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3191
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3192
|
+
}
|
|
3193
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3194
|
+
if (foreignKeys.length > 0)
|
|
3195
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3196
|
+
}
|
|
3197
|
+
if (!applied.has(81)) {
|
|
3198
|
+
this.transaction(() => {
|
|
3199
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC)");
|
|
3200
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC)");
|
|
3201
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC)");
|
|
3202
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3203
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3204
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3205
|
+
}
|
|
3206
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3207
|
+
if (foreignKeys.length > 0)
|
|
3208
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3209
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (81, ?)", new Date().toISOString());
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
if (!applied.has(83)) {
|
|
3213
|
+
this.transaction(() => {
|
|
3214
|
+
this.run(`CREATE TABLE IF NOT EXISTS presence_entries (
|
|
3215
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
3216
|
+
client_id TEXT NOT NULL,
|
|
3217
|
+
user_id TEXT NOT NULL,
|
|
3218
|
+
username TEXT NOT NULL,
|
|
3219
|
+
display_name TEXT NOT NULL,
|
|
3220
|
+
avatar_url TEXT,
|
|
3221
|
+
page_kind TEXT NOT NULL,
|
|
3222
|
+
page_module TEXT,
|
|
3223
|
+
page_resource_id TEXT,
|
|
3224
|
+
last_seen_at TEXT NOT NULL,
|
|
3225
|
+
PRIMARY KEY(work_id, client_id)
|
|
3226
|
+
)`);
|
|
3227
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_presence_entries_work ON presence_entries(work_id, last_seen_at)");
|
|
3228
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_presence_entries_last_seen ON presence_entries(last_seen_at)");
|
|
3229
|
+
this.run(`CREATE TABLE IF NOT EXISTS presence_changes (
|
|
3230
|
+
id TEXT PRIMARY KEY,
|
|
3231
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
3232
|
+
page_key TEXT NOT NULL,
|
|
3233
|
+
label TEXT NOT NULL,
|
|
3234
|
+
actor_user_id TEXT NOT NULL,
|
|
3235
|
+
actor_display_name TEXT NOT NULL,
|
|
3236
|
+
saved_at TEXT NOT NULL,
|
|
3237
|
+
recipient_client_ids_json TEXT NOT NULL DEFAULT '[]'
|
|
3238
|
+
CHECK(json_valid(recipient_client_ids_json) AND json_type(recipient_client_ids_json) = 'array')
|
|
3239
|
+
)`);
|
|
3240
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_presence_changes_work ON presence_changes(work_id, page_key, saved_at DESC)");
|
|
3241
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_presence_changes_saved_at ON presence_changes(saved_at)");
|
|
3242
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3243
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3244
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3245
|
+
}
|
|
3246
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3247
|
+
if (foreignKeys.length > 0)
|
|
3248
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3249
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (83, ?)", new Date().toISOString());
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
if (!applied.has(84)) {
|
|
3253
|
+
this.transaction(() => {
|
|
3254
|
+
// 兼容曾使用更早迁移号创建备份加密表的开发版本,并确保列表索引完整。
|
|
3255
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_ai_suggestions_work ON ai_suggestions(work_id, status, created_at DESC)");
|
|
3256
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_file_versions_work ON file_versions(work_id, created_at DESC, id DESC)");
|
|
3257
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_chapter_insights_chapter ON chapter_insights(chapter_id, chapter_version DESC, created_at DESC)");
|
|
3258
|
+
this.run(`CREATE TABLE IF NOT EXISTS s3_backup_encryption (
|
|
3259
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
3260
|
+
enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0, 1)),
|
|
3261
|
+
kek_encrypted TEXT,
|
|
3262
|
+
kek_iv TEXT,
|
|
3263
|
+
kek_tag TEXT,
|
|
3264
|
+
created_at TEXT NOT NULL,
|
|
3265
|
+
updated_at TEXT NOT NULL,
|
|
3266
|
+
CHECK(
|
|
3267
|
+
(kek_encrypted IS NULL AND kek_iv IS NULL AND kek_tag IS NULL)
|
|
3268
|
+
OR (kek_encrypted IS NOT NULL AND kek_iv IS NOT NULL AND kek_tag IS NOT NULL)
|
|
3269
|
+
),
|
|
3270
|
+
CHECK(enabled = 0 OR kek_encrypted IS NOT NULL)
|
|
3271
|
+
)`);
|
|
3272
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3273
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3274
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3275
|
+
}
|
|
3276
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3277
|
+
if (foreignKeys.length > 0)
|
|
3278
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3279
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (84, ?)", new Date().toISOString());
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
if (!applied.has(85)) {
|
|
3283
|
+
this.transaction(() => {
|
|
3284
|
+
const columns = new Set(this.all("PRAGMA table_info(presence_changes)").map((row) => String(row.name)));
|
|
3285
|
+
if (!columns.has("action")) {
|
|
3286
|
+
this.run("ALTER TABLE presence_changes ADD COLUMN action TEXT NOT NULL DEFAULT 'save' CHECK(action IN ('save', 'delete'))");
|
|
3287
|
+
}
|
|
3288
|
+
if (!columns.has("page_deleted")) {
|
|
3289
|
+
this.run("ALTER TABLE presence_changes ADD COLUMN page_deleted INTEGER NOT NULL DEFAULT 0 CHECK(page_deleted IN (0, 1))");
|
|
3290
|
+
}
|
|
3291
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3292
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3293
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3294
|
+
}
|
|
3295
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3296
|
+
if (foreignKeys.length > 0)
|
|
3297
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3298
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (85, ?)", new Date().toISOString());
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
if (!applied.has(86)) {
|
|
3302
|
+
this.transaction(() => {
|
|
3303
|
+
this.run(`CREATE TABLE work_covers_v86 (
|
|
3304
|
+
work_id TEXT PRIMARY KEY REFERENCES works(id) ON DELETE CASCADE,
|
|
3305
|
+
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/jpeg', 'image/png', 'image/webp', 'image/gif')),
|
|
3306
|
+
content BLOB NOT NULL,
|
|
3307
|
+
byte_length INTEGER NOT NULL,
|
|
3308
|
+
sha256 TEXT NOT NULL,
|
|
3309
|
+
updated_at TEXT NOT NULL
|
|
3310
|
+
)`);
|
|
3311
|
+
this.run("INSERT INTO work_covers_v86 SELECT work_id, mime_type, content, byte_length, sha256, updated_at FROM work_covers");
|
|
3312
|
+
this.run("DROP TABLE work_covers");
|
|
3313
|
+
this.run("ALTER TABLE work_covers_v86 RENAME TO work_covers");
|
|
3314
|
+
this.run(`CREATE TABLE user_avatars_v86 (
|
|
3315
|
+
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
3316
|
+
mime_type TEXT NOT NULL CHECK(mime_type IN ('image/png', 'image/jpeg', 'image/webp', 'image/gif')),
|
|
3317
|
+
content BLOB NOT NULL,
|
|
3318
|
+
byte_length INTEGER NOT NULL,
|
|
3319
|
+
sha256 TEXT NOT NULL,
|
|
3320
|
+
width INTEGER NOT NULL,
|
|
3321
|
+
height INTEGER NOT NULL,
|
|
3322
|
+
updated_at TEXT NOT NULL
|
|
3323
|
+
)`);
|
|
3324
|
+
this.run("INSERT INTO user_avatars_v86 SELECT user_id, mime_type, content, byte_length, sha256, width, height, updated_at FROM user_avatars");
|
|
3325
|
+
this.run("DROP TABLE user_avatars");
|
|
3326
|
+
this.run("ALTER TABLE user_avatars_v86 RENAME TO user_avatars");
|
|
3327
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
3328
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
3329
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
3330
|
+
}
|
|
3331
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
3332
|
+
if (foreignKeys.length > 0)
|
|
3333
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
3334
|
+
this.run("INSERT INTO schema_migrations (version, applied_at) VALUES (86, ?)", new Date().toISOString());
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3178
3337
|
}
|
|
3179
3338
|
normalizeCharacterName(value) {
|
|
3180
3339
|
return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");
|