@musnows/scriverse 0.9.0 → 0.9.2
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 +24 -2
- package/dist/app.js.map +1 -1
- package/dist/database.js +159 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +281 -21
- package/dist/public/index.html +6 -2
- package/dist/public/styles.css +43 -5
- package/dist/store.js +238 -60
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -10
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/database.js
CHANGED
|
@@ -6,9 +6,10 @@ import { documentParagraphLineRanges } from "./hybrid-search.js";
|
|
|
6
6
|
import { logger, sanitizeError } from "./logger.js";
|
|
7
7
|
import { documentShortSearchTerms, normalizeDocumentSearchText, splitDocumentParagraphs } from "./utils.js";
|
|
8
8
|
export const PLATFORM_AI_WORK_ID = "__scriverse_platform_ai__";
|
|
9
|
-
|
|
9
|
+
export const SYSTEM_USER_ID = "__scriverse_system_user__";
|
|
10
|
+
// 版本 81 用于列表查询索引;版本 82 由 Store 写入实体版本基线标记;版本 83 创建协作状态表;版本 84 创建备份加密表;版本 85 持久化协作变更动作;版本 86 扩展直接图片上传格式;版本 87 增加作品与分卷回收站;版本 88 持久化 AI 对话分支幂等键;版本 89 持久化 AI 对话流请求锁与幂等状态;版本 90 持久化 AI 连通性测试冷却状态;版本 91 建立章节段落行号索引;版本 92 增加 AI 对话收藏状态;版本 93 优化伏笔计划回收章节查询;版本 94 增加模型思考强度;版本 95 增加供应商最大输出参数选择;版本 96 扩展模型思考强度档位;版本 97 增加人物性别字段;版本 98 增加正文稳定等待配置;版本 99 持久化关系扮演中的用户角色;版本 100 增加平台 AI 流事件空闲超时配置;版本 101 将平台 AI 流事件空闲超时上限提升至 600 秒;版本 102 创建角色头像元数据表;版本 103 回填历史 AI 对话归属并建立用户列表索引;版本 104 扩大供应商协议约束以支持 OpenAI Responses;版本 105 增加独立分卷剧情顺序;版本 106 增加供应商思考类型配置;版本 107 增加 AI Cache Write 输入 Token 统计;版本 108 增加作品 AI 每月 Token 额度;版本 109 增加供应商日、月 Token 额度;版本 110 将日、月 Token 额度下限调整为大于 0;版本 111 扩展模型思考强度为 auto;版本 112 为 CLI API Key 增加可复制的加密密文;版本 113 增加角色收藏状态与列表索引;版本 114 增加组织、设定档案与想法收藏状态及列表索引;版本 115 增加 AI 对话会话级场景钉;版本 116 增加可持久化且可撤销的 Desktop Bearer 会话;版本 117 增加作品离线授权、同步变更游标与幂等变更结果;版本 118 增加供应商分析请求超时配置;版本 119 记录分析任务是否由 API Key 创建;版本 120 将书籍资料收藏按用户隔离并增加书籍级共享置顶;版本 121 强制作品 Owner 非空并建立用户外键约束。
|
|
10
11
|
export const ENTITY_VERSION_BASELINE_MIGRATION_VERSION = 82;
|
|
11
|
-
export const DATABASE_SCHEMA_VERSION =
|
|
12
|
+
export const DATABASE_SCHEMA_VERSION = 121;
|
|
12
13
|
export const SQLITE_IOERR_SHMSIZE = 4874;
|
|
13
14
|
export function isSqliteDiskIoError(error) {
|
|
14
15
|
if (!(error instanceof Error))
|
|
@@ -4345,6 +4346,162 @@ export class Database {
|
|
|
4345
4346
|
if (foreignKeys.length > 0)
|
|
4346
4347
|
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4347
4348
|
}
|
|
4349
|
+
const workEntityFavoritesTablePresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'work_entity_favorites'") !== undefined;
|
|
4350
|
+
const workEntityPinsTablePresent = this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'work_entity_pins'") !== undefined;
|
|
4351
|
+
const workEntityPreferenceIndexesPresent = [
|
|
4352
|
+
"idx_work_entity_favorites_user",
|
|
4353
|
+
"idx_work_entity_favorites_entity",
|
|
4354
|
+
"idx_work_entity_pins_entity"
|
|
4355
|
+
].every((index) => this.get("SELECT 1 AS present FROM sqlite_master WHERE type = 'index' AND name = ?", index) !== undefined);
|
|
4356
|
+
if (!applied.has(120) || !workEntityFavoritesTablePresent || !workEntityPinsTablePresent || !workEntityPreferenceIndexesPresent) {
|
|
4357
|
+
this.transaction(() => {
|
|
4358
|
+
this.run(`CREATE TABLE IF NOT EXISTS work_entity_favorites (
|
|
4359
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4360
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('character', 'draft', 'setting', 'organization')),
|
|
4361
|
+
entity_id TEXT NOT NULL,
|
|
4362
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
4363
|
+
is_favorite INTEGER NOT NULL DEFAULT 0 CHECK(is_favorite IN (0, 1)),
|
|
4364
|
+
created_at TEXT NOT NULL,
|
|
4365
|
+
updated_at TEXT NOT NULL,
|
|
4366
|
+
PRIMARY KEY(work_id, entity_type, entity_id, user_id)
|
|
4367
|
+
)`);
|
|
4368
|
+
this.run(`CREATE TABLE IF NOT EXISTS work_entity_pins (
|
|
4369
|
+
work_id TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
4370
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('character', 'draft', 'setting', 'organization')),
|
|
4371
|
+
entity_id TEXT NOT NULL,
|
|
4372
|
+
is_pinned INTEGER NOT NULL DEFAULT 0 CHECK(is_pinned IN (0, 1)),
|
|
4373
|
+
pinned_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4374
|
+
created_at TEXT NOT NULL,
|
|
4375
|
+
updated_at TEXT NOT NULL,
|
|
4376
|
+
PRIMARY KEY(work_id, entity_type, entity_id)
|
|
4377
|
+
)`);
|
|
4378
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_work_entity_favorites_user ON work_entity_favorites(work_id, user_id, entity_type, is_favorite, entity_id)");
|
|
4379
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_work_entity_favorites_entity ON work_entity_favorites(work_id, entity_type, entity_id, is_favorite)");
|
|
4380
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_work_entity_pins_entity ON work_entity_pins(work_id, entity_type, is_pinned, entity_id)");
|
|
4381
|
+
const timestamp = new Date().toISOString();
|
|
4382
|
+
this.run(`
|
|
4383
|
+
INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
4384
|
+
SELECT character.work_id, 'character', character.id, work.owner_user_id, 1,
|
|
4385
|
+
COALESCE(character.created_at, ?), COALESCE(character.updated_at, ?)
|
|
4386
|
+
FROM characters character
|
|
4387
|
+
JOIN works work ON work.id = character.work_id
|
|
4388
|
+
WHERE character.is_favorite = 1 AND work.owner_user_id IS NOT NULL
|
|
4389
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id) DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, timestamp, timestamp);
|
|
4390
|
+
this.run(`
|
|
4391
|
+
INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
4392
|
+
SELECT draft.work_id, 'draft', draft.id, work.owner_user_id, 1,
|
|
4393
|
+
COALESCE(draft.created_at, ?), COALESCE(draft.updated_at, ?)
|
|
4394
|
+
FROM drafts draft
|
|
4395
|
+
JOIN works work ON work.id = draft.work_id
|
|
4396
|
+
WHERE draft.is_favorite = 1 AND work.owner_user_id IS NOT NULL
|
|
4397
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id) DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, timestamp, timestamp);
|
|
4398
|
+
this.run(`
|
|
4399
|
+
INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
4400
|
+
SELECT setting.work_id, 'setting', setting.id, work.owner_user_id, 1,
|
|
4401
|
+
COALESCE(setting.created_at, ?), COALESCE(setting.updated_at, ?)
|
|
4402
|
+
FROM settings setting
|
|
4403
|
+
JOIN works work ON work.id = setting.work_id
|
|
4404
|
+
WHERE setting.is_favorite = 1 AND work.owner_user_id IS NOT NULL
|
|
4405
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id) DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, timestamp, timestamp);
|
|
4406
|
+
this.run(`
|
|
4407
|
+
INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
4408
|
+
SELECT organization.work_id, 'organization', organization.id, work.owner_user_id, 1,
|
|
4409
|
+
COALESCE(organization.created_at, ?), COALESCE(organization.updated_at, ?)
|
|
4410
|
+
FROM organizations organization
|
|
4411
|
+
JOIN works work ON work.id = organization.work_id
|
|
4412
|
+
WHERE organization.is_favorite = 1 AND work.owner_user_id IS NOT NULL
|
|
4413
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id) DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, timestamp, timestamp);
|
|
4414
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (120, ?)", timestamp);
|
|
4415
|
+
});
|
|
4416
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4417
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4418
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4419
|
+
}
|
|
4420
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4421
|
+
if (foreignKeys.length > 0)
|
|
4422
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4423
|
+
}
|
|
4424
|
+
const workOwnerColumn = this.all("PRAGMA table_info(works)")
|
|
4425
|
+
.find((column) => column.name === "owner_user_id");
|
|
4426
|
+
const workOwnerForeignKeyPresent = this.all("PRAGMA foreign_key_list(works)")
|
|
4427
|
+
.some((foreignKey) => foreignKey.table === "users" && foreignKey.from === "owner_user_id" && foreignKey.on_delete === "RESTRICT");
|
|
4428
|
+
const systemUserPresent = this.get("SELECT 1 AS present FROM users WHERE id = ?", SYSTEM_USER_ID) !== undefined;
|
|
4429
|
+
if (!applied.has(121) || workOwnerColumn?.notnull !== 1 || !workOwnerForeignKeyPresent || !systemUserPresent) {
|
|
4430
|
+
this.raw.exec("PRAGMA foreign_keys = OFF");
|
|
4431
|
+
try {
|
|
4432
|
+
this.transaction(() => {
|
|
4433
|
+
const timestamp = new Date().toISOString();
|
|
4434
|
+
this.run(`INSERT INTO users (
|
|
4435
|
+
id, username, normalized_username, display_name, password_hash, password_salt,
|
|
4436
|
+
role, status, created_at, updated_at
|
|
4437
|
+
) VALUES (?, '__scriverse_system__', '__scriverse_system__', 'Scriverse System', 'disabled', 'disabled', 'user', 'disabled', ?, ?)
|
|
4438
|
+
ON CONFLICT(id) DO UPDATE SET status = 'disabled', updated_at = excluded.updated_at`, SYSTEM_USER_ID, timestamp, timestamp);
|
|
4439
|
+
this.run("UPDATE works SET owner_user_id = ? WHERE id = ?", SYSTEM_USER_ID, PLATFORM_AI_WORK_ID);
|
|
4440
|
+
this.run(`UPDATE works SET owner_user_id = COALESCE(
|
|
4441
|
+
(
|
|
4442
|
+
SELECT membership.user_id
|
|
4443
|
+
FROM work_memberships membership
|
|
4444
|
+
JOIN users member ON member.id = membership.user_id
|
|
4445
|
+
WHERE membership.work_id = works.id AND member.id <> ?
|
|
4446
|
+
ORDER BY CASE membership.role WHEN 'owner' THEN 0 ELSE 1 END, membership.created_at, membership.user_id
|
|
4447
|
+
LIMIT 1
|
|
4448
|
+
),
|
|
4449
|
+
(
|
|
4450
|
+
SELECT candidate.id
|
|
4451
|
+
FROM users candidate
|
|
4452
|
+
WHERE candidate.id <> ? AND candidate.status = 'active'
|
|
4453
|
+
ORDER BY CASE candidate.role WHEN 'admin' THEN 0 ELSE 1 END, candidate.created_at, candidate.id
|
|
4454
|
+
LIMIT 1
|
|
4455
|
+
),
|
|
4456
|
+
?
|
|
4457
|
+
)
|
|
4458
|
+
WHERE id <> ? AND (
|
|
4459
|
+
owner_user_id IS NULL
|
|
4460
|
+
OR NOT EXISTS (SELECT 1 FROM users owner WHERE owner.id = works.owner_user_id)
|
|
4461
|
+
)`, SYSTEM_USER_ID, SYSTEM_USER_ID, SYSTEM_USER_ID, PLATFORM_AI_WORK_ID);
|
|
4462
|
+
this.run("DROP TABLE IF EXISTS works_v121");
|
|
4463
|
+
this.run(`CREATE TABLE works_v121 (
|
|
4464
|
+
id TEXT PRIMARY KEY,
|
|
4465
|
+
title TEXT NOT NULL,
|
|
4466
|
+
author TEXT NOT NULL DEFAULT '',
|
|
4467
|
+
description TEXT NOT NULL DEFAULT '',
|
|
4468
|
+
language TEXT NOT NULL DEFAULT 'zh-CN',
|
|
4469
|
+
cover_url TEXT,
|
|
4470
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
4471
|
+
is_internal INTEGER NOT NULL DEFAULT 0,
|
|
4472
|
+
offline_access_enabled INTEGER NOT NULL DEFAULT 0 CHECK(offline_access_enabled IN (0, 1)),
|
|
4473
|
+
version_no INTEGER NOT NULL DEFAULT 1,
|
|
4474
|
+
deleted_at TEXT,
|
|
4475
|
+
created_at TEXT NOT NULL,
|
|
4476
|
+
updated_at TEXT NOT NULL,
|
|
4477
|
+
owner_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT
|
|
4478
|
+
)`);
|
|
4479
|
+
this.run(`INSERT INTO works_v121 (
|
|
4480
|
+
id, title, author, description, language, cover_url, tags_json, is_internal,
|
|
4481
|
+
offline_access_enabled, version_no, deleted_at, created_at, updated_at, owner_user_id
|
|
4482
|
+
)
|
|
4483
|
+
SELECT
|
|
4484
|
+
id, title, author, description, language, cover_url, tags_json, is_internal,
|
|
4485
|
+
offline_access_enabled, version_no, deleted_at, created_at, updated_at, owner_user_id
|
|
4486
|
+
FROM works`);
|
|
4487
|
+
this.run("DROP TABLE works");
|
|
4488
|
+
this.run("ALTER TABLE works_v121 RENAME TO works");
|
|
4489
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_works_owner ON works(owner_user_id, updated_at DESC)");
|
|
4490
|
+
this.run("CREATE INDEX IF NOT EXISTS idx_works_recycle_bin ON works(deleted_at, owner_user_id)");
|
|
4491
|
+
this.run("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (121, ?)", timestamp);
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
finally {
|
|
4495
|
+
this.raw.exec("PRAGMA foreign_keys = ON");
|
|
4496
|
+
}
|
|
4497
|
+
const integrity = this.all("PRAGMA integrity_check");
|
|
4498
|
+
if (integrity.some((row) => row.integrity_check !== "ok")) {
|
|
4499
|
+
throw new Error(`数据库完整性检查失败:${integrity.map((row) => row.integrity_check).join(";")}`);
|
|
4500
|
+
}
|
|
4501
|
+
const foreignKeys = this.all("PRAGMA foreign_key_check");
|
|
4502
|
+
if (foreignKeys.length > 0)
|
|
4503
|
+
throw new Error(`数据库外键检查失败:发现 ${foreignKeys.length} 条异常记录`);
|
|
4504
|
+
}
|
|
4348
4505
|
}
|
|
4349
4506
|
normalizeCharacterName(value) {
|
|
4350
4507
|
return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");
|