@musnows/scriverse 0.6.8 → 0.6.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.
@@ -36,7 +36,7 @@ import {
36
36
  taskScopeLabel,
37
37
  timelineStatusLabel,
38
38
  characterStateFieldLabel
39
- } from "/display-labels.js?v=20260801-google-vertex";
39
+ } from "/display-labels.js?v=20260804-agent-history-search-v1";
40
40
  import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260731-work-comments-v2";
41
41
  import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
42
42
  import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
@@ -45,7 +45,7 @@ import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20
45
45
  import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260731-drafts-to-ideas-v1";
46
46
  import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
47
47
  import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
48
- import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
48
+ import { prioritizeGlobalSearchResults, resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260804-agent-history-search-v1";
49
49
  import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
50
50
  import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
51
51
  import {
@@ -1812,8 +1812,10 @@ async function ensureAiConversationsLoaded() {
1812
1812
  }
1813
1813
  }
1814
1814
 
1815
- async function openAiConversation(conversationId, hideHistory = true) {
1816
- const conversation = await api(`/api/ai-conversations/${conversationId}?page=1&limit=100`);
1815
+ async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
1816
+ const parameters = new URLSearchParams({ page: "1", limit: "100" });
1817
+ if (focusMessageId) parameters.set("messageId", String(focusMessageId));
1818
+ const conversation = await api(`/api/ai-conversations/${conversationId}?${parameters}`);
1817
1819
  upsertAiConversationSummary(conversation);
1818
1820
  state.aiConversationId = conversation.id;
1819
1821
  state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
@@ -1839,6 +1841,15 @@ async function openAiConversation(conversationId, hideHistory = true) {
1839
1841
  if (hideHistory) setAiHistoryVisible(false);
1840
1842
  }
1841
1843
 
1844
+ function focusAiConversationMessage(messageId) {
1845
+ const message = [...$("#ai-feed").querySelectorAll("[data-message-id]")]
1846
+ .find((candidate) => candidate.dataset.messageId === String(messageId));
1847
+ if (!message) return;
1848
+ message.scrollIntoView({ behavior: "smooth", block: "center" });
1849
+ message.classList.add("is-search-target");
1850
+ window.setTimeout(() => message.classList.remove("is-search-target"), 1800);
1851
+ }
1852
+
1842
1853
  async function createNewAiConversation(taskType = "chat") {
1843
1854
  if (!state.work) return;
1844
1855
  const conversation = await api(`/api/works/${state.work.id}/ai-conversations`, { method: "POST", body: { taskType } });
@@ -3965,8 +3976,9 @@ function renderSearchResults(results, query) {
3965
3976
  $("#search-results").innerHTML = '<p class="search-results-status">未找到相关内容。</p>';
3966
3977
  return;
3967
3978
  }
3979
+ const orderedResults = prioritizeGlobalSearchResults(results);
3968
3980
  const matchKindLabel = { metadata: "资料命中", exact: "精确命中", phonetic: "拼音命中" };
3969
- $("#search-results").innerHTML = `<p class="search-results-summary">找到 ${results.length} 条结果,按综合相关度排序。</p>${results.map((item) => {
3981
+ $("#search-results").innerHTML = `<p class="search-results-summary">找到 ${orderedResults.length} 条结果,非正文结果优先,正文条目随后展示。</p>${orderedResults.map((item) => {
3970
3982
  const matchKinds = Array.isArray(item.matchKinds) ? item.matchKinds : [];
3971
3983
  const lineRange = Number.isInteger(item.startLine)
3972
3984
  ? `<span class="search-result-chip">${item.startLine === item.endLine ? `第 ${item.startLine} 行` : `第 ${item.startLine}-${item.endLine} 行`}</span>`
@@ -3981,7 +3993,7 @@ function renderSearchResults(results, query) {
3981
3993
  }).join("")}`;
3982
3994
  $("#search-results").querySelectorAll(".search-result").forEach((button, index) => {
3983
3995
  button.addEventListener("click", () => {
3984
- openSearchResult(results[index])
3996
+ openSearchResult(orderedResults[index])
3985
3997
  .catch((error) => toast(error.message, "error"));
3986
3998
  });
3987
3999
  });
@@ -4050,6 +4062,12 @@ async function openSearchResult(result) {
4050
4062
  || !$("#platform-usage-view").classList.contains("hidden")
4051
4063
  || !$("#work-audit-view").classList.contains("hidden");
4052
4064
  if (inSettings) await returnFromSettings();
4065
+ if (target.kind === "agent-history") {
4066
+ ensureAiPanelExpanded();
4067
+ await openAiConversation(target.conversationId, true, target.messageId);
4068
+ if (target.messageId) focusAiConversationMessage(target.messageId);
4069
+ return;
4070
+ }
4053
4071
  if (target.kind === "chapter") {
4054
4072
  await selectChapter(target.id);
4055
4073
  if (state.chapter?.id === target.id && target.startLine) {
@@ -6841,11 +6859,9 @@ function renderProviderCards(providers, models) {
6841
6859
  const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
6842
6860
  const modelStatus = !model.enabled
6843
6861
  ? `<span class="model-status-badge is-disabled">模型已停用</span>`
6844
- : provider.status !== "enabled"
6845
- ? `<span class="model-status-badge is-disabled">供应商已停用</span>`
6846
- : provider.connectionStatus !== "success"
6847
- ? `<span class="model-status-badge is-unavailable">连接不可用</span>`
6848
- : "";
6862
+ : provider.connectionStatus !== "success"
6863
+ ? `<span class="model-status-badge is-unavailable">连接不可用</span>`
6864
+ : "";
6849
6865
  const capability = model.multimodalEnabled ? " · 多模态" : "";
6850
6866
  const defaultBadge = model.imageToolDefault ? " · 默认读图模型" : "";
6851
6867
  return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"}${capability}${defaultBadge} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}</div>`;
@@ -93,7 +93,8 @@ export function searchResultTypeLabel(value) {
93
93
  relationship: "人物关系",
94
94
  "chapter-outline": "章节大纲",
95
95
  foreshadow: "伏笔",
96
- review: "审核项"
96
+ review: "审核项",
97
+ "agent-history": "Agent 历史"
97
98
  }, value, "其他资料");
98
99
  }
99
100
 
@@ -1,5 +1,6 @@
1
1
  export type GlobalSearchTarget =
2
2
  | { kind: "chapter"; type: "chapter"; id: string; module: "editor"; startLine?: number; endLine?: number }
3
+ | { kind: "agent-history"; type: "agent-history"; id: string; module: "editor"; conversationId: string; messageId?: string }
3
4
  | {
4
5
  kind: "entity";
5
6
  type: "setting" | "character" | "race" | "organization" | "timeline-track" | "timeline-event" | "relationship" | "chapter-outline" | "foreshadow" | "review";
@@ -9,5 +10,6 @@ export type GlobalSearchTarget =
9
10
  apiPath: string;
10
11
  };
11
12
 
13
+ export function prioritizeGlobalSearchResults<T extends { type?: unknown }>(results: readonly T[]): T[];
12
14
  export function splitGlobalSearchHighlight(value: unknown, query: unknown): Array<{ text: string; match: boolean }>;
13
- export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown; startLine?: unknown; endLine?: unknown }): GlobalSearchTarget | null;
15
+ export function resolveGlobalSearchTarget(result?: { type?: unknown; id?: unknown; startLine?: unknown; endLine?: unknown; conversationId?: unknown; messageId?: unknown }): GlobalSearchTarget | null;
@@ -11,6 +11,15 @@ const entityTargets = Object.freeze({
11
11
  review: Object.freeze({ module: "reviews", entity: "review", resource: "reviews" })
12
12
  });
13
13
 
14
+ export function prioritizeGlobalSearchResults(results = []) {
15
+ const settingResults = [];
16
+ const proseResults = [];
17
+ for (const result of Array.isArray(results) ? results : []) {
18
+ (String(result?.type ?? "") === "chapter" ? proseResults : settingResults).push(result);
19
+ }
20
+ return [...settingResults, ...proseResults];
21
+ }
22
+
14
23
  function positiveLine(value) {
15
24
  const line = Number(value);
16
25
  return Number.isInteger(line) && line > 0 ? line : null;
@@ -39,6 +48,19 @@ export function resolveGlobalSearchTarget(result = {}) {
39
48
  const type = String(result.type ?? "").trim();
40
49
  const id = String(result.id ?? "").trim();
41
50
  if (!id) return null;
51
+ if (type === "agent-history") {
52
+ const conversationId = String(result.conversationId ?? "").trim();
53
+ if (!conversationId) return null;
54
+ const messageId = String(result.messageId ?? "").trim();
55
+ return {
56
+ kind: "agent-history",
57
+ type,
58
+ id,
59
+ module: "editor",
60
+ conversationId,
61
+ ...(messageId ? { messageId } : {})
62
+ };
63
+ }
42
64
  if (type === "chapter") {
43
65
  const startLine = positiveLine(result.startLine);
44
66
  const endLine = positiveLine(result.endLine);
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260802-draft-readonly-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260804-agent-history-search-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -613,7 +613,7 @@
613
613
  </div>
614
614
  <div class="access-dialog-body">
615
615
  <form id="search-form" class="search-form">
616
- <label>关键词<input id="search-query" name="query" type="search" maxlength="500" placeholder="搜索正文、设定、人物、时间线、关系、大纲或伏笔" required></label>
616
+ <label>关键词<input id="search-query" name="query" type="search" maxlength="500" placeholder="搜索正文、设定、人物、时间线、关系、大纲、伏笔或 Agent 历史" required></label>
617
617
  <label>资料类型<select id="search-type" name="type">
618
618
  <option value="">全部资料</option>
619
619
  <option value="chapter">章节</option>
@@ -627,6 +627,7 @@
627
627
  <option value="chapter-outline">章节大纲</option>
628
628
  <option value="foreshadow">伏笔</option>
629
629
  <option value="review">审核项</option>
630
+ <option value="agent-history">Agent 历史</option>
630
631
  </select></label>
631
632
  <button class="primary-button" type="submit">搜索</button>
632
633
  </form>
@@ -953,6 +954,6 @@
953
954
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
954
955
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
955
956
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
956
- <script type="module" src="/app.js?v=20260802-draft-readonly-v1"></script>
957
+ <script type="module" src="/app.js?v=20260804-agent-history-search-v1"></script>
957
958
  </body>
958
959
  </html>
@@ -2193,6 +2193,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2193
2193
  .ai-context-compaction-divider span { letter-spacing: .08em; white-space: nowrap; }
2194
2194
  .assistant-message, .user-message { position: relative; margin-bottom: 12px; padding: 12px 13px; border-radius: 5px; font-size: 12px; line-height: 1.55; }
2195
2195
  .assistant-message { background: var(--paper-deep); }
2196
+ .assistant-message.is-search-target, .user-message.is-search-target { outline: 2px solid var(--accent); outline-offset: 3px; }
2196
2197
  .assistant-message.is-error { border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line)); border-left: 3px solid var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--paper-deep)); }
2197
2198
  .assistant-message.has-message-actions, .user-message.has-message-actions { margin-bottom: 42px; }
2198
2199
  .user-message { margin-left: 24px; background: var(--accent); color: #fff; }
@@ -3209,7 +3210,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3209
3210
  .app-shell.ai-panel-collapsed .ai-panel > :not(.ai-heading) { display: none; }
3210
3211
  .app-shell.ai-panel-collapsed .ai-heading { display: flex; justify-content: space-between; padding: 0; }
3211
3212
 
3212
- .shelf-view { height: auto; min-height: 100%; padding: 28px var(--mobile-gutter) 42px; }
3213
+ .shelf-view { height: 100%; min-height: 0; padding: 28px var(--mobile-gutter) 42px; }
3213
3214
  #shelf-view, #settings-hub-view { padding-bottom: 0; }
3214
3215
  #shelf-view .product-footer, #settings-hub-view .product-footer { margin-bottom: 0; padding-bottom: env(safe-area-inset-bottom); }
3215
3216
  .shelf-header, .module-header { align-items: stretch; flex-direction: column; gap: 14px; margin-bottom: 20px; }
package/dist/store.js CHANGED
@@ -3369,9 +3369,11 @@ export class Store {
3369
3369
  JOIN characters character ON character.id = search.character_id`;
3370
3370
  const rows = [...normalized].length <= 2
3371
3371
  ? this.db.all(`${columns} JOIN character_profile_section_short_terms term ON term.search_id = search.id
3372
- WHERE search.work_id = ? AND term.term = ? ORDER BY character.name, section.sort_order LIMIT ?`, workId, normalized, limit)
3372
+ WHERE search.work_id = ? AND character.merged_into_character_id IS NULL AND term.term = ?
3373
+ ORDER BY character.name, section.sort_order LIMIT ?`, workId, normalized, limit)
3373
3374
  : this.db.all(`${columns} JOIN character_profile_section_search_fts fts ON fts.rowid = search.id
3374
- WHERE search.work_id = ? AND character_profile_section_search_fts MATCH ?
3375
+ WHERE search.work_id = ? AND character.merged_into_character_id IS NULL
3376
+ AND character_profile_section_search_fts MATCH ?
3375
3377
  ORDER BY bm25(character_profile_section_search_fts), character.name, section.sort_order LIMIT ?`, workId, `"${normalized.replaceAll('"', '""')}"`, limit);
3376
3378
  return rows.map((row) => ({ ...this.mapCharacterProfileSection(row), characterName: requiredString(row, "character_name") }));
3377
3379
  }
@@ -4403,7 +4405,10 @@ export class Store {
4403
4405
  const conversationId = id("conversation");
4404
4406
  const timestamp = now();
4405
4407
  const agentTools = normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools);
4406
- this.db.run("INSERT INTO ai_conversations (id, work_id, task_type, title, agent_tools_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", conversationId, workId, taskType, title.trim() || "新对话", JSON.stringify(agentTools), timestamp, timestamp, currentRequestActor()?.userId ?? null);
4408
+ this.db.transaction(() => {
4409
+ this.db.run("INSERT INTO ai_conversations (id, work_id, task_type, title, agent_tools_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", conversationId, workId, taskType, title.trim() || "新对话", JSON.stringify(agentTools), timestamp, timestamp, currentRequestActor()?.userId ?? null);
4410
+ this.syncAiHistorySearchShortTermsForSource("conversation", conversationId);
4411
+ });
4407
4412
  return this.getAiConversation(conversationId);
4408
4413
  }
4409
4414
  listAiConversations(workId) {
@@ -4444,14 +4449,29 @@ export class Store {
4444
4449
  const messages = this.db.all("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId).map((message) => this.mapAiConversationMessage(message));
4445
4450
  return { ...this.mapAiConversation(row), messageCount: messages.length, messages };
4446
4451
  }
4447
- getAiConversationPage(conversationId, pagination) {
4452
+ getAiConversationPage(conversationId, pagination, focusMessageId) {
4448
4453
  const row = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
4449
4454
  if (!row)
4450
4455
  throw notFound("AI 对话");
4451
4456
  const countRow = this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId);
4452
- const page = paginationSql(pagination);
4457
+ let effectivePagination = pagination;
4458
+ if (focusMessageId) {
4459
+ const focused = this.db.get("SELECT rowid, created_at FROM ai_conversation_messages WHERE conversation_id = ? AND id = ?", conversationId, focusMessageId);
4460
+ if (focused) {
4461
+ const newerCount = this.db.get(`SELECT COUNT(*) AS count FROM ai_conversation_messages
4462
+ WHERE conversation_id = ?
4463
+ AND (created_at > ? OR (created_at = ? AND rowid > ?))`, conversationId, String(focused.created_at ?? ""), String(focused.created_at ?? ""), Number(focused.rowid ?? 0));
4464
+ const focusPage = Math.floor(Number(newerCount?.count ?? 0) / pagination.limit) + 1;
4465
+ effectivePagination = {
4466
+ ...pagination,
4467
+ page: focusPage,
4468
+ offset: (focusPage - 1) * pagination.limit
4469
+ };
4470
+ }
4471
+ }
4472
+ const page = paginationSql(effectivePagination);
4453
4473
  const rows = this.db.all(`SELECT * FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at DESC, rowid DESC${page.sql}`, conversationId, ...page.params);
4454
- const messagesPage = paginated(rows.map((message) => this.mapAiConversationMessage(message)), pagination);
4474
+ const messagesPage = paginated(rows.map((message) => this.mapAiConversationMessage(message)), effectivePagination);
4455
4475
  messagesPage.items.reverse();
4456
4476
  return {
4457
4477
  ...this.mapAiConversation(row),
@@ -4555,7 +4575,10 @@ export class Store {
4555
4575
  const conversation = this.db.get("SELECT id FROM ai_conversations WHERE id = ?", conversationId);
4556
4576
  if (!conversation)
4557
4577
  throw notFound("AI 对话");
4558
- this.db.run("UPDATE ai_conversations SET compacted_summary = ?, compacted_message_count = ?, context_warning_at = NULL, updated_at = ? WHERE id = ?", summary, Math.max(0, compactedMessageCount), now(), conversationId);
4578
+ this.db.transaction(() => {
4579
+ this.db.run("UPDATE ai_conversations SET compacted_summary = ?, compacted_message_count = ?, context_warning_at = NULL, updated_at = ? WHERE id = ?", summary, Math.max(0, compactedMessageCount), now(), conversationId);
4580
+ this.syncAiHistorySearchShortTermsForSource("conversation", conversationId);
4581
+ });
4559
4582
  return this.getAiConversation(conversationId);
4560
4583
  }
4561
4584
  setAiConversationTitle(conversationId, title) {
@@ -4563,7 +4586,10 @@ export class Store {
4563
4586
  if (!conversation)
4564
4587
  throw notFound("AI 对话");
4565
4588
  const normalizedTitle = title.replace(/\s+/gu, " ").trim().slice(0, 200) || "新对话";
4566
- this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", normalizedTitle, now(), conversationId);
4589
+ this.db.transaction(() => {
4590
+ this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", normalizedTitle, now(), conversationId);
4591
+ this.syncAiHistorySearchShortTermsForSource("conversation", conversationId);
4592
+ });
4567
4593
  return this.getAiConversation(conversationId);
4568
4594
  }
4569
4595
  setAiConversationRoleplayCharacter(conversationId, characterId) {
@@ -4666,14 +4692,19 @@ export class Store {
4666
4692
  }
4667
4693
  const messageId = id("message");
4668
4694
  const timestamp = now();
4669
- const title = requiredString(conversation, "title") === "新对话" && input.role === "user"
4695
+ const previousTitle = requiredString(conversation, "title");
4696
+ const title = previousTitle === "新对话" && input.role === "user"
4670
4697
  ? defaultAiConversationTitle(input.content)
4671
- : requiredString(conversation, "title");
4698
+ : previousTitle;
4672
4699
  this.db.transaction(() => {
4673
4700
  this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(conversation_id, request_id) WHERE request_id IS NOT NULL DO NOTHING", messageId, conversationId, input.role, input.content, JSON.stringify(input.citations ?? []), JSON.stringify(input.metadata ?? {}), requestId, timestamp, currentRequestActor()?.userId ?? null);
4674
4701
  const inserted = this.db.get("SELECT id FROM ai_conversation_messages WHERE id = ?", messageId);
4675
- if (inserted)
4702
+ if (inserted) {
4676
4703
  this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", title, timestamp, conversationId);
4704
+ if (title !== previousTitle)
4705
+ this.syncAiHistorySearchShortTermsForSource("conversation", conversationId);
4706
+ this.syncAiHistorySearchShortTermsForSource("message", messageId);
4707
+ }
4677
4708
  });
4678
4709
  const message = requestId
4679
4710
  ? this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId)
@@ -4707,9 +4738,33 @@ export class Store {
4707
4738
  for (const message of messages.slice(0, targetIndex + 1)) {
4708
4739
  this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), optionalString(message, "request_id"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
4709
4740
  }
4741
+ this.syncAiHistorySearchShortTermsForConversation(forkId);
4710
4742
  });
4711
4743
  return this.getAiConversation(forkId);
4712
4744
  }
4745
+ syncAiHistorySearchShortTermsForSource(sourceType, sourceId) {
4746
+ const row = this.db.get("SELECT id, title, content, search_content FROM ai_history_search WHERE source_type = ? AND source_id = ?", sourceType, sourceId);
4747
+ if (!row)
4748
+ return;
4749
+ const searchId = numberValue(row, "id");
4750
+ const searchContent = sourceType === "message"
4751
+ ? normalizeDocumentSearchText(String(row.content ?? ""))
4752
+ : normalizeDocumentSearchText(`${String(row.title ?? "")}\n${String(row.content ?? "")}`);
4753
+ if (String(row.search_content ?? "") !== searchContent) {
4754
+ this.db.run("UPDATE ai_history_search SET search_content = ? WHERE id = ?", searchContent, searchId);
4755
+ }
4756
+ this.db.run("DELETE FROM ai_history_search_short_terms WHERE search_id = ?", searchId);
4757
+ for (const term of documentShortSearchTerms(searchContent)) {
4758
+ this.db.run("INSERT INTO ai_history_search_short_terms (search_id, term) VALUES (?, ?)", searchId, term);
4759
+ }
4760
+ }
4761
+ syncAiHistorySearchShortTermsForConversation(conversationId) {
4762
+ const rows = this.db.all("SELECT source_type, source_id FROM ai_history_search WHERE conversation_id = ?", conversationId);
4763
+ for (const row of rows) {
4764
+ const sourceType = row.source_type === "message" ? "message" : "conversation";
4765
+ this.syncAiHistorySearchShortTermsForSource(sourceType, String(row.source_id));
4766
+ }
4767
+ }
4713
4768
  mapAiConversation(row) {
4714
4769
  const roleplayCharacterId = optionalString(row, "roleplay_character_id");
4715
4770
  const roleplayCharacter = roleplayCharacterId
@@ -6085,7 +6140,7 @@ export class Store {
6085
6140
  SELECT character.id, character.name, character.aliases_json, character.species, character.is_dead,
6086
6141
  COALESCE(path.path, character.species) AS race_path
6087
6142
  FROM characters character LEFT JOIN character_race_paths path ON path.character_id = character.id
6088
- WHERE character.work_id = ? AND (
6143
+ WHERE character.work_id = ? AND character.merged_into_character_id IS NULL AND (
6089
6144
  character.name LIKE ? ESCAPE '\\' OR character.aliases_json LIKE ? ESCAPE '\\' OR character.species LIKE ? ESCAPE '\\'
6090
6145
  OR EXISTS (SELECT 1 FROM character_race_lineage lineage WHERE lineage.character_id = character.id AND lineage.name LIKE ? ESCAPE '\\')
6091
6146
  ) LIMIT 50`, workId, workId, pattern, pattern, pattern, pattern);