@musnows/scriverse 0.7.8 → 0.7.9
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/ai.js +202 -56
- package/dist/ai.js.map +1 -1
- package/dist/app.js +2 -2
- package/dist/app.js.map +1 -1
- package/dist/database.js +36 -2
- package/dist/database.js.map +1 -1
- package/dist/hybrid-search.js +36 -5
- package/dist/hybrid-search.js.map +1 -1
- package/dist/store.js +114 -27
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +5 -4
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID } from "
|
|
|
4
4
|
import { exportWorkDocx } from "./docx-export.js";
|
|
5
5
|
import { createEpubArchive } from "./epub-export.js";
|
|
6
6
|
import { AppError, notFound } from "./errors.js";
|
|
7
|
-
import { normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
7
|
+
import { documentParagraphLineRanges, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
8
8
|
import { accountReference, logger } from "./logger.js";
|
|
9
9
|
import { paginated, paginationSql } from "./pagination.js";
|
|
10
10
|
import { currentRequestActor } from "./request-context.js";
|
|
@@ -1240,6 +1240,51 @@ export class Store {
|
|
|
1240
1240
|
}));
|
|
1241
1241
|
return { ...work, volumes, directoryPage: pageResult };
|
|
1242
1242
|
}
|
|
1243
|
+
getStoryIndexChapterPage(workId, offset, limit) {
|
|
1244
|
+
const work = this.getWork(workId);
|
|
1245
|
+
const permissions = work.modulePermissions;
|
|
1246
|
+
if (permissions.prose === "none")
|
|
1247
|
+
return { totalChapters: 0, chapters: [] };
|
|
1248
|
+
const countRow = this.db.get(`SELECT COUNT(*) AS count FROM chapters chapter
|
|
1249
|
+
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1250
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL`, workId);
|
|
1251
|
+
const chapterRows = this.db.all(`SELECT chapter.id, chapter.title, chapter.version_no, volume.title AS volume_title
|
|
1252
|
+
FROM chapters chapter
|
|
1253
|
+
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1254
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL
|
|
1255
|
+
ORDER BY volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at
|
|
1256
|
+
LIMIT ? OFFSET ?`, workId, limit, offset);
|
|
1257
|
+
const chapterIds = chapterRows.map((row) => requiredString(row, "id"));
|
|
1258
|
+
const summaries = new Map();
|
|
1259
|
+
if (chapterIds.length > 0) {
|
|
1260
|
+
const placeholders = chapterIds.map(() => "?").join(", ");
|
|
1261
|
+
const insightRows = this.db.all(`SELECT insight.chapter_id, insight.summary
|
|
1262
|
+
FROM chapter_insights insight
|
|
1263
|
+
JOIN chapters chapter ON chapter.id = insight.chapter_id AND chapter.version_no = insight.chapter_version
|
|
1264
|
+
WHERE chapter.work_id = ? AND insight.chapter_id IN (${placeholders})
|
|
1265
|
+
AND NOT EXISTS (
|
|
1266
|
+
SELECT 1 FROM chapter_insights newer
|
|
1267
|
+
WHERE newer.chapter_id = insight.chapter_id
|
|
1268
|
+
AND newer.chapter_version = insight.chapter_version
|
|
1269
|
+
AND (newer.created_at > insight.created_at OR (newer.created_at = insight.created_at AND newer.id > insight.id))
|
|
1270
|
+
)`, workId, ...chapterIds);
|
|
1271
|
+
for (const row of insightRows)
|
|
1272
|
+
summaries.set(requiredString(row, "chapter_id"), requiredString(row, "summary"));
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
totalChapters: numberValue(countRow ?? {}, "count"),
|
|
1276
|
+
chapters: chapterRows.map((row) => {
|
|
1277
|
+
const chapterId = requiredString(row, "id");
|
|
1278
|
+
return {
|
|
1279
|
+
id: chapterId,
|
|
1280
|
+
volumeTitle: requiredString(row, "volume_title"),
|
|
1281
|
+
title: requiredString(row, "title"),
|
|
1282
|
+
versionNo: numberValue(row, "version_no"),
|
|
1283
|
+
summary: summaries.get(chapterId) ?? ""
|
|
1284
|
+
};
|
|
1285
|
+
})
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1243
1288
|
listFileVersions(workId) {
|
|
1244
1289
|
this.getWork(workId);
|
|
1245
1290
|
return this.db
|
|
@@ -1989,6 +2034,7 @@ export class Store {
|
|
|
1989
2034
|
AND json_extract(scope_json, '$.type') = 'volume'
|
|
1990
2035
|
AND json_extract(scope_json, '$.volumeId') IN (?, ?)`, timestamp, String(lockedChapter.workId), sourceVolumeId, targetVolumeId);
|
|
1991
2036
|
this.db.run("UPDATE chapters SET version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", versionNo, timestamp, chapterId);
|
|
2037
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
1992
2038
|
this.insertChapterVersionRow({
|
|
1993
2039
|
workId: String(lockedChapter.workId),
|
|
1994
2040
|
chapterId,
|
|
@@ -2181,6 +2227,7 @@ export class Store {
|
|
|
2181
2227
|
const versionNo = Number(chapter.versionNo) + 1;
|
|
2182
2228
|
const sortOrder = orderedByVolume.get(action.volumeId)?.indexOf(chapterId) ?? 0;
|
|
2183
2229
|
this.db.run("UPDATE chapters SET version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", versionNo, timestamp, chapterId);
|
|
2230
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2184
2231
|
this.insertChapterVersionRow({
|
|
2185
2232
|
workId,
|
|
2186
2233
|
chapterId,
|
|
@@ -2344,16 +2391,27 @@ export class Store {
|
|
|
2344
2391
|
return chapterId;
|
|
2345
2392
|
}
|
|
2346
2393
|
syncChapterParagraphSearch(workId, chapterId, content) {
|
|
2394
|
+
const chapterVersion = numberValue(this.db.get("SELECT version_no FROM chapters WHERE id = ? AND work_id = ?", chapterId, workId) ?? {}, "version_no");
|
|
2395
|
+
const ranges = documentParagraphLineRanges(content);
|
|
2347
2396
|
this.db.run("DELETE FROM chapter_paragraph_search WHERE chapter_id = ?", chapterId);
|
|
2348
2397
|
for (const [paragraphOrder, paragraph] of splitDocumentParagraphs(content).entries()) {
|
|
2349
2398
|
const searchContent = normalizeDocumentSearchText(paragraph);
|
|
2350
2399
|
const inserted = this.db.run(`INSERT INTO chapter_paragraph_search (work_id, chapter_id, paragraph_order, content, search_content)
|
|
2351
2400
|
VALUES (?, ?, ?, ?, ?)`, workId, chapterId, paragraphOrder, paragraph, searchContent);
|
|
2401
|
+
const range = ranges[paragraphOrder];
|
|
2402
|
+
if (range) {
|
|
2403
|
+
this.db.run(`INSERT INTO chapter_paragraph_line_ranges (paragraph_id, chapter_version, start_line, end_line)
|
|
2404
|
+
VALUES (?, ?, ?, ?)`, inserted.lastInsertRowid, chapterVersion, range.startLine, range.endLine);
|
|
2405
|
+
}
|
|
2352
2406
|
for (const term of documentShortSearchTerms(searchContent)) {
|
|
2353
2407
|
this.db.run("INSERT INTO chapter_paragraph_short_terms (paragraph_id, term) VALUES (?, ?)", inserted.lastInsertRowid, term);
|
|
2354
2408
|
}
|
|
2355
2409
|
}
|
|
2356
2410
|
}
|
|
2411
|
+
syncChapterParagraphSearchVersion(chapterId, versionNo) {
|
|
2412
|
+
this.db.run(`UPDATE chapter_paragraph_line_ranges SET chapter_version = ?
|
|
2413
|
+
WHERE paragraph_id IN (SELECT id FROM chapter_paragraph_search WHERE chapter_id = ?)`, versionNo, chapterId);
|
|
2414
|
+
}
|
|
2357
2415
|
searchChapterParagraphs(workId, keyword, limit = 20) {
|
|
2358
2416
|
this.getWork(workId);
|
|
2359
2417
|
const normalizedKeyword = normalizeDocumentSearchText(keyword.trim());
|
|
@@ -4119,7 +4177,7 @@ export class Store {
|
|
|
4119
4177
|
searchCharacterProfileSections(workId, query, limit = 20) {
|
|
4120
4178
|
this.getWork(workId);
|
|
4121
4179
|
const normalized = normalizeDocumentSearchText(query);
|
|
4122
|
-
const columns = `SELECT section.*, character.name AS character_name
|
|
4180
|
+
const columns = `SELECT section.*, character.name AS character_name, character.is_dead AS character_is_dead
|
|
4123
4181
|
FROM character_profile_section_search search
|
|
4124
4182
|
JOIN character_profile_sections section ON section.id = search.section_id
|
|
4125
4183
|
JOIN characters character ON character.id = search.character_id`;
|
|
@@ -4131,7 +4189,11 @@ export class Store {
|
|
|
4131
4189
|
WHERE search.work_id = ? AND character.merged_into_character_id IS NULL
|
|
4132
4190
|
AND character_profile_section_search_fts MATCH ?
|
|
4133
4191
|
ORDER BY bm25(character_profile_section_search_fts), character.name, section.sort_order LIMIT ?`, workId, `"${normalized.replaceAll('"', '""')}"`, limit);
|
|
4134
|
-
return rows.map((row) => ({
|
|
4192
|
+
return rows.map((row) => ({
|
|
4193
|
+
...this.mapCharacterProfileSection(row),
|
|
4194
|
+
characterName: requiredString(row, "character_name"),
|
|
4195
|
+
isDead: booleanValue(row, "character_is_dead")
|
|
4196
|
+
}));
|
|
4135
4197
|
}
|
|
4136
4198
|
mapAttachment(row) {
|
|
4137
4199
|
const attachmentId = requiredString(row, "id");
|
|
@@ -5264,18 +5326,34 @@ export class Store {
|
|
|
5264
5326
|
throw notFound("AI 对话");
|
|
5265
5327
|
if (requiredString(conversation, "work_id") !== workId)
|
|
5266
5328
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
5267
|
-
const
|
|
5268
|
-
const
|
|
5329
|
+
const countRow = this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId);
|
|
5330
|
+
const totalMessageCount = numberValue(countRow ?? {}, "count");
|
|
5331
|
+
const compactedMessageCount = Math.min(totalMessageCount, Math.max(0, numberValue(conversation, "compacted_message_count")));
|
|
5332
|
+
const tailMessageCount = totalMessageCount - compactedMessageCount;
|
|
5333
|
+
let rows = [];
|
|
5334
|
+
if (tailMessageCount > 0 && compactedMessageCount === 0) {
|
|
5335
|
+
rows = this.db.all(`SELECT id, role, content, metadata_json FROM ai_conversation_messages
|
|
5336
|
+
WHERE conversation_id = ? ORDER BY created_at, rowid LIMIT ?`, conversationId, tailMessageCount);
|
|
5337
|
+
}
|
|
5338
|
+
else if (tailMessageCount > 0) {
|
|
5339
|
+
const boundary = this.db.get(`SELECT created_at, rowid FROM ai_conversation_messages
|
|
5340
|
+
WHERE conversation_id = ? ORDER BY created_at, rowid LIMIT 1 OFFSET ?`, conversationId, compactedMessageCount - 1);
|
|
5341
|
+
if (boundary) {
|
|
5342
|
+
rows = this.db.all(`SELECT id, role, content, metadata_json FROM ai_conversation_messages
|
|
5343
|
+
WHERE conversation_id = ?
|
|
5344
|
+
AND (created_at > ? OR (created_at = ? AND rowid > ?))
|
|
5345
|
+
ORDER BY created_at, rowid LIMIT ?`, conversationId, requiredString(boundary, "created_at"), requiredString(boundary, "created_at"), numberValue(boundary, "rowid"), tailMessageCount);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5269
5348
|
return {
|
|
5270
5349
|
workId,
|
|
5271
5350
|
roleplayCharacterId: optionalString(conversation, "roleplay_character_id"),
|
|
5272
5351
|
summary: requiredString(conversation, "compacted_summary"),
|
|
5273
5352
|
compactedMessageCount,
|
|
5274
|
-
totalMessageCount
|
|
5353
|
+
totalMessageCount,
|
|
5275
5354
|
warningPending: Boolean(optionalString(conversation, "context_warning_at")),
|
|
5276
5355
|
injectedEntities: parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES),
|
|
5277
|
-
messages: rows.
|
|
5278
|
-
.filter((message) => requiredString(message, "id") !== excludeMessageId)
|
|
5356
|
+
messages: rows.filter((message) => requiredString(message, "id") !== excludeMessageId)
|
|
5279
5357
|
.map((message) => ({
|
|
5280
5358
|
id: requiredString(message, "id"),
|
|
5281
5359
|
role: requiredString(message, "role") === "assistant" ? "assistant" : "user",
|
|
@@ -7236,26 +7314,33 @@ export class Store {
|
|
|
7236
7314
|
return [{ type: "none" }];
|
|
7237
7315
|
return [{ type: "unknown", scope }];
|
|
7238
7316
|
}
|
|
7239
|
-
search(workId, query) {
|
|
7317
|
+
search(workId, query, requestedTypes) {
|
|
7240
7318
|
this.getWork(workId);
|
|
7241
7319
|
const normalizedQuery = normalizeWorkSearchQuery(query);
|
|
7242
7320
|
if (!normalizedQuery)
|
|
7243
7321
|
return [];
|
|
7244
7322
|
const pattern = `%${escapeSqlLikePattern(normalizedQuery)}%`;
|
|
7245
|
-
const
|
|
7246
|
-
const
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
race.
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7323
|
+
const accepts = (type) => !requestedTypes || requestedTypes.has(type);
|
|
7324
|
+
const chapters = accepts("chapter")
|
|
7325
|
+
? this.db.all("SELECT id, title, content, volume_id FROM chapters WHERE work_id = ? AND deleted_at IS NULL AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\') LIMIT 50", workId, pattern, pattern)
|
|
7326
|
+
: [];
|
|
7327
|
+
const races = accepts("race")
|
|
7328
|
+
? this.listRaces(workId).filter((race) => {
|
|
7329
|
+
const lineage = race.lineage;
|
|
7330
|
+
const effectiveSettings = race.effectiveSettings;
|
|
7331
|
+
return [
|
|
7332
|
+
race.name,
|
|
7333
|
+
race.description,
|
|
7334
|
+
...race.settings,
|
|
7335
|
+
...lineage.map((item) => item.name),
|
|
7336
|
+
...effectiveSettings.flatMap((item) => [item.value, item.sourceRaceName])
|
|
7337
|
+
].join("\n").toLocaleLowerCase("zh-CN").includes(normalizedQuery);
|
|
7338
|
+
}).slice(0, 50)
|
|
7339
|
+
: [];
|
|
7340
|
+
const settings = accepts("setting")
|
|
7341
|
+
? this.db.all("SELECT id, title, content, category FROM settings WHERE work_id = ? AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\') LIMIT 50", workId, pattern, pattern)
|
|
7342
|
+
: [];
|
|
7343
|
+
const characters = accepts("character") ? this.db.all(`WITH RECURSIVE character_race_lineage(character_id, race_id, parent_race_id, name, path) AS (
|
|
7259
7344
|
SELECT character.id, race.id, race.parent_race_id, race.name, race.name
|
|
7260
7345
|
FROM characters character JOIN races race ON race.id = character.race_id
|
|
7261
7346
|
WHERE character.work_id = ?
|
|
@@ -7271,9 +7356,11 @@ export class Store {
|
|
|
7271
7356
|
WHERE character.work_id = ? AND character.merged_into_character_id IS NULL AND (
|
|
7272
7357
|
character.name LIKE ? ESCAPE '\\' OR character.aliases_json LIKE ? ESCAPE '\\' OR character.species LIKE ? ESCAPE '\\'
|
|
7273
7358
|
OR EXISTS (SELECT 1 FROM character_race_lineage lineage WHERE lineage.character_id = character.id AND lineage.name LIKE ? ESCAPE '\\')
|
|
7274
|
-
) LIMIT 50`, workId, workId, pattern, pattern, pattern, pattern);
|
|
7275
|
-
const organizations =
|
|
7276
|
-
|
|
7359
|
+
) LIMIT 50`, workId, workId, pattern, pattern, pattern, pattern) : [];
|
|
7360
|
+
const organizations = accepts("organization")
|
|
7361
|
+
? this.db.all("SELECT id, name, description, is_dissolved, settings_json FROM organizations WHERE work_id = ? AND (name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR settings_json LIKE ? ESCAPE '\\') LIMIT 50", workId, pattern, pattern, pattern)
|
|
7362
|
+
: [];
|
|
7363
|
+
const characterSections = accepts("character") ? this.searchCharacterProfileSections(workId, normalizedQuery, 30) : [];
|
|
7277
7364
|
const snippet = (content) => {
|
|
7278
7365
|
const index = content.toLocaleLowerCase().indexOf(normalizedQuery);
|
|
7279
7366
|
const start = Math.max(0, index - 40);
|
|
@@ -7295,7 +7382,7 @@ export class Store {
|
|
|
7295
7382
|
title: `${String(section.characterName)} / ${String(section.title)}`,
|
|
7296
7383
|
snippet: snippet(String(section.contentMarkdown)),
|
|
7297
7384
|
sectionType: String(section.sectionType),
|
|
7298
|
-
isDead: Boolean(
|
|
7385
|
+
isDead: Boolean(section.isDead)
|
|
7299
7386
|
})),
|
|
7300
7387
|
...settings.map((row) => ({ type: "setting", id: requiredString(row, "id"), title: requiredString(row, "title"), snippet: snippet(requiredString(row, "content")), category: requiredString(row, "category") })),
|
|
7301
7388
|
...races.map((race) => {
|