@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.
- package/dist/ai.js +209 -56
- package/dist/ai.js.map +1 -1
- package/dist/app.js +24 -6
- 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/public/ai-message-meta.js +11 -2
- package/dist/public/app.js +113 -93
- package/dist/public/index.html +3 -3
- package/dist/public/styles.css +12 -7
- package/dist/store.js +138 -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/public/styles.css
CHANGED
|
@@ -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;
|
|
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:
|
|
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:
|
|
3115
|
-
.vditor-editor-host.vditor.show-line-numbers .vditor-
|
|
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
|
-
.
|
|
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) => ({
|
|
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",
|
|
@@ -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
|
|
7222
|
-
const
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
race.
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
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 =
|
|
7252
|
-
|
|
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(
|
|
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) => {
|