@musnows/scriverse 0.8.7 → 0.9.0
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/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai-analysis-timeout.js +12 -0
- package/dist/ai-analysis-timeout.js.map +1 -0
- package/dist/ai.js +943 -222
- package/dist/ai.js.map +1 -1
- package/dist/app.js +340 -28
- package/dist/app.js.map +1 -1
- package/dist/database.js +220 -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/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +470 -76
- package/dist/public/index.html +24 -11
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +76 -17
- package/dist/public/toast-layer.d.ts +4 -0
- package/dist/public/toast-layer.js +5 -0
- package/dist/roleplay-turn.js +98 -0
- package/dist/roleplay-turn.js.map +1 -0
- package/dist/security.js +36 -9
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +13 -3
- 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 +152 -24
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +194 -35
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -13,6 +13,7 @@ import { countWords, documentShortSearchTerms, escapeSqlLikePattern, id, json, n
|
|
|
13
13
|
import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
|
|
14
14
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
15
15
|
import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
16
|
+
import { normalizeRoleplayScenePin, roleplayUserTurnDisplayText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
|
|
16
17
|
const WORK_LIST_BATCH_SIZE = 500;
|
|
17
18
|
const ENTITY_LIST_BATCH_SIZE = 400;
|
|
18
19
|
export const RECYCLE_BIN_RETENTION_DAYS = 30;
|
|
@@ -211,7 +212,7 @@ export const versionedEntityTypes = [
|
|
|
211
212
|
export const AI_CONVERSATION_STREAM_REQUEST_LEASE_MS = 3 * 60_000;
|
|
212
213
|
export const aiConversationTaskTypes = ["chat", "roleplay", "continue", "polish"];
|
|
213
214
|
export function defaultAiConversationTitle(prompt) {
|
|
214
|
-
const normalized = prompt.replace(/\s+/gu, " ").trim();
|
|
215
|
+
const normalized = roleplayUserTurnTitleSource(prompt).replace(/\s+/gu, " ").trim();
|
|
215
216
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
216
217
|
}
|
|
217
218
|
function isRecord(value) {
|
|
@@ -571,8 +572,16 @@ export class Store {
|
|
|
571
572
|
const versionNo = latest ? numberValue(latest, "version_no") + 1 : 1;
|
|
572
573
|
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)
|
|
573
574
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id("entityVersion"), type === "work" ? entityId : String(entity.workId), type, entityId, versionNo, snapshotJson, source, sourceRef, changeNote.trim(), timestamp ?? now(), currentRequestActor()?.userId ?? null);
|
|
575
|
+
if (type === "setting") {
|
|
576
|
+
this.recordSyncChange(String(entity.workId), "setting", entityId, source === "delete" ? "delete" : "upsert", versionNo, timestamp);
|
|
577
|
+
}
|
|
574
578
|
return versionNo;
|
|
575
579
|
}
|
|
580
|
+
recordSyncChange(workId, entityType, entityId, operation, versionNo, timestamp) {
|
|
581
|
+
this.db.run(`INSERT INTO sync_changes (
|
|
582
|
+
work_id, entity_type, entity_id, operation, version_no, changed_by_user_id, changed_at
|
|
583
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`, workId, entityType, entityId, operation, versionNo, currentRequestActor()?.userId ?? null, timestamp ?? now());
|
|
584
|
+
}
|
|
576
585
|
backfillEntityVersionBaselines() {
|
|
577
586
|
const entities = [
|
|
578
587
|
...this.db.all("SELECT id, updated_at FROM works").map((row) => ["work", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
@@ -787,7 +796,7 @@ export class Store {
|
|
|
787
796
|
}
|
|
788
797
|
listWorks() {
|
|
789
798
|
const actor = currentRequestActor();
|
|
790
|
-
if (!actor
|
|
799
|
+
if (!actor) {
|
|
791
800
|
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"));
|
|
792
801
|
}
|
|
793
802
|
return this.mapWorks(this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
|
|
@@ -798,7 +807,7 @@ export class Store {
|
|
|
798
807
|
listWorksPage(pagination) {
|
|
799
808
|
const actor = currentRequestActor();
|
|
800
809
|
const page = paginationSql(pagination);
|
|
801
|
-
const rows = !actor
|
|
810
|
+
const rows = !actor
|
|
802
811
|
? 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)
|
|
803
812
|
: this.db.all(`SELECT DISTINCT work.* FROM works work LEFT JOIN work_memberships membership ON membership.work_id = work.id
|
|
804
813
|
WHERE COALESCE(work.is_internal, 0) = 0 AND work.deleted_at IS NULL
|
|
@@ -814,7 +823,7 @@ export class Store {
|
|
|
814
823
|
}
|
|
815
824
|
listDeletedWorks() {
|
|
816
825
|
const actor = currentRequestActor();
|
|
817
|
-
const actorRestricted = Boolean(actor
|
|
826
|
+
const actorRestricted = Boolean(actor);
|
|
818
827
|
const rows = this.db.all(`SELECT work.*,
|
|
819
828
|
(SELECT COUNT(*) FROM volumes volume WHERE volume.work_id = work.id) AS volume_count,
|
|
820
829
|
(SELECT COUNT(*) FROM chapters chapter WHERE chapter.work_id = work.id) AS chapter_count,
|
|
@@ -1107,6 +1116,18 @@ export class Store {
|
|
|
1107
1116
|
});
|
|
1108
1117
|
return this.getWork(workId);
|
|
1109
1118
|
}
|
|
1119
|
+
setWorkOfflineAccess(workId, enabled) {
|
|
1120
|
+
this.db.transaction(() => {
|
|
1121
|
+
const current = this.getWork(workId);
|
|
1122
|
+
if (Boolean(current.offlineAccessEnabled) === enabled)
|
|
1123
|
+
return;
|
|
1124
|
+
const timestamp = now();
|
|
1125
|
+
this.db.run("UPDATE works SET offline_access_enabled = ?, version_no = version_no + 1, updated_at = ? WHERE id = ?", enabled ? 1 : 0, timestamp, workId);
|
|
1126
|
+
this.recordEntityVersion("work", workId, "manual", null, enabled ? "允许 Desktop 离线访问" : "禁止 Desktop 离线访问", timestamp);
|
|
1127
|
+
this.audit(workId, enabled ? "work.offline-access.enabled" : "work.offline-access.disabled", "work", workId, { enabled });
|
|
1128
|
+
});
|
|
1129
|
+
return this.getWork(workId);
|
|
1130
|
+
}
|
|
1110
1131
|
deleteWork(workId, expectedVersionNo) {
|
|
1111
1132
|
return this.db.transaction(() => {
|
|
1112
1133
|
const current = this.getWork(workId);
|
|
@@ -1902,10 +1923,12 @@ export class Store {
|
|
|
1902
1923
|
};
|
|
1903
1924
|
}
|
|
1904
1925
|
insertChapterVersionRow(input) {
|
|
1926
|
+
const timestamp = input.timestamp ?? now();
|
|
1905
1927
|
this.db.run(`INSERT INTO chapter_versions (
|
|
1906
1928
|
id, work_id, chapter_id, version_no, title, content, volume_id, sort_order, chapter_type,
|
|
1907
1929
|
source, source_ref, change_note, created_at, created_by_user_id
|
|
1908
|
-
) 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(),
|
|
1930
|
+
) 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);
|
|
1931
|
+
this.recordSyncChange(input.workId, "chapter", input.chapterId, input.source === "delete" ? "delete" : "upsert", input.versionNo, timestamp);
|
|
1909
1932
|
}
|
|
1910
1933
|
listChapterVersions(chapterId) {
|
|
1911
1934
|
const rows = this.findChapterVersionRows(chapterId);
|
|
@@ -1997,7 +2020,7 @@ export class Store {
|
|
|
1997
2020
|
if (!hasTextChange && !hasOtherChange)
|
|
1998
2021
|
return current;
|
|
1999
2022
|
const timestamp = now();
|
|
2000
|
-
const versionNo = Number(current.versionNo) + (hasTextChange ? 1 : 0);
|
|
2023
|
+
const versionNo = Number(current.versionNo) + (hasTextChange || hasTypeChange ? 1 : 0);
|
|
2001
2024
|
this.db.transaction(() => {
|
|
2002
2025
|
const lockedCurrent = this.getChapter(chapterId);
|
|
2003
2026
|
this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", Number(lockedCurrent.versionNo));
|
|
@@ -2005,7 +2028,9 @@ export class Store {
|
|
|
2005
2028
|
excluded_from_analysis = ?, updated_at = ? WHERE id = ?`, nextTitle, nextContent, nextChapterType, countWords(nextContent), versionNo, hasTextChange || hasTypeChange ? "expired" : String(current.analysisStatus), nextExcluded ? 1 : 0, timestamp, chapterId);
|
|
2006
2029
|
if (hasTextChange)
|
|
2007
2030
|
this.syncChapterParagraphSearch(String(current.workId), chapterId, nextContent);
|
|
2008
|
-
if (
|
|
2031
|
+
else if (hasTypeChange)
|
|
2032
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2033
|
+
if (hasTextChange || hasTypeChange) {
|
|
2009
2034
|
this.insertChapterVersionRow({
|
|
2010
2035
|
workId: String(current.workId),
|
|
2011
2036
|
chapterId,
|
|
@@ -2017,7 +2042,7 @@ export class Store {
|
|
|
2017
2042
|
chapterType: nextChapterType,
|
|
2018
2043
|
source,
|
|
2019
2044
|
sourceRef,
|
|
2020
|
-
changeNote: changeNote || "更新章节正文",
|
|
2045
|
+
changeNote: changeNote || (hasTextChange ? "更新章节正文" : "更新章节类型"),
|
|
2021
2046
|
timestamp
|
|
2022
2047
|
});
|
|
2023
2048
|
}
|
|
@@ -2529,9 +2554,28 @@ export class Store {
|
|
|
2529
2554
|
}
|
|
2530
2555
|
else if (action.type === "setType") {
|
|
2531
2556
|
for (const chapter of currentChapters) {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2557
|
+
if (chapter.chapterType === action.chapterType)
|
|
2558
|
+
continue;
|
|
2559
|
+
const chapterId = String(chapter.id);
|
|
2560
|
+
const versionNo = Number(chapter.versionNo) + 1;
|
|
2561
|
+
this.db.run("UPDATE chapters SET chapter_type = ?, version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", action.chapterType, versionNo, timestamp, chapterId);
|
|
2562
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2563
|
+
this.insertChapterVersionRow({
|
|
2564
|
+
workId,
|
|
2565
|
+
chapterId,
|
|
2566
|
+
versionNo,
|
|
2567
|
+
title: String(chapter.title),
|
|
2568
|
+
content: String(chapter.content),
|
|
2569
|
+
volumeId: String(chapter.volumeId),
|
|
2570
|
+
sortOrder: Number(chapter.sortOrder),
|
|
2571
|
+
chapterType: action.chapterType,
|
|
2572
|
+
source: "manual",
|
|
2573
|
+
sourceRef: null,
|
|
2574
|
+
changeNote: "批量更新章节类型",
|
|
2575
|
+
timestamp
|
|
2576
|
+
});
|
|
2577
|
+
this.invalidateChapter(workId, chapterId, versionNo);
|
|
2578
|
+
this.audit(workId, "chapter.saved", "chapter", chapterId, { chapterType: action.chapterType, versionNo, batch: true });
|
|
2535
2579
|
}
|
|
2536
2580
|
}
|
|
2537
2581
|
else if (action.type === "setAnalysisExclusion") {
|
|
@@ -2955,6 +2999,7 @@ export class Store {
|
|
|
2955
2999
|
? `/api/works/${encodeURIComponent(workId)}/cover?v=${encodeURIComponent(requiredString(cover, "updated_at"))}`
|
|
2956
3000
|
: optionalString(row, "cover_url"),
|
|
2957
3001
|
tags: json(requiredString(row, "tags_json"), []),
|
|
3002
|
+
offlineAccessEnabled: numberValue(row, "offline_access_enabled") === 1,
|
|
2958
3003
|
versionNo: numberValue(row, "version_no") || this.currentEntityVersionNo("work", workId),
|
|
2959
3004
|
ownerUserId,
|
|
2960
3005
|
accessRole,
|
|
@@ -3706,7 +3751,7 @@ export class Store {
|
|
|
3706
3751
|
return this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3707
3752
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3708
3753
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3709
|
-
ORDER BY draft.updated_at DESC, draft.title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
|
|
3754
|
+
ORDER BY draft.is_favorite DESC, draft.updated_at DESC, draft.title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
|
|
3710
3755
|
}
|
|
3711
3756
|
listDraftsPage(workId, pagination, draftType, includeContent = false) {
|
|
3712
3757
|
this.getWork(workId);
|
|
@@ -3714,7 +3759,7 @@ export class Store {
|
|
|
3714
3759
|
const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3715
3760
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3716
3761
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3717
|
-
ORDER BY draft.updated_at DESC, draft.title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
|
|
3762
|
+
ORDER BY draft.is_favorite DESC, draft.updated_at DESC, draft.title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
|
|
3718
3763
|
return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
|
|
3719
3764
|
}
|
|
3720
3765
|
searchDrafts(workId, query, draftType, limit = 20) {
|
|
@@ -3743,6 +3788,21 @@ export class Store {
|
|
|
3743
3788
|
throw notFound("想法");
|
|
3744
3789
|
return this.mapDraft(row, true);
|
|
3745
3790
|
}
|
|
3791
|
+
setDraftFavorite(draftId, isFavorite) {
|
|
3792
|
+
const draft = this.db.get("SELECT id, work_id, is_favorite FROM drafts WHERE id = ?", draftId);
|
|
3793
|
+
if (!draft)
|
|
3794
|
+
throw notFound("想法");
|
|
3795
|
+
const workId = requiredString(draft, "work_id");
|
|
3796
|
+
const previousFavorite = booleanValue(draft, "is_favorite");
|
|
3797
|
+
this.db.transaction(() => {
|
|
3798
|
+
this.db.run("UPDATE drafts SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, draftId);
|
|
3799
|
+
this.audit(workId, "draft.favorite-updated", "draft", draftId, {
|
|
3800
|
+
previousFavorite,
|
|
3801
|
+
isFavorite
|
|
3802
|
+
});
|
|
3803
|
+
});
|
|
3804
|
+
return this.getDraft(draftId);
|
|
3805
|
+
}
|
|
3746
3806
|
updateDraft(draftId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
|
|
3747
3807
|
const current = this.getDraft(draftId);
|
|
3748
3808
|
const content = input.content ?? String(current.content);
|
|
@@ -3781,6 +3841,7 @@ export class Store {
|
|
|
3781
3841
|
volumeTitle: optionalString(row, "volume_title"),
|
|
3782
3842
|
settingModule: optionalString(row, "setting_module"),
|
|
3783
3843
|
title: requiredString(row, "title"),
|
|
3844
|
+
isFavorite: booleanValue(row, "is_favorite"),
|
|
3784
3845
|
...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
|
|
3785
3846
|
versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
|
|
3786
3847
|
createdAt: requiredString(row, "created_at"),
|
|
@@ -3844,12 +3905,12 @@ export class Store {
|
|
|
3844
3905
|
}
|
|
3845
3906
|
listSettings(workId, includeContent = true) {
|
|
3846
3907
|
this.getWork(workId);
|
|
3847
|
-
return this.db.all("SELECT * FROM settings WHERE work_id = ? ORDER BY locked DESC, category, title", workId).map((row) => this.mapSetting(row, includeContent));
|
|
3908
|
+
return this.db.all("SELECT * FROM settings WHERE work_id = ? ORDER BY is_favorite DESC, locked DESC, category, title", workId).map((row) => this.mapSetting(row, includeContent));
|
|
3848
3909
|
}
|
|
3849
3910
|
listSettingsPage(workId, pagination, includeContent = true) {
|
|
3850
3911
|
this.getWork(workId);
|
|
3851
3912
|
const page = paginationSql(pagination);
|
|
3852
|
-
const rows = this.db.all(`SELECT * FROM settings WHERE work_id = ? ORDER BY locked DESC, category, title${page.sql}`, workId, ...page.params);
|
|
3913
|
+
const rows = this.db.all(`SELECT * FROM settings WHERE work_id = ? ORDER BY is_favorite DESC, locked DESC, category, title${page.sql}`, workId, ...page.params);
|
|
3853
3914
|
return paginated(rows.map((row) => this.mapSetting(row, includeContent)), pagination);
|
|
3854
3915
|
}
|
|
3855
3916
|
getSetting(settingId) {
|
|
@@ -3858,6 +3919,21 @@ export class Store {
|
|
|
3858
3919
|
throw notFound("设定");
|
|
3859
3920
|
return this.mapSetting(row);
|
|
3860
3921
|
}
|
|
3922
|
+
setSettingFavorite(settingId, isFavorite) {
|
|
3923
|
+
const setting = this.db.get("SELECT id, work_id, is_favorite FROM settings WHERE id = ?", settingId);
|
|
3924
|
+
if (!setting)
|
|
3925
|
+
throw notFound("设定");
|
|
3926
|
+
const workId = requiredString(setting, "work_id");
|
|
3927
|
+
const previousFavorite = booleanValue(setting, "is_favorite");
|
|
3928
|
+
this.db.transaction(() => {
|
|
3929
|
+
this.db.run("UPDATE settings SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, settingId);
|
|
3930
|
+
this.audit(workId, "setting.favorite-updated", "setting", settingId, {
|
|
3931
|
+
previousFavorite,
|
|
3932
|
+
isFavorite
|
|
3933
|
+
});
|
|
3934
|
+
});
|
|
3935
|
+
return this.getSetting(settingId);
|
|
3936
|
+
}
|
|
3861
3937
|
updateSetting(settingId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
|
|
3862
3938
|
const current = this.getSetting(settingId);
|
|
3863
3939
|
const content = input.content ?? String(current.content);
|
|
@@ -3892,6 +3968,7 @@ export class Store {
|
|
|
3892
3968
|
tags: json(requiredString(row, "tags_json"), []),
|
|
3893
3969
|
status: requiredString(row, "status"),
|
|
3894
3970
|
locked: booleanValue(row, "locked"),
|
|
3971
|
+
isFavorite: booleanValue(row, "is_favorite"),
|
|
3895
3972
|
evidence: json(requiredString(row, "evidence_json"), []),
|
|
3896
3973
|
scope: json(requiredString(row, "scope_json"), {}),
|
|
3897
3974
|
authorNote: requiredString(row, "author_note"),
|
|
@@ -4205,14 +4282,14 @@ export class Store {
|
|
|
4205
4282
|
}
|
|
4206
4283
|
listOrganizations(workId, includeMarkdown = true) {
|
|
4207
4284
|
this.getWork(workId);
|
|
4208
|
-
const rows = this.db.all("SELECT * FROM organizations WHERE work_id = ? ORDER BY name", workId);
|
|
4285
|
+
const rows = this.db.all("SELECT * FROM organizations WHERE work_id = ? ORDER BY is_favorite DESC, name", workId);
|
|
4209
4286
|
const batch = this.organizationListBatch(rows);
|
|
4210
4287
|
return rows.map((row) => this.mapOrganization(row, includeMarkdown, batch));
|
|
4211
4288
|
}
|
|
4212
4289
|
listOrganizationsPage(workId, pagination, includeMarkdown = true) {
|
|
4213
4290
|
this.getWork(workId);
|
|
4214
4291
|
const page = paginationSql(pagination);
|
|
4215
|
-
const rows = this.db.all(`SELECT * FROM organizations WHERE work_id = ? ORDER BY name${page.sql}`, workId, ...page.params);
|
|
4292
|
+
const rows = this.db.all(`SELECT * FROM organizations WHERE work_id = ? ORDER BY is_favorite DESC, name${page.sql}`, workId, ...page.params);
|
|
4216
4293
|
const batch = this.organizationListBatch(rows);
|
|
4217
4294
|
return paginated(rows.map((row) => this.mapOrganization(row, includeMarkdown, batch)), pagination);
|
|
4218
4295
|
}
|
|
@@ -4222,6 +4299,21 @@ export class Store {
|
|
|
4222
4299
|
throw notFound("组织");
|
|
4223
4300
|
return this.mapOrganization(row);
|
|
4224
4301
|
}
|
|
4302
|
+
setOrganizationFavorite(organizationId, isFavorite) {
|
|
4303
|
+
const organization = this.db.get("SELECT id, work_id, is_favorite FROM organizations WHERE id = ?", organizationId);
|
|
4304
|
+
if (!organization)
|
|
4305
|
+
throw notFound("组织");
|
|
4306
|
+
const workId = requiredString(organization, "work_id");
|
|
4307
|
+
const previousFavorite = booleanValue(organization, "is_favorite");
|
|
4308
|
+
this.db.transaction(() => {
|
|
4309
|
+
this.db.run("UPDATE organizations SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, organizationId);
|
|
4310
|
+
this.audit(workId, "organization.favorite-updated", "organization", organizationId, {
|
|
4311
|
+
previousFavorite,
|
|
4312
|
+
isFavorite
|
|
4313
|
+
});
|
|
4314
|
+
});
|
|
4315
|
+
return this.getOrganization(organizationId);
|
|
4316
|
+
}
|
|
4225
4317
|
updateOrganization(organizationId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
|
|
4226
4318
|
const current = this.getOrganization(organizationId);
|
|
4227
4319
|
const workId = String(current.workId);
|
|
@@ -4349,6 +4441,7 @@ export class Store {
|
|
|
4349
4441
|
name: requiredString(row, "name"),
|
|
4350
4442
|
description: requiredString(row, "description"),
|
|
4351
4443
|
isDissolved: booleanValue(row, "is_dissolved"),
|
|
4444
|
+
isFavorite: booleanValue(row, "is_favorite"),
|
|
4352
4445
|
...(includeMarkdown
|
|
4353
4446
|
? { settings, settingsMarkdown: settingsMarkdownFromList(settings), settingsSections }
|
|
4354
4447
|
: { settings: [], settingsCount: settingsSections.length }),
|
|
@@ -4467,14 +4560,14 @@ export class Store {
|
|
|
4467
4560
|
}
|
|
4468
4561
|
listCharacters(workId, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4469
4562
|
this.getWork(workId);
|
|
4470
|
-
return this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY name`, workId)
|
|
4563
|
+
return this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY is_favorite DESC, name`, workId)
|
|
4471
4564
|
.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown));
|
|
4472
4565
|
}
|
|
4473
4566
|
listCharactersPage(workId, pagination, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4474
4567
|
this.getWork(workId);
|
|
4475
4568
|
const page = paginationSql(pagination);
|
|
4476
4569
|
const count = this.db.get(`SELECT COUNT(*) AS count FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"}`, workId);
|
|
4477
|
-
const rows = this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY name${page.sql}`, workId, ...page.params);
|
|
4570
|
+
const rows = this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY is_favorite DESC, name${page.sql}`, workId, ...page.params);
|
|
4478
4571
|
return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination, Number(count?.count ?? 0));
|
|
4479
4572
|
}
|
|
4480
4573
|
mapCharacterProfileSection(row) {
|
|
@@ -4955,6 +5048,26 @@ export class Store {
|
|
|
4955
5048
|
throw notFound("角色");
|
|
4956
5049
|
return this.mapCharacter(row);
|
|
4957
5050
|
}
|
|
5051
|
+
setCharacterFavorite(characterId, isFavorite) {
|
|
5052
|
+
const character = this.db.get("SELECT id, work_id, is_favorite, merged_into_character_id FROM characters WHERE id = ?", characterId);
|
|
5053
|
+
if (!character)
|
|
5054
|
+
throw notFound("角色");
|
|
5055
|
+
if (optionalString(character, "merged_into_character_id")) {
|
|
5056
|
+
throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能收藏");
|
|
5057
|
+
}
|
|
5058
|
+
const workId = requiredString(character, "work_id");
|
|
5059
|
+
const previousFavorite = booleanValue(character, "is_favorite");
|
|
5060
|
+
if (previousFavorite === isFavorite)
|
|
5061
|
+
return this.getCharacter(characterId);
|
|
5062
|
+
this.db.transaction(() => {
|
|
5063
|
+
this.db.run("UPDATE characters SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, characterId);
|
|
5064
|
+
this.audit(workId, "character.favorite-updated", "character", characterId, {
|
|
5065
|
+
previousFavorite,
|
|
5066
|
+
isFavorite
|
|
5067
|
+
});
|
|
5068
|
+
});
|
|
5069
|
+
return this.getCharacter(characterId);
|
|
5070
|
+
}
|
|
4958
5071
|
getCharacterAvatar(characterId) {
|
|
4959
5072
|
const character = this.db.get("SELECT id FROM characters WHERE id = ?", characterId);
|
|
4960
5073
|
if (!character)
|
|
@@ -5229,6 +5342,7 @@ export class Store {
|
|
|
5229
5342
|
profileSectionCount,
|
|
5230
5343
|
currentState: json(requiredString(row, "current_state_json"), {}),
|
|
5231
5344
|
isDead: booleanValue(row, "is_dead"),
|
|
5345
|
+
isFavorite: booleanValue(row, "is_favorite"),
|
|
5232
5346
|
avatarUrl: avatarSha256
|
|
5233
5347
|
? `/api/characters/${encodeURIComponent(characterId)}/avatar?v=${encodeURIComponent(avatarSha256)}`
|
|
5234
5348
|
: null,
|
|
@@ -6053,6 +6167,7 @@ export class Store {
|
|
|
6053
6167
|
totalMessageCount,
|
|
6054
6168
|
warningPending: Boolean(optionalString(conversation, "context_warning_at")),
|
|
6055
6169
|
injectedEntities: parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES),
|
|
6170
|
+
scenePin: normalizeRoleplayScenePin(json(optionalString(conversation, "scene_pin_json") ?? "{}", {})),
|
|
6056
6171
|
messages: rows.filter((message) => requiredString(message, "id") !== excludeMessageId)
|
|
6057
6172
|
.map((message) => ({
|
|
6058
6173
|
id: requiredString(message, "id"),
|
|
@@ -6134,6 +6249,16 @@ export class Store {
|
|
|
6134
6249
|
const refreshed = this.db.get("SELECT system_clock_text FROM ai_conversations WHERE id = ?", conversationId);
|
|
6135
6250
|
return (optionalString(refreshed ?? {}, "system_clock_text") ?? clock).trim() || clock;
|
|
6136
6251
|
}
|
|
6252
|
+
setAiConversationScenePin(conversationId, workId, pin) {
|
|
6253
|
+
const conversation = this.db.get("SELECT work_id FROM ai_conversations WHERE id = ?", conversationId);
|
|
6254
|
+
if (!conversation)
|
|
6255
|
+
throw notFound("AI 对话");
|
|
6256
|
+
if (requiredString(conversation, "work_id") !== workId)
|
|
6257
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
6258
|
+
const normalized = normalizeRoleplayScenePin(pin);
|
|
6259
|
+
this.db.run("UPDATE ai_conversations SET scene_pin_json = ?, updated_at = ? WHERE id = ?", JSON.stringify(normalized), now(), conversationId);
|
|
6260
|
+
return normalized;
|
|
6261
|
+
}
|
|
6137
6262
|
listCharacterNameEntries(workId) {
|
|
6138
6263
|
this.getWork(workId);
|
|
6139
6264
|
return this.db.all(`SELECT character_id, normalized_name, display_name, kind FROM character_names
|
|
@@ -6577,10 +6702,11 @@ export class Store {
|
|
|
6577
6702
|
const injectedEntitiesJson = optionalString(conversation, "injected_entities_json")
|
|
6578
6703
|
?? JSON.stringify(EMPTY_AI_INJECTED_ENTITIES);
|
|
6579
6704
|
const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
|
|
6705
|
+
const scenePinJson = optionalString(conversation, "scene_pin_json") ?? "{}";
|
|
6580
6706
|
this.db.transaction(() => {
|
|
6581
|
-
this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
6707
|
+
this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, scene_pin_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
6582
6708
|
? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
|
|
6583
|
-
: String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
6709
|
+
: String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, scenePinJson, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
6584
6710
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
6585
6711
|
const role = requiredString(message, "role");
|
|
6586
6712
|
const inheritedMetadata = json(requiredString(message, "metadata_json"), {});
|
|
@@ -6647,7 +6773,7 @@ export class Store {
|
|
|
6647
6773
|
title: requiredString(row, "title"),
|
|
6648
6774
|
isFavorite: booleanValue(row, "is_favorite"),
|
|
6649
6775
|
messageCount: numberValue(row, "message_count"),
|
|
6650
|
-
preview: requiredString(row, "preview"),
|
|
6776
|
+
preview: roleplayUserTurnDisplayText(requiredString(row, "preview")),
|
|
6651
6777
|
compactedMessageCount: numberValue(row, "compacted_message_count"),
|
|
6652
6778
|
hasCompactedSummary: Boolean(requiredString(row, "compacted_summary")),
|
|
6653
6779
|
contextWarningPending: Boolean(optionalString(row, "context_warning_at")),
|
|
@@ -6655,6 +6781,7 @@ export class Store {
|
|
|
6655
6781
|
...(lockedModelId ? { modelId: lockedModelId } : {}),
|
|
6656
6782
|
...(hasImageAttachments ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
|
|
6657
6783
|
contextScope: json(optionalString(row, "context_scope_json") ?? "", { type: "none" }),
|
|
6784
|
+
scenePin: normalizeRoleplayScenePin(json(optionalString(row, "scene_pin_json") ?? "{}", {})),
|
|
6658
6785
|
roleplayCharacter: roleplayCharacter ? {
|
|
6659
6786
|
id: requiredString(roleplayCharacter, "id"),
|
|
6660
6787
|
name: requiredString(roleplayCharacter, "name"),
|
|
@@ -6884,8 +7011,9 @@ export class Store {
|
|
|
6884
7011
|
scope.targetCharacters = targetCharacters;
|
|
6885
7012
|
}
|
|
6886
7013
|
const sourceVersions = this.analysisTaskSourceVersions(workId, scope);
|
|
6887
|
-
|
|
6888
|
-
|
|
7014
|
+
const actor = currentRequestActor();
|
|
7015
|
+
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)
|
|
7016
|
+
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);
|
|
6889
7017
|
this.audit(workId, "task.created", "analysis-task", taskId, {
|
|
6890
7018
|
taskType: input.taskType,
|
|
6891
7019
|
scope,
|