@musnows/scriverse 0.7.7 → 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.
@@ -379,7 +379,7 @@ input[type="checkbox"][hidden] { display: none; }
379
379
  }
380
380
 
381
381
  .brand-block { display: flex; align-items: center; gap: 11px; padding: 0 20px; border: 0; background: transparent; text-align: left; }
382
- .brand-mark { display: grid; place-items: center; width: 34px; height: 34px; color: #fff; background: var(--accent); border-radius: 3px; font-weight: 700; }
382
+ .brand-mark { display: grid; place-items: center; width: 34px; height: 34px; color: #fff; border-radius: 3px; font-weight: 700; }
383
383
  .brand-block strong, .brand-block small { display: block; }
384
384
  .brand-block strong { font-size: 16px; letter-spacing: .16em; }
385
385
  .brand-block small { color: var(--muted); font-size: 10px; margin-top: 2px; }
@@ -2796,11 +2796,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2796
2796
  .large-dialog { width: min(1120px, 94vw); max-height: 94dvh; }
2797
2797
  .large-dialog .dialog-fields { max-height: 72dvh; padding: 24px 28px; }
2798
2798
  .editor-dialog { width: min(1180px, 94vw); max-height: calc(100dvh - 16px); }
2799
- .editor-dialog .dialog-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 76dvh; padding: 24px 28px; }
2799
+ .editor-dialog .dialog-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: min(76dvh, calc(100dvh - 214px)); padding: 24px 28px; }
2800
2800
  .editor-dialog .dialog-fields > .form-field-note, .editor-dialog .dialog-fields > .markdown-editor-field { grid-column: 1 / -1; }
2801
2801
  .dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
2802
2802
  .dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
2803
2803
  .dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
2804
+ .dialog-title-input { display: block; width: min(560px, calc(100vw - 150px)); min-width: 0; margin: -5px 0 0; padding: 0 0 4px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--ink); font-size: 23px; font-weight: 500; line-height: 1.3; }
2805
+ .dialog-title-input:focus { border-bottom-color: var(--accent); outline: none; }
2806
+ .dialog-title-input::placeholder { color: var(--muted); opacity: .8; }
2804
2807
  .dialog-header-meta { margin: 7px 0 0; color: var(--muted); font-size: 10px; line-height: 1.5; }
2805
2808
  .dialog-close {
2806
2809
  display: grid;
@@ -3109,10 +3112,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3109
3112
  .vditor-editor-host .vditor-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
3110
3113
  .vditor-editor-host .markdown-word-count { display: inline-flex; align-items: center; align-self: center; min-height: 28px; margin-left: 24px; padding: 0 10px; color: var(--muted); font: 10px/1 var(--font-latin), var(--font-cjk), sans-serif; white-space: nowrap; }
3111
3114
  .vditor-editor-host .vditor-content { position: relative; }
3112
- .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 0; overflow: hidden; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 88%, transparent); color: var(--muted); font: 12px/1.75 var(--font-latin), monospace; text-align: right; user-select: none; pointer-events: none; will-change: transform; }
3115
+ .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 0; overflow: hidden; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 88%, transparent); color: var(--muted); font: 12px/1.75 var(--font-latin), monospace; text-align: center; user-select: none; pointer-events: none; will-change: transform; }
3113
3116
  .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: block; }
3114
- .vditor-editor-host .vditor-line-number { position: absolute; top: 0; right: 8px; left: 0; display: block; box-sizing: border-box; overflow: hidden; padding: 0; text-align: right; white-space: nowrap; }
3115
- .vditor-editor-host.vditor.show-line-numbers .vditor-ir, .vditor-editor-host.vditor.show-line-numbers .vditor-wysiwyg, .vditor-editor-host.vditor.show-line-numbers .vditor-sv, .vditor-editor-host.vditor.show-line-numbers .vditor-sv textarea { padding-left: 42px; }
3117
+ .vditor-editor-host .vditor-line-number { position: absolute; top: 0; right: 0; left: 0; display: block; box-sizing: border-box; overflow: hidden; padding: 0; text-align: center; white-space: nowrap; }
3118
+ .vditor-editor-host.vditor.show-line-numbers .vditor-sv, .vditor-editor-host.vditor.show-line-numbers .vditor-sv textarea { padding-left: 42px; }
3116
3119
  .vditor-editor-host .vditor-line-number-button[aria-pressed="true"] { color: var(--accent-dark); background: color-mix(in srgb, var(--accent) 16%, transparent); }
3117
3120
  .vditor-editor-host .vditor-content { min-height: 0; }
3118
3121
  .vditor-editor-host .vditor-reset { color: var(--ink); font-family: var(--font-cjk), var(--font-latin), sans-serif; }
@@ -3228,7 +3231,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3228
3231
  .character-section-editor-title-input::placeholder { color: var(--ink); opacity: .92; }
3229
3232
  .character-section-editor-title-input:focus { border-color: var(--accent); box-shadow: none; }
3230
3233
  .character-markdown-editor { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 12px; min-height: 0; padding: 16px 22px 18px; overflow: hidden; background: var(--surface-soft); }
3231
- .character-markdown-editor > .vditor-editor-host { width: 100%; max-width: 960px; justify-self: center; }
3234
+ .knowledge-markdown-editor { grid-template-rows: minmax(0, 1fr) auto; }
3235
+ .character-markdown-editor > .vditor-editor-host { width: 100%; justify-self: stretch; }
3232
3236
  .character-markdown-editor > .vditor-editor-host.vditor { border-color: color-mix(in srgb, var(--accent) 22%, var(--line)); box-shadow: 0 12px 34px color-mix(in srgb, var(--ink) 12%, transparent); transition: border-color .16s ease, box-shadow .16s ease; }
3233
3237
  .character-markdown-editor > .vditor-editor-host.vditor:focus-within { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 15%, transparent), 0 16px 42px color-mix(in srgb, var(--ink) 16%, transparent); }
3234
3238
  .character-markdown-editor-meta { display: grid; grid-template-columns: minmax(160px, .55fr) minmax(280px, 1.45fr); gap: 12px; }
@@ -3393,7 +3397,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3393
3397
  .setting-markdown-field { height: 70dvh; }
3394
3398
  .setting-markdown-heading, .markdown-editor-field-heading { align-items: flex-start; flex-direction: column; gap: 4px; }
3395
3399
  .setting-markdown-field > .vditor-editor-host, .markdown-editor-field .vditor-editor-host { min-height: 42dvh; }
3396
- .editor-dialog .dialog-fields { grid-template-columns: minmax(0, 1fr); max-height: 76dvh; }
3400
+ .editor-dialog .dialog-fields { grid-template-columns: minmax(0, 1fr); max-height: min(76dvh, calc(100dvh - 264px)); }
3397
3401
  .editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: 52dvh !important; }
3398
3402
  .character-editor-workspace, .character-editor-workspace.history-open { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
3399
3403
  .character-editor-nav { flex-direction: row; overflow-x: auto; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--line); }
@@ -3630,6 +3634,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3630
3634
  }
3631
3635
  .dialog-header { padding: 14px 16px 12px; }
3632
3636
  .dialog-header h2 { font-size: 20px; }
3637
+ .dialog-title-input { width: min(100%, calc(100vw - 100px)); font-size: 20px; }
3633
3638
  .settings-dialog-header-actions { gap: 6px; }
3634
3639
  .dialog-fields, .appearance-grid, .access-dialog-body, .account-settings-body, .versions-list, .entity-history-list, .chapter-annotations-list { padding-inline: 16px; }
3635
3640
  .chapter-version-compare { grid-template-columns: minmax(0, 1fr); padding-inline: 16px; }
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) => ({ ...this.mapCharacterProfileSection(row), characterName: requiredString(row, "character_name") }));
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 rows = this.db.all("SELECT id, role, content, metadata_json FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId);
5268
- const compactedMessageCount = Math.min(rows.length, Math.max(0, numberValue(conversation, "compacted_message_count")));
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: rows.length,
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.slice(compactedMessageCount)
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",
@@ -5284,6 +5362,19 @@ export class Store {
5284
5362
  }))
5285
5363
  };
5286
5364
  }
5365
+ getAiConversationLockedModelId(conversationId, workId) {
5366
+ const conversation = this.db.get("SELECT work_id FROM ai_conversations WHERE id = ?", conversationId);
5367
+ if (!conversation)
5368
+ throw notFound("AI 对话");
5369
+ if (requiredString(conversation, "work_id") !== workId)
5370
+ throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
5371
+ const message = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
5372
+ WHERE conversation_id = ? AND role = 'user'
5373
+ ORDER BY created_at, rowid
5374
+ LIMIT 1`, conversationId);
5375
+ const metadata = message ? json(requiredString(message, "metadata_json"), {}) : {};
5376
+ return typeof metadata.modelId === "string" && metadata.modelId.trim() ? metadata.modelId.trim() : null;
5377
+ }
5287
5378
  getAiConversationInjectedEntities(conversationId, workId) {
5288
5379
  const conversation = this.db.get("SELECT work_id, injected_entities_json FROM ai_conversations WHERE id = ?", conversationId);
5289
5380
  if (!conversation)
@@ -5705,6 +5796,16 @@ export class Store {
5705
5796
  }
5706
5797
  mapAiConversation(row) {
5707
5798
  const roleplayCharacterId = optionalString(row, "roleplay_character_id");
5799
+ const firstUserMessage = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
5800
+ WHERE conversation_id = ? AND role = 'user'
5801
+ ORDER BY created_at, rowid
5802
+ LIMIT 1`, requiredString(row, "id"));
5803
+ const firstUserMetadata = firstUserMessage
5804
+ ? json(requiredString(firstUserMessage, "metadata_json"), {})
5805
+ : {};
5806
+ const lockedModelId = typeof firstUserMetadata.modelId === "string" && firstUserMetadata.modelId.trim()
5807
+ ? firstUserMetadata.modelId.trim()
5808
+ : null;
5708
5809
  const roleplayCharacter = roleplayCharacterId
5709
5810
  ? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayCharacterId, requiredString(row, "work_id"))
5710
5811
  : undefined;
@@ -5718,6 +5819,7 @@ export class Store {
5718
5819
  hasCompactedSummary: Boolean(requiredString(row, "compacted_summary")),
5719
5820
  contextWarningPending: Boolean(optionalString(row, "context_warning_at")),
5720
5821
  taskType: optionalString(row, "task_type") ?? (roleplayCharacterId ? "roleplay" : "chat"),
5822
+ ...(lockedModelId ? { modelId: lockedModelId } : {}),
5721
5823
  contextScope: json(optionalString(row, "context_scope_json") ?? "", { type: "none" }),
5722
5824
  roleplayCharacter: roleplayCharacter ? {
5723
5825
  id: requiredString(roleplayCharacter, "id"),
@@ -7212,26 +7314,33 @@ export class Store {
7212
7314
  return [{ type: "none" }];
7213
7315
  return [{ type: "unknown", scope }];
7214
7316
  }
7215
- search(workId, query) {
7317
+ search(workId, query, requestedTypes) {
7216
7318
  this.getWork(workId);
7217
7319
  const normalizedQuery = normalizeWorkSearchQuery(query);
7218
7320
  if (!normalizedQuery)
7219
7321
  return [];
7220
7322
  const pattern = `%${escapeSqlLikePattern(normalizedQuery)}%`;
7221
- const chapters = 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);
7222
- const races = this.listRaces(workId).filter((race) => {
7223
- const lineage = race.lineage;
7224
- const effectiveSettings = race.effectiveSettings;
7225
- return [
7226
- race.name,
7227
- race.description,
7228
- ...race.settings,
7229
- ...lineage.map((item) => item.name),
7230
- ...effectiveSettings.flatMap((item) => [item.value, item.sourceRaceName])
7231
- ].join("\n").toLocaleLowerCase("zh-CN").includes(normalizedQuery);
7232
- }).slice(0, 50);
7233
- const settings = 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);
7234
- const characters = this.db.all(`WITH RECURSIVE character_race_lineage(character_id, race_id, parent_race_id, name, path) AS (
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 (
7235
7344
  SELECT character.id, race.id, race.parent_race_id, race.name, race.name
7236
7345
  FROM characters character JOIN races race ON race.id = character.race_id
7237
7346
  WHERE character.work_id = ?
@@ -7247,9 +7356,11 @@ export class Store {
7247
7356
  WHERE character.work_id = ? AND character.merged_into_character_id IS NULL AND (
7248
7357
  character.name LIKE ? ESCAPE '\\' OR character.aliases_json LIKE ? ESCAPE '\\' OR character.species LIKE ? ESCAPE '\\'
7249
7358
  OR EXISTS (SELECT 1 FROM character_race_lineage lineage WHERE lineage.character_id = character.id AND lineage.name LIKE ? ESCAPE '\\')
7250
- ) LIMIT 50`, workId, workId, pattern, pattern, pattern, pattern);
7251
- const organizations = 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);
7252
- const characterSections = this.searchCharacterProfileSections(workId, normalizedQuery, 30);
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) : [];
7253
7364
  const snippet = (content) => {
7254
7365
  const index = content.toLocaleLowerCase().indexOf(normalizedQuery);
7255
7366
  const start = Math.max(0, index - 40);
@@ -7271,7 +7382,7 @@ export class Store {
7271
7382
  title: `${String(section.characterName)} / ${String(section.title)}`,
7272
7383
  snippet: snippet(String(section.contentMarkdown)),
7273
7384
  sectionType: String(section.sectionType),
7274
- isDead: Boolean(this.getCharacter(String(section.characterId)).isDead)
7385
+ isDead: Boolean(section.isDead)
7275
7386
  })),
7276
7387
  ...settings.map((row) => ({ type: "setting", id: requiredString(row, "id"), title: requiredString(row, "title"), snippet: snippet(requiredString(row, "content")), category: requiredString(row, "category") })),
7277
7388
  ...races.map((race) => {