@musnows/scriverse 0.8.8 → 0.9.1
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-analysis-timeout.js +12 -0
- package/dist/ai-analysis-timeout.js.map +1 -0
- package/dist/ai.js +411 -91
- package/dist/ai.js.map +1 -1
- package/dist/app.js +291 -22
- package/dist/app.js.map +1 -1
- package/dist/database.js +203 -2
- package/dist/database.js.map +1 -1
- package/dist/desktop-protocol.js +21 -0
- package/dist/desktop-protocol.js.map +1 -0
- package/dist/offline-sync.js +436 -0
- package/dist/offline-sync.js.map +1 -0
- package/dist/public/app.js +228 -20
- package/dist/public/index.html +6 -3
- package/dist/public/styles.css +48 -6
- package/dist/security.js +3 -2
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +11 -2
- package/dist/server-runtime.js.map +1 -1
- package/dist/storage-manifest.js +116 -0
- package/dist/storage-manifest.js.map +1 -0
- package/dist/store.js +277 -62
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +158 -29
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -209,6 +209,13 @@ export const versionedEntityTypes = [
|
|
|
209
209
|
"chapter-outline",
|
|
210
210
|
"foreshadow"
|
|
211
211
|
];
|
|
212
|
+
const bookEntityTypes = ["character", "draft", "setting", "organization"];
|
|
213
|
+
const legacyFavoriteTables = {
|
|
214
|
+
character: "characters",
|
|
215
|
+
draft: "drafts",
|
|
216
|
+
setting: "settings",
|
|
217
|
+
organization: "organizations"
|
|
218
|
+
};
|
|
212
219
|
export const AI_CONVERSATION_STREAM_REQUEST_LEASE_MS = 3 * 60_000;
|
|
213
220
|
export const aiConversationTaskTypes = ["chat", "roleplay", "continue", "polish"];
|
|
214
221
|
export function defaultAiConversationTitle(prompt) {
|
|
@@ -329,6 +336,83 @@ export class Store {
|
|
|
329
336
|
this.migrateEntityVersionBaselines();
|
|
330
337
|
this.purgeExpiredRecycleBin();
|
|
331
338
|
}
|
|
339
|
+
entityPreferenceProjection(entityType, alias) {
|
|
340
|
+
const actor = currentRequestActor();
|
|
341
|
+
if (!actor) {
|
|
342
|
+
return {
|
|
343
|
+
columns: `${alias}.is_favorite AS user_is_favorite,
|
|
344
|
+
EXISTS (
|
|
345
|
+
SELECT 1 FROM work_entity_pins pin
|
|
346
|
+
WHERE pin.work_id = ${alias}.work_id
|
|
347
|
+
AND pin.entity_type = '${entityType}'
|
|
348
|
+
AND pin.entity_id = ${alias}.id
|
|
349
|
+
AND pin.is_pinned = 1
|
|
350
|
+
) AS is_pinned`,
|
|
351
|
+
orderBy: "is_pinned DESC, user_is_favorite DESC",
|
|
352
|
+
params: []
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
columns: `
|
|
357
|
+
EXISTS (
|
|
358
|
+
SELECT 1 FROM work_entity_favorites favorite
|
|
359
|
+
WHERE favorite.work_id = ${alias}.work_id
|
|
360
|
+
AND favorite.entity_type = '${entityType}'
|
|
361
|
+
AND favorite.entity_id = ${alias}.id
|
|
362
|
+
AND favorite.user_id = ?
|
|
363
|
+
AND favorite.is_favorite = 1
|
|
364
|
+
) AS user_is_favorite,
|
|
365
|
+
EXISTS (
|
|
366
|
+
SELECT 1 FROM work_entity_pins pin
|
|
367
|
+
WHERE pin.work_id = ${alias}.work_id
|
|
368
|
+
AND pin.entity_type = '${entityType}'
|
|
369
|
+
AND pin.entity_id = ${alias}.id
|
|
370
|
+
AND pin.is_pinned = 1
|
|
371
|
+
) AS is_pinned`,
|
|
372
|
+
orderBy: "is_pinned DESC, user_is_favorite DESC",
|
|
373
|
+
params: [actor.userId]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
mapEntityFavorite(row) {
|
|
377
|
+
return Object.hasOwn(row, "user_is_favorite")
|
|
378
|
+
? booleanValue(row, "user_is_favorite")
|
|
379
|
+
: booleanValue(row, "is_favorite");
|
|
380
|
+
}
|
|
381
|
+
mapEntityPin(row) {
|
|
382
|
+
return booleanValue(row, "is_pinned");
|
|
383
|
+
}
|
|
384
|
+
setEntityFavorite(workId, entityType, entityId, isFavorite, legacyFavorite) {
|
|
385
|
+
const actor = currentRequestActor();
|
|
386
|
+
const previousFavorite = actor
|
|
387
|
+
? this.db.get("SELECT is_favorite FROM work_entity_favorites WHERE work_id = ? AND entity_type = ? AND entity_id = ? AND user_id = ?", workId, entityType, entityId, actor.userId)
|
|
388
|
+
: undefined;
|
|
389
|
+
const previous = actor ? booleanValue(previousFavorite ?? {}, "is_favorite") : legacyFavorite;
|
|
390
|
+
if (previous === isFavorite)
|
|
391
|
+
return previous;
|
|
392
|
+
const timestamp = now();
|
|
393
|
+
if (actor) {
|
|
394
|
+
this.db.run(`INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
396
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id)
|
|
397
|
+
DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, workId, entityType, entityId, actor.userId, isFavorite ? 1 : 0, timestamp, timestamp);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.db.run(`UPDATE ${legacyFavoriteTables[entityType]} SET is_favorite = ? WHERE id = ?`, isFavorite ? 1 : 0, entityId);
|
|
401
|
+
}
|
|
402
|
+
return previous;
|
|
403
|
+
}
|
|
404
|
+
setEntityPin(workId, entityType, entityId, isPinned) {
|
|
405
|
+
const previousPin = this.db.get("SELECT is_pinned FROM work_entity_pins WHERE work_id = ? AND entity_type = ? AND entity_id = ?", workId, entityType, entityId);
|
|
406
|
+
const previous = booleanValue(previousPin ?? {}, "is_pinned");
|
|
407
|
+
if (previous === isPinned)
|
|
408
|
+
return previous;
|
|
409
|
+
const timestamp = now();
|
|
410
|
+
this.db.run(`INSERT INTO work_entity_pins (work_id, entity_type, entity_id, is_pinned, pinned_by_user_id, created_at, updated_at)
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
412
|
+
ON CONFLICT(work_id, entity_type, entity_id)
|
|
413
|
+
DO UPDATE SET is_pinned = excluded.is_pinned, pinned_by_user_id = excluded.pinned_by_user_id, updated_at = excluded.updated_at`, workId, entityType, entityId, isPinned ? 1 : 0, currentRequestActor()?.userId ?? null, timestamp, timestamp);
|
|
414
|
+
return previous;
|
|
415
|
+
}
|
|
332
416
|
purgeExpiredRecycleBin(referenceTime = new Date()) {
|
|
333
417
|
const cutoff = new Date(referenceTime.getTime() - RECYCLE_BIN_RETENTION_DAYS * 24 * 60 * 60_000).toISOString();
|
|
334
418
|
return this.db.transaction(() => {
|
|
@@ -572,8 +656,16 @@ export class Store {
|
|
|
572
656
|
const versionNo = latest ? numberValue(latest, "version_no") + 1 : 1;
|
|
573
657
|
this.db.run(`INSERT INTO entity_versions (id, work_id, entity_type, entity_id, version_no, snapshot_json, source, source_ref, change_note, created_at, created_by_user_id)
|
|
574
658
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id("entityVersion"), type === "work" ? entityId : String(entity.workId), type, entityId, versionNo, snapshotJson, source, sourceRef, changeNote.trim(), timestamp ?? now(), currentRequestActor()?.userId ?? null);
|
|
659
|
+
if (type === "setting") {
|
|
660
|
+
this.recordSyncChange(String(entity.workId), "setting", entityId, source === "delete" ? "delete" : "upsert", versionNo, timestamp);
|
|
661
|
+
}
|
|
575
662
|
return versionNo;
|
|
576
663
|
}
|
|
664
|
+
recordSyncChange(workId, entityType, entityId, operation, versionNo, timestamp) {
|
|
665
|
+
this.db.run(`INSERT INTO sync_changes (
|
|
666
|
+
work_id, entity_type, entity_id, operation, version_no, changed_by_user_id, changed_at
|
|
667
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`, workId, entityType, entityId, operation, versionNo, currentRequestActor()?.userId ?? null, timestamp ?? now());
|
|
668
|
+
}
|
|
577
669
|
backfillEntityVersionBaselines() {
|
|
578
670
|
const entities = [
|
|
579
671
|
...this.db.all("SELECT id, updated_at FROM works").map((row) => ["work", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
@@ -788,7 +880,7 @@ export class Store {
|
|
|
788
880
|
}
|
|
789
881
|
listWorks() {
|
|
790
882
|
const actor = currentRequestActor();
|
|
791
|
-
if (!actor
|
|
883
|
+
if (!actor) {
|
|
792
884
|
return this.mapWorks(this.db.all("SELECT * FROM works WHERE COALESCE(is_internal, 0) = 0 AND deleted_at IS NULL ORDER BY updated_at DESC"));
|
|
793
885
|
}
|
|
794
886
|
return this.mapWorks(this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
|
|
@@ -799,7 +891,7 @@ export class Store {
|
|
|
799
891
|
listWorksPage(pagination) {
|
|
800
892
|
const actor = currentRequestActor();
|
|
801
893
|
const page = paginationSql(pagination);
|
|
802
|
-
const rows = !actor
|
|
894
|
+
const rows = !actor
|
|
803
895
|
? this.db.all(`SELECT * FROM works WHERE COALESCE(is_internal, 0) = 0 AND deleted_at IS NULL ORDER BY updated_at DESC${page.sql}`, ...page.params)
|
|
804
896
|
: this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
|
|
805
897
|
WHERE COALESCE(work.is_internal, 0) = 0 AND work.deleted_at IS NULL
|
|
@@ -815,7 +907,7 @@ export class Store {
|
|
|
815
907
|
}
|
|
816
908
|
listDeletedWorks() {
|
|
817
909
|
const actor = currentRequestActor();
|
|
818
|
-
const actorRestricted = Boolean(actor
|
|
910
|
+
const actorRestricted = Boolean(actor);
|
|
819
911
|
const rows = this.db.all(`SELECT work.*,
|
|
820
912
|
(SELECT COUNT(*) FROM volumes volume WHERE volume.work_id = work.id) AS volume_count,
|
|
821
913
|
(SELECT COUNT(*) FROM chapters chapter WHERE chapter.work_id = work.id) AS chapter_count,
|
|
@@ -1108,6 +1200,18 @@ export class Store {
|
|
|
1108
1200
|
});
|
|
1109
1201
|
return this.getWork(workId);
|
|
1110
1202
|
}
|
|
1203
|
+
setWorkOfflineAccess(workId, enabled) {
|
|
1204
|
+
this.db.transaction(() => {
|
|
1205
|
+
const current = this.getWork(workId);
|
|
1206
|
+
if (Boolean(current.offlineAccessEnabled) === enabled)
|
|
1207
|
+
return;
|
|
1208
|
+
const timestamp = now();
|
|
1209
|
+
this.db.run("UPDATE works SET offline_access_enabled = ?, version_no = version_no + 1, updated_at = ? WHERE id = ?", enabled ? 1 : 0, timestamp, workId);
|
|
1210
|
+
this.recordEntityVersion("work", workId, "manual", null, enabled ? "允许 Desktop 离线访问" : "禁止 Desktop 离线访问", timestamp);
|
|
1211
|
+
this.audit(workId, enabled ? "work.offline-access.enabled" : "work.offline-access.disabled", "work", workId, { enabled });
|
|
1212
|
+
});
|
|
1213
|
+
return this.getWork(workId);
|
|
1214
|
+
}
|
|
1111
1215
|
deleteWork(workId, expectedVersionNo) {
|
|
1112
1216
|
return this.db.transaction(() => {
|
|
1113
1217
|
const current = this.getWork(workId);
|
|
@@ -1903,10 +2007,12 @@ export class Store {
|
|
|
1903
2007
|
};
|
|
1904
2008
|
}
|
|
1905
2009
|
insertChapterVersionRow(input) {
|
|
2010
|
+
const timestamp = input.timestamp ?? now();
|
|
1906
2011
|
this.db.run(`INSERT INTO chapter_versions (
|
|
1907
2012
|
id, work_id, chapter_id, version_no, title, content, volume_id, sort_order, chapter_type,
|
|
1908
2013
|
source, source_ref, change_note, created_at, created_by_user_id
|
|
1909
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id("chapterVersion"), input.workId, input.chapterId, input.versionNo, input.title, input.content, input.volumeId, input.sortOrder, input.chapterType, input.source, input.sourceRef, input.changeNote.trim(),
|
|
2014
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id("chapterVersion"), input.workId, input.chapterId, input.versionNo, input.title, input.content, input.volumeId, input.sortOrder, input.chapterType, input.source, input.sourceRef, input.changeNote.trim(), timestamp, currentRequestActor()?.userId ?? null);
|
|
2015
|
+
this.recordSyncChange(input.workId, "chapter", input.chapterId, input.source === "delete" ? "delete" : "upsert", input.versionNo, timestamp);
|
|
1910
2016
|
}
|
|
1911
2017
|
listChapterVersions(chapterId) {
|
|
1912
2018
|
const rows = this.findChapterVersionRows(chapterId);
|
|
@@ -1998,7 +2104,7 @@ export class Store {
|
|
|
1998
2104
|
if (!hasTextChange && !hasOtherChange)
|
|
1999
2105
|
return current;
|
|
2000
2106
|
const timestamp = now();
|
|
2001
|
-
const versionNo = Number(current.versionNo) + (hasTextChange ? 1 : 0);
|
|
2107
|
+
const versionNo = Number(current.versionNo) + (hasTextChange || hasTypeChange ? 1 : 0);
|
|
2002
2108
|
this.db.transaction(() => {
|
|
2003
2109
|
const lockedCurrent = this.getChapter(chapterId);
|
|
2004
2110
|
this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", Number(lockedCurrent.versionNo));
|
|
@@ -2006,7 +2112,9 @@ export class Store {
|
|
|
2006
2112
|
excluded_from_analysis = ?, updated_at = ? WHERE id = ?`, nextTitle, nextContent, nextChapterType, countWords(nextContent), versionNo, hasTextChange || hasTypeChange ? "expired" : String(current.analysisStatus), nextExcluded ? 1 : 0, timestamp, chapterId);
|
|
2007
2113
|
if (hasTextChange)
|
|
2008
2114
|
this.syncChapterParagraphSearch(String(current.workId), chapterId, nextContent);
|
|
2009
|
-
if (
|
|
2115
|
+
else if (hasTypeChange)
|
|
2116
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2117
|
+
if (hasTextChange || hasTypeChange) {
|
|
2010
2118
|
this.insertChapterVersionRow({
|
|
2011
2119
|
workId: String(current.workId),
|
|
2012
2120
|
chapterId,
|
|
@@ -2018,7 +2126,7 @@ export class Store {
|
|
|
2018
2126
|
chapterType: nextChapterType,
|
|
2019
2127
|
source,
|
|
2020
2128
|
sourceRef,
|
|
2021
|
-
changeNote: changeNote || "更新章节正文",
|
|
2129
|
+
changeNote: changeNote || (hasTextChange ? "更新章节正文" : "更新章节类型"),
|
|
2022
2130
|
timestamp
|
|
2023
2131
|
});
|
|
2024
2132
|
}
|
|
@@ -2530,9 +2638,28 @@ export class Store {
|
|
|
2530
2638
|
}
|
|
2531
2639
|
else if (action.type === "setType") {
|
|
2532
2640
|
for (const chapter of currentChapters) {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2641
|
+
if (chapter.chapterType === action.chapterType)
|
|
2642
|
+
continue;
|
|
2643
|
+
const chapterId = String(chapter.id);
|
|
2644
|
+
const versionNo = Number(chapter.versionNo) + 1;
|
|
2645
|
+
this.db.run("UPDATE chapters SET chapter_type = ?, version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", action.chapterType, versionNo, timestamp, chapterId);
|
|
2646
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2647
|
+
this.insertChapterVersionRow({
|
|
2648
|
+
workId,
|
|
2649
|
+
chapterId,
|
|
2650
|
+
versionNo,
|
|
2651
|
+
title: String(chapter.title),
|
|
2652
|
+
content: String(chapter.content),
|
|
2653
|
+
volumeId: String(chapter.volumeId),
|
|
2654
|
+
sortOrder: Number(chapter.sortOrder),
|
|
2655
|
+
chapterType: action.chapterType,
|
|
2656
|
+
source: "manual",
|
|
2657
|
+
sourceRef: null,
|
|
2658
|
+
changeNote: "批量更新章节类型",
|
|
2659
|
+
timestamp
|
|
2660
|
+
});
|
|
2661
|
+
this.invalidateChapter(workId, chapterId, versionNo);
|
|
2662
|
+
this.audit(workId, "chapter.saved", "chapter", chapterId, { chapterType: action.chapterType, versionNo, batch: true });
|
|
2536
2663
|
}
|
|
2537
2664
|
}
|
|
2538
2665
|
else if (action.type === "setAnalysisExclusion") {
|
|
@@ -2956,6 +3083,7 @@ export class Store {
|
|
|
2956
3083
|
? `/api/works/${encodeURIComponent(workId)}/cover?v=${encodeURIComponent(requiredString(cover, "updated_at"))}`
|
|
2957
3084
|
: optionalString(row, "cover_url"),
|
|
2958
3085
|
tags: json(requiredString(row, "tags_json"), []),
|
|
3086
|
+
offlineAccessEnabled: numberValue(row, "offline_access_enabled") === 1,
|
|
2959
3087
|
versionNo: numberValue(row, "version_no") || this.currentEntityVersionNo("work", workId),
|
|
2960
3088
|
ownerUserId,
|
|
2961
3089
|
accessRole,
|
|
@@ -3704,18 +3832,20 @@ export class Store {
|
|
|
3704
3832
|
}
|
|
3705
3833
|
listDrafts(workId, draftType, includeContent = false) {
|
|
3706
3834
|
this.getWork(workId);
|
|
3707
|
-
|
|
3835
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3836
|
+
return this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3708
3837
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3709
3838
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3710
|
-
ORDER BY
|
|
3839
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title`, ...preferences.params, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
|
|
3711
3840
|
}
|
|
3712
3841
|
listDraftsPage(workId, pagination, draftType, includeContent = false) {
|
|
3713
3842
|
this.getWork(workId);
|
|
3843
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3714
3844
|
const page = paginationSql(pagination);
|
|
3715
|
-
const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3845
|
+
const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3716
3846
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3717
3847
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3718
|
-
ORDER BY
|
|
3848
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title${page.sql}`, ...preferences.params, workId, draftType ?? null, draftType ?? null, ...page.params);
|
|
3719
3849
|
return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
|
|
3720
3850
|
}
|
|
3721
3851
|
searchDrafts(workId, query, draftType, limit = 20) {
|
|
@@ -3724,22 +3854,24 @@ export class Store {
|
|
|
3724
3854
|
const normalizedQuery = query.normalize("NFKC").trim();
|
|
3725
3855
|
const escapedQuery = escapeSqlLikePattern(normalizedQuery);
|
|
3726
3856
|
const pattern = `%${escapedQuery}%`;
|
|
3857
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3727
3858
|
const rows = normalizedQuery
|
|
3728
|
-
? this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3859
|
+
? this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3729
3860
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3730
3861
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3731
3862
|
AND (draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE OR draft.content LIKE ? ESCAPE '\\' COLLATE NOCASE)
|
|
3732
|
-
ORDER BY CASE WHEN draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, draft.updated_at DESC
|
|
3733
|
-
LIMIT ?`, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
|
|
3734
|
-
: this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3863
|
+
ORDER BY ${preferences.orderBy}, CASE WHEN draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, draft.updated_at DESC
|
|
3864
|
+
LIMIT ?`, ...preferences.params, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
|
|
3865
|
+
: this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3735
3866
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3736
3867
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3737
|
-
ORDER BY draft.updated_at DESC, draft.title LIMIT ?`, workId, draftType ?? null, draftType ?? null, safeLimit);
|
|
3868
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title LIMIT ?`, ...preferences.params, workId, draftType ?? null, draftType ?? null, safeLimit);
|
|
3738
3869
|
return rows.map((row) => this.mapDraft(row, true));
|
|
3739
3870
|
}
|
|
3740
3871
|
getDraft(draftId) {
|
|
3741
|
-
const
|
|
3742
|
-
|
|
3872
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3873
|
+
const row = this.db.get(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3874
|
+
LEFT JOIN volumes volume ON volume.id = draft.volume_id WHERE draft.id = ?`, ...preferences.params, draftId);
|
|
3743
3875
|
if (!row)
|
|
3744
3876
|
throw notFound("想法");
|
|
3745
3877
|
return this.mapDraft(row, true);
|
|
@@ -3749,13 +3881,30 @@ export class Store {
|
|
|
3749
3881
|
if (!draft)
|
|
3750
3882
|
throw notFound("想法");
|
|
3751
3883
|
const workId = requiredString(draft, "work_id");
|
|
3752
|
-
const previousFavorite = booleanValue(draft, "is_favorite");
|
|
3753
3884
|
this.db.transaction(() => {
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3885
|
+
const previousFavorite = this.setEntityFavorite(workId, "draft", draftId, isFavorite, booleanValue(draft, "is_favorite"));
|
|
3886
|
+
if (previousFavorite !== isFavorite) {
|
|
3887
|
+
this.audit(workId, "draft.favorite-updated", "draft", draftId, {
|
|
3888
|
+
previousFavorite,
|
|
3889
|
+
isFavorite
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
});
|
|
3893
|
+
return this.getDraft(draftId);
|
|
3894
|
+
}
|
|
3895
|
+
setDraftPin(draftId, isPinned) {
|
|
3896
|
+
const draft = this.db.get("SELECT id, work_id FROM drafts WHERE id = ?", draftId);
|
|
3897
|
+
if (!draft)
|
|
3898
|
+
throw notFound("想法");
|
|
3899
|
+
const workId = requiredString(draft, "work_id");
|
|
3900
|
+
this.db.transaction(() => {
|
|
3901
|
+
const previousPin = this.setEntityPin(workId, "draft", draftId, isPinned);
|
|
3902
|
+
if (previousPin !== isPinned) {
|
|
3903
|
+
this.audit(workId, "draft.pin-updated", "draft", draftId, {
|
|
3904
|
+
previousPin,
|
|
3905
|
+
isPinned
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3759
3908
|
});
|
|
3760
3909
|
return this.getDraft(draftId);
|
|
3761
3910
|
}
|
|
@@ -3797,7 +3946,8 @@ export class Store {
|
|
|
3797
3946
|
volumeTitle: optionalString(row, "volume_title"),
|
|
3798
3947
|
settingModule: optionalString(row, "setting_module"),
|
|
3799
3948
|
title: requiredString(row, "title"),
|
|
3800
|
-
isFavorite:
|
|
3949
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
3950
|
+
isPinned: this.mapEntityPin(row),
|
|
3801
3951
|
...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
|
|
3802
3952
|
versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
|
|
3803
3953
|
createdAt: requiredString(row, "created_at"),
|
|
@@ -3861,16 +4011,19 @@ export class Store {
|
|
|
3861
4011
|
}
|
|
3862
4012
|
listSettings(workId, includeContent = true) {
|
|
3863
4013
|
this.getWork(workId);
|
|
3864
|
-
|
|
4014
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
4015
|
+
return this.db.all(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.work_id = ? ORDER BY ${preferences.orderBy}, setting.locked DESC, setting.category, setting.title`, ...preferences.params, workId).map((row) => this.mapSetting(row, includeContent));
|
|
3865
4016
|
}
|
|
3866
4017
|
listSettingsPage(workId, pagination, includeContent = true) {
|
|
3867
4018
|
this.getWork(workId);
|
|
4019
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
3868
4020
|
const page = paginationSql(pagination);
|
|
3869
|
-
const rows = this.db.all(`SELECT
|
|
4021
|
+
const rows = this.db.all(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.work_id = ? ORDER BY ${preferences.orderBy}, setting.locked DESC, setting.category, setting.title${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
3870
4022
|
return paginated(rows.map((row) => this.mapSetting(row, includeContent)), pagination);
|
|
3871
4023
|
}
|
|
3872
4024
|
getSetting(settingId) {
|
|
3873
|
-
const
|
|
4025
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
4026
|
+
const row = this.db.get(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.id = ?`, ...preferences.params, settingId);
|
|
3874
4027
|
if (!row)
|
|
3875
4028
|
throw notFound("设定");
|
|
3876
4029
|
return this.mapSetting(row);
|
|
@@ -3880,13 +4033,30 @@ export class Store {
|
|
|
3880
4033
|
if (!setting)
|
|
3881
4034
|
throw notFound("设定");
|
|
3882
4035
|
const workId = requiredString(setting, "work_id");
|
|
3883
|
-
const previousFavorite = booleanValue(setting, "is_favorite");
|
|
3884
4036
|
this.db.transaction(() => {
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4037
|
+
const previousFavorite = this.setEntityFavorite(workId, "setting", settingId, isFavorite, booleanValue(setting, "is_favorite"));
|
|
4038
|
+
if (previousFavorite !== isFavorite) {
|
|
4039
|
+
this.audit(workId, "setting.favorite-updated", "setting", settingId, {
|
|
4040
|
+
previousFavorite,
|
|
4041
|
+
isFavorite
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
});
|
|
4045
|
+
return this.getSetting(settingId);
|
|
4046
|
+
}
|
|
4047
|
+
setSettingPin(settingId, isPinned) {
|
|
4048
|
+
const setting = this.db.get("SELECT id, work_id FROM settings WHERE id = ?", settingId);
|
|
4049
|
+
if (!setting)
|
|
4050
|
+
throw notFound("设定");
|
|
4051
|
+
const workId = requiredString(setting, "work_id");
|
|
4052
|
+
this.db.transaction(() => {
|
|
4053
|
+
const previousPin = this.setEntityPin(workId, "setting", settingId, isPinned);
|
|
4054
|
+
if (previousPin !== isPinned) {
|
|
4055
|
+
this.audit(workId, "setting.pin-updated", "setting", settingId, {
|
|
4056
|
+
previousPin,
|
|
4057
|
+
isPinned
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
3890
4060
|
});
|
|
3891
4061
|
return this.getSetting(settingId);
|
|
3892
4062
|
}
|
|
@@ -3924,7 +4094,8 @@ export class Store {
|
|
|
3924
4094
|
tags: json(requiredString(row, "tags_json"), []),
|
|
3925
4095
|
status: requiredString(row, "status"),
|
|
3926
4096
|
locked: booleanValue(row, "locked"),
|
|
3927
|
-
isFavorite:
|
|
4097
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
4098
|
+
isPinned: this.mapEntityPin(row),
|
|
3928
4099
|
evidence: json(requiredString(row, "evidence_json"), []),
|
|
3929
4100
|
scope: json(requiredString(row, "scope_json"), {}),
|
|
3930
4101
|
authorNote: requiredString(row, "author_note"),
|
|
@@ -4238,19 +4409,22 @@ export class Store {
|
|
|
4238
4409
|
}
|
|
4239
4410
|
listOrganizations(workId, includeMarkdown = true) {
|
|
4240
4411
|
this.getWork(workId);
|
|
4241
|
-
const
|
|
4412
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4413
|
+
const rows = this.db.all(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.work_id = ? ORDER BY ${preferences.orderBy}, organization.name`, ...preferences.params, workId);
|
|
4242
4414
|
const batch = this.organizationListBatch(rows);
|
|
4243
4415
|
return rows.map((row) => this.mapOrganization(row, includeMarkdown, batch));
|
|
4244
4416
|
}
|
|
4245
4417
|
listOrganizationsPage(workId, pagination, includeMarkdown = true) {
|
|
4246
4418
|
this.getWork(workId);
|
|
4419
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4247
4420
|
const page = paginationSql(pagination);
|
|
4248
|
-
const rows = this.db.all(`SELECT
|
|
4421
|
+
const rows = this.db.all(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.work_id = ? ORDER BY ${preferences.orderBy}, organization.name${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
4249
4422
|
const batch = this.organizationListBatch(rows);
|
|
4250
4423
|
return paginated(rows.map((row) => this.mapOrganization(row, includeMarkdown, batch)), pagination);
|
|
4251
4424
|
}
|
|
4252
4425
|
getOrganization(organizationId) {
|
|
4253
|
-
const
|
|
4426
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4427
|
+
const row = this.db.get(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.id = ?`, ...preferences.params, organizationId);
|
|
4254
4428
|
if (!row)
|
|
4255
4429
|
throw notFound("组织");
|
|
4256
4430
|
return this.mapOrganization(row);
|
|
@@ -4260,13 +4434,30 @@ export class Store {
|
|
|
4260
4434
|
if (!organization)
|
|
4261
4435
|
throw notFound("组织");
|
|
4262
4436
|
const workId = requiredString(organization, "work_id");
|
|
4263
|
-
const previousFavorite = booleanValue(organization, "is_favorite");
|
|
4264
4437
|
this.db.transaction(() => {
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4438
|
+
const previousFavorite = this.setEntityFavorite(workId, "organization", organizationId, isFavorite, booleanValue(organization, "is_favorite"));
|
|
4439
|
+
if (previousFavorite !== isFavorite) {
|
|
4440
|
+
this.audit(workId, "organization.favorite-updated", "organization", organizationId, {
|
|
4441
|
+
previousFavorite,
|
|
4442
|
+
isFavorite
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
return this.getOrganization(organizationId);
|
|
4447
|
+
}
|
|
4448
|
+
setOrganizationPin(organizationId, isPinned) {
|
|
4449
|
+
const organization = this.db.get("SELECT id, work_id FROM organizations WHERE id = ?", organizationId);
|
|
4450
|
+
if (!organization)
|
|
4451
|
+
throw notFound("组织");
|
|
4452
|
+
const workId = requiredString(organization, "work_id");
|
|
4453
|
+
this.db.transaction(() => {
|
|
4454
|
+
const previousPin = this.setEntityPin(workId, "organization", organizationId, isPinned);
|
|
4455
|
+
if (previousPin !== isPinned) {
|
|
4456
|
+
this.audit(workId, "organization.pin-updated", "organization", organizationId, {
|
|
4457
|
+
previousPin,
|
|
4458
|
+
isPinned
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4270
4461
|
});
|
|
4271
4462
|
return this.getOrganization(organizationId);
|
|
4272
4463
|
}
|
|
@@ -4397,7 +4588,8 @@ export class Store {
|
|
|
4397
4588
|
name: requiredString(row, "name"),
|
|
4398
4589
|
description: requiredString(row, "description"),
|
|
4399
4590
|
isDissolved: booleanValue(row, "is_dissolved"),
|
|
4400
|
-
isFavorite:
|
|
4591
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
4592
|
+
isPinned: this.mapEntityPin(row),
|
|
4401
4593
|
...(includeMarkdown
|
|
4402
4594
|
? { settings, settingsMarkdown: settingsMarkdownFromList(settings), settingsSections }
|
|
4403
4595
|
: { settings: [], settingsCount: settingsSections.length }),
|
|
@@ -4516,14 +4708,16 @@ export class Store {
|
|
|
4516
4708
|
}
|
|
4517
4709
|
listCharacters(workId, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4518
4710
|
this.getWork(workId);
|
|
4519
|
-
|
|
4711
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
4712
|
+
return this.db.all(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.work_id = ?${includeMerged ? "" : " AND character.merged_into_character_id IS NULL"} ORDER BY ${preferences.orderBy}, character.name`, ...preferences.params, workId)
|
|
4520
4713
|
.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown));
|
|
4521
4714
|
}
|
|
4522
4715
|
listCharactersPage(workId, pagination, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4523
4716
|
this.getWork(workId);
|
|
4717
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
4524
4718
|
const page = paginationSql(pagination);
|
|
4525
4719
|
const count = this.db.get(`SELECT COUNT(*) AS count FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"}`, workId);
|
|
4526
|
-
const rows = this.db.all(`SELECT
|
|
4720
|
+
const rows = this.db.all(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.work_id = ?${includeMerged ? "" : " AND character.merged_into_character_id IS NULL"} ORDER BY ${preferences.orderBy}, character.name${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
4527
4721
|
return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination, Number(count?.count ?? 0));
|
|
4528
4722
|
}
|
|
4529
4723
|
mapCharacterProfileSection(row) {
|
|
@@ -4999,7 +5193,8 @@ export class Store {
|
|
|
4999
5193
|
return { storageKey, cleanupQueued: !this.attachmentStorageKeyInUse(storageKey) };
|
|
5000
5194
|
}
|
|
5001
5195
|
getCharacter(characterId) {
|
|
5002
|
-
const
|
|
5196
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
5197
|
+
const row = this.db.get(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.id = ?`, ...preferences.params, characterId);
|
|
5003
5198
|
if (!row)
|
|
5004
5199
|
throw notFound("角色");
|
|
5005
5200
|
return this.mapCharacter(row);
|
|
@@ -5012,15 +5207,33 @@ export class Store {
|
|
|
5012
5207
|
throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能收藏");
|
|
5013
5208
|
}
|
|
5014
5209
|
const workId = requiredString(character, "work_id");
|
|
5015
|
-
const previousFavorite = booleanValue(character, "is_favorite");
|
|
5016
|
-
if (previousFavorite === isFavorite)
|
|
5017
|
-
return this.getCharacter(characterId);
|
|
5018
5210
|
this.db.transaction(() => {
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5211
|
+
const previousFavorite = this.setEntityFavorite(workId, "character", characterId, isFavorite, booleanValue(character, "is_favorite"));
|
|
5212
|
+
if (previousFavorite !== isFavorite) {
|
|
5213
|
+
this.audit(workId, "character.favorite-updated", "character", characterId, {
|
|
5214
|
+
previousFavorite,
|
|
5215
|
+
isFavorite
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5218
|
+
});
|
|
5219
|
+
return this.getCharacter(characterId);
|
|
5220
|
+
}
|
|
5221
|
+
setCharacterPin(characterId, isPinned) {
|
|
5222
|
+
const character = this.db.get("SELECT id, work_id, merged_into_character_id FROM characters WHERE id = ?", characterId);
|
|
5223
|
+
if (!character)
|
|
5224
|
+
throw notFound("角色");
|
|
5225
|
+
if (optionalString(character, "merged_into_character_id")) {
|
|
5226
|
+
throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能置顶");
|
|
5227
|
+
}
|
|
5228
|
+
const workId = requiredString(character, "work_id");
|
|
5229
|
+
this.db.transaction(() => {
|
|
5230
|
+
const previousPin = this.setEntityPin(workId, "character", characterId, isPinned);
|
|
5231
|
+
if (previousPin !== isPinned) {
|
|
5232
|
+
this.audit(workId, "character.pin-updated", "character", characterId, {
|
|
5233
|
+
previousPin,
|
|
5234
|
+
isPinned
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5024
5237
|
});
|
|
5025
5238
|
return this.getCharacter(characterId);
|
|
5026
5239
|
}
|
|
@@ -5298,7 +5511,8 @@ export class Store {
|
|
|
5298
5511
|
profileSectionCount,
|
|
5299
5512
|
currentState: json(requiredString(row, "current_state_json"), {}),
|
|
5300
5513
|
isDead: booleanValue(row, "is_dead"),
|
|
5301
|
-
isFavorite:
|
|
5514
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
5515
|
+
isPinned: this.mapEntityPin(row),
|
|
5302
5516
|
avatarUrl: avatarSha256
|
|
5303
5517
|
? `/api/characters/${encodeURIComponent(characterId)}/avatar?v=${encodeURIComponent(avatarSha256)}`
|
|
5304
5518
|
: null,
|
|
@@ -6967,8 +7181,9 @@ export class Store {
|
|
|
6967
7181
|
scope.targetCharacters = targetCharacters;
|
|
6968
7182
|
}
|
|
6969
7183
|
const sourceVersions = this.analysisTaskSourceVersions(workId, scope);
|
|
6970
|
-
|
|
6971
|
-
|
|
7184
|
+
const actor = currentRequestActor();
|
|
7185
|
+
this.db.run(`INSERT INTO analysis_tasks (id, work_id, model_id, task_type, scope_json, status, source_versions_json, created_at, updated_at, created_by_user_id, created_via_api_key)
|
|
7186
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`, taskId, workId, input.modelId ?? null, input.taskType, JSON.stringify(scope), JSON.stringify(sourceVersions), timestamp, timestamp, actor?.userId ?? null, actor?.authentication === "api-key" ? 1 : 0);
|
|
6972
7187
|
this.audit(workId, "task.created", "analysis-task", taskId, {
|
|
6973
7188
|
taskType: input.taskType,
|
|
6974
7189
|
scope,
|