@musnows/scriverse 0.7.13 → 0.8.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 +4 -0
- package/README.md +5 -0
- package/dist/ai-chat-tab-limit.js +14 -0
- package/dist/ai-chat-tab-limit.js.map +1 -0
- package/dist/ai-protocol.js +10 -2
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-retry.js +52 -0
- package/dist/ai-retry.js.map +1 -0
- package/dist/ai.js +525 -117
- package/dist/ai.js.map +1 -1
- package/dist/app.js +41 -12
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +2 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/database.js +127 -2
- package/dist/database.js.map +1 -1
- package/dist/domain.js +1 -0
- package/dist/domain.js.map +1 -1
- package/dist/public/ai-chat-tabs.js +75 -0
- package/dist/public/ai-request-manager.js +32 -15
- package/dist/public/app.js +1151 -252
- package/dist/public/background-task-center.d.ts +9 -0
- package/dist/public/background-task-center.js +18 -0
- package/dist/public/chapter-search.d.ts +6 -0
- package/dist/public/chapter-search.js +23 -0
- package/dist/public/character-filters.d.ts +3 -1
- package/dist/public/character-filters.js +17 -2
- package/dist/public/character-version.js +1 -0
- package/dist/public/display-labels.d.ts +1 -0
- package/dist/public/display-labels.js +5 -1
- package/dist/public/index.html +66 -25
- package/dist/public/model-config.d.ts +3 -1
- package/dist/public/model-config.js +13 -0
- package/dist/public/relationship-filters.d.ts +5 -0
- package/dist/public/relationship-filters.js +31 -0
- package/dist/public/relationship-graph.js +289 -58
- package/dist/public/styles.css +170 -32
- package/dist/server-runtime.js +6 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +77 -34
- package/dist/store.js.map +1 -1
- package/dist/version.js +6 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DRAFT_SETTING_MODULES } from "./domain.js";
|
|
1
|
+
import { CHARACTER_GENDERS, DRAFT_SETTING_MODULES } from "./domain.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID } from "./database.js";
|
|
4
4
|
import { exportWorkDocx } from "./docx-export.js";
|
|
@@ -26,9 +26,19 @@ export const WORK_AGENT_TOOL_IDS = [
|
|
|
26
26
|
"search_story_entities",
|
|
27
27
|
"read_character_sections",
|
|
28
28
|
"search_drafts",
|
|
29
|
-
"image"
|
|
29
|
+
"image",
|
|
30
|
+
"calculate_time"
|
|
30
31
|
];
|
|
31
32
|
const DEFAULT_WORK_AGENT_TOOLS = [...WORK_AGENT_TOOL_IDS];
|
|
33
|
+
const LEGACY_DEFAULT_WORK_AGENT_TOOLS = [
|
|
34
|
+
"story_index",
|
|
35
|
+
"read_chapters",
|
|
36
|
+
"grep",
|
|
37
|
+
"search_story_entities",
|
|
38
|
+
"read_character_sections",
|
|
39
|
+
"search_drafts",
|
|
40
|
+
"image"
|
|
41
|
+
];
|
|
32
42
|
export function normalizeWorkAgentTools(value) {
|
|
33
43
|
const source = Array.isArray(value)
|
|
34
44
|
? value
|
|
@@ -43,6 +53,8 @@ export function normalizeWorkAgentTools(value) {
|
|
|
43
53
|
if (WORK_AGENT_TOOL_IDS.includes(toolId))
|
|
44
54
|
enabled.add(toolId);
|
|
45
55
|
}
|
|
56
|
+
if (LEGACY_DEFAULT_WORK_AGENT_TOOLS.every((toolId) => enabled.has(toolId)))
|
|
57
|
+
enabled.add("calculate_time");
|
|
46
58
|
return WORK_AGENT_TOOL_IDS.filter((toolId) => enabled.has(toolId));
|
|
47
59
|
}
|
|
48
60
|
const galaxyFrameRates = [24, 30, 60, 90, 120, 144, 165, 240];
|
|
@@ -204,6 +216,11 @@ export function defaultAiConversationTitle(prompt) {
|
|
|
204
216
|
function isRecord(value) {
|
|
205
217
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
206
218
|
}
|
|
219
|
+
function characterGender(value) {
|
|
220
|
+
return typeof value === "string" && CHARACTER_GENDERS.includes(value)
|
|
221
|
+
? value
|
|
222
|
+
: "unknown";
|
|
223
|
+
}
|
|
207
224
|
function invalidFileSnapshot() {
|
|
208
225
|
throw new AppError(409, "FILE_VERSION_INVALID", "正文历史快照已损坏,未执行恢复");
|
|
209
226
|
}
|
|
@@ -869,10 +886,14 @@ export class Store {
|
|
|
869
886
|
return this.getPlatformUiSettings();
|
|
870
887
|
}
|
|
871
888
|
analysisTaskQueuedHandler = null;
|
|
889
|
+
chapterAnalysisInvalidatedHandler = null;
|
|
872
890
|
relationshipIndexQueuedHandler = null;
|
|
873
891
|
setAnalysisTaskQueuedHandler(handler) {
|
|
874
892
|
this.analysisTaskQueuedHandler = handler;
|
|
875
893
|
}
|
|
894
|
+
setChapterAnalysisInvalidatedHandler(handler) {
|
|
895
|
+
this.chapterAnalysisInvalidatedHandler = handler;
|
|
896
|
+
}
|
|
876
897
|
setRelationshipIndexQueuedHandler(handler) {
|
|
877
898
|
this.relationshipIndexQueuedHandler = handler;
|
|
878
899
|
}
|
|
@@ -884,6 +905,14 @@ export class Store {
|
|
|
884
905
|
// 自动运行调度失败不影响主写入路径
|
|
885
906
|
}
|
|
886
907
|
}
|
|
908
|
+
notifyChapterAnalysisInvalidated(workId, chapterId, versionNo) {
|
|
909
|
+
try {
|
|
910
|
+
this.chapterAnalysisInvalidatedHandler?.(workId, chapterId, versionNo);
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// 稳定等待调度失败不影响主写入路径
|
|
914
|
+
}
|
|
915
|
+
}
|
|
887
916
|
getWorkAiSettings(workId) {
|
|
888
917
|
this.getWork(workId);
|
|
889
918
|
const row = this.db.get("SELECT * FROM work_ai_settings WHERE work_id = ?", workId);
|
|
@@ -899,6 +928,7 @@ export class Store {
|
|
|
899
928
|
autoRunBatchLimit: Math.min(200, Math.max(1, Number(row?.auto_run_batch_limit ?? 20) || 20)),
|
|
900
929
|
autoRunDailyTaskLimit: Math.min(10_000, Math.max(0, Number(row?.auto_run_daily_task_limit ?? 0) || 0)),
|
|
901
930
|
autoRunFailureThreshold: Math.min(10, Math.max(1, Number(row?.auto_run_failure_threshold ?? 3) || 3)),
|
|
931
|
+
autoRunStabilityDelayMinutes: Math.min(120, Math.max(1, Number(row?.auto_run_stability_delay_minutes ?? 2) || 2)),
|
|
902
932
|
autoRunPaused: Number(row?.auto_run_paused ?? 0) === 1,
|
|
903
933
|
autoRunPauseReason: String(row?.auto_run_pause_reason ?? ""),
|
|
904
934
|
autoRunResumeAt: row?.auto_run_resume_at === null || row?.auto_run_resume_at === undefined ? null : String(row.auto_run_resume_at),
|
|
@@ -933,6 +963,7 @@ export class Store {
|
|
|
933
963
|
const nextBatchLimit = input.autoRunBatchLimit ?? Number(current.autoRunBatchLimit);
|
|
934
964
|
const nextDailyTaskLimit = input.autoRunDailyTaskLimit ?? Number(current.autoRunDailyTaskLimit);
|
|
935
965
|
const nextFailureThreshold = input.autoRunFailureThreshold ?? Number(current.autoRunFailureThreshold);
|
|
966
|
+
const nextStabilityDelayMinutes = input.autoRunStabilityDelayMinutes ?? Number(current.autoRunStabilityDelayMinutes);
|
|
936
967
|
const nextBookSummaryContextPercent = input.bookSummaryContextPercent ?? Number(current.bookSummaryContextPercent);
|
|
937
968
|
const nextContextCompactThreshold = input.contextCompactThreshold ?? Number(current.contextCompactThreshold);
|
|
938
969
|
const nextAgentToolCallLimit = input.agentToolCallLimit ?? Number(current.agentToolCallLimit);
|
|
@@ -947,11 +978,11 @@ export class Store {
|
|
|
947
978
|
: input.titleGenerationModelId?.trim() || null;
|
|
948
979
|
this.db.run(`INSERT INTO work_ai_settings (
|
|
949
980
|
work_id, system_prompt, daily_token_quota, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
|
|
950
|
-
auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
|
|
981
|
+
auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_stability_delay_minutes, auto_run_paused, auto_run_pause_reason,
|
|
951
982
|
auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
|
|
952
983
|
context_compact_threshold, agent_tool_call_limit, agent_tool_call_global_multiplier,
|
|
953
984
|
agent_tools_json, title_generation_model_id, image_tool_model_id, always_include_setting_info, updated_at
|
|
954
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
985
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
955
986
|
ON CONFLICT(work_id) DO UPDATE SET
|
|
956
987
|
system_prompt = excluded.system_prompt,
|
|
957
988
|
daily_token_quota = excluded.daily_token_quota,
|
|
@@ -960,6 +991,7 @@ export class Store {
|
|
|
960
991
|
auto_run_batch_limit = excluded.auto_run_batch_limit,
|
|
961
992
|
auto_run_daily_task_limit = excluded.auto_run_daily_task_limit,
|
|
962
993
|
auto_run_failure_threshold = excluded.auto_run_failure_threshold,
|
|
994
|
+
auto_run_stability_delay_minutes = excluded.auto_run_stability_delay_minutes,
|
|
963
995
|
auto_run_paused = excluded.auto_run_paused,
|
|
964
996
|
auto_run_pause_reason = excluded.auto_run_pause_reason,
|
|
965
997
|
auto_run_resume_at = excluded.auto_run_resume_at,
|
|
@@ -972,7 +1004,7 @@ export class Store {
|
|
|
972
1004
|
title_generation_model_id = excluded.title_generation_model_id,
|
|
973
1005
|
image_tool_model_id = excluded.image_tool_model_id,
|
|
974
1006
|
always_include_setting_info = excluded.always_include_setting_info,
|
|
975
|
-
updated_at = excluded.updated_at`, workId, nextPrompt, nextDailyTokenQuota, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), Math.min(maximumAgentToolCallLimit, Math.max(5, nextAgentToolCallLimit)), Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, nextImageToolModelId, nextAlwaysIncludeSettingInfo ? 1 : 0, timestamp);
|
|
1007
|
+
updated_at = excluded.updated_at`, workId, nextPrompt, nextDailyTokenQuota, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), Math.min(120, Math.max(1, nextStabilityDelayMinutes)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), Math.min(maximumAgentToolCallLimit, Math.max(5, nextAgentToolCallLimit)), Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, nextImageToolModelId, nextAlwaysIncludeSettingInfo ? 1 : 0, timestamp);
|
|
976
1008
|
this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
|
|
977
1009
|
systemPromptChanged: input.systemPrompt !== undefined,
|
|
978
1010
|
dailyTokenQuota: nextDailyTokenQuota,
|
|
@@ -981,6 +1013,7 @@ export class Store {
|
|
|
981
1013
|
autoRunBatchLimit: Math.min(200, Math.max(1, nextBatchLimit)),
|
|
982
1014
|
autoRunDailyTaskLimit: Math.min(10_000, Math.max(0, nextDailyTaskLimit)),
|
|
983
1015
|
autoRunFailureThreshold: Math.min(10, Math.max(1, nextFailureThreshold)),
|
|
1016
|
+
autoRunStabilityDelayMinutes: Math.min(120, Math.max(1, nextStabilityDelayMinutes)),
|
|
984
1017
|
bookSummaryContextPercent: Math.min(90, Math.max(1, nextBookSummaryContextPercent)),
|
|
985
1018
|
contextCompactThreshold: Math.min(90, Math.max(50, nextContextCompactThreshold)),
|
|
986
1019
|
agentToolCallLimit: Math.min(maximumAgentToolCallLimit, Math.max(5, nextAgentToolCallLimit)),
|
|
@@ -1851,6 +1884,16 @@ export class Store {
|
|
|
1851
1884
|
throw new AppError(400, "REPLACE_SCOPE_INVALID", "替换范围无效");
|
|
1852
1885
|
}
|
|
1853
1886
|
const work = this.getWork(workId);
|
|
1887
|
+
const volumeId = input.volumeId ?? null;
|
|
1888
|
+
if (volumeId) {
|
|
1889
|
+
const volume = this.getVolume(volumeId);
|
|
1890
|
+
if (String(volume.workId) !== workId) {
|
|
1891
|
+
throw new AppError(400, "REPLACE_VOLUME_INVALID", "分卷不属于当前作品");
|
|
1892
|
+
}
|
|
1893
|
+
if (input.scope === "settings") {
|
|
1894
|
+
throw new AppError(400, "REPLACE_VOLUME_SCOPE_INVALID", "分卷范围只能用于正文替换");
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1854
1897
|
const permissions = work.modulePermissions;
|
|
1855
1898
|
const requestedProse = input.scope === "prose" || input.scope === "prose-and-settings";
|
|
1856
1899
|
const requestedSettings = input.scope === "settings" || input.scope === "prose-and-settings";
|
|
@@ -1887,7 +1930,9 @@ export class Store {
|
|
|
1887
1930
|
};
|
|
1888
1931
|
this.db.transaction(() => {
|
|
1889
1932
|
if (includeProse) {
|
|
1890
|
-
const chapters =
|
|
1933
|
+
const chapters = volumeId
|
|
1934
|
+
? this.db.all("SELECT id, content FROM chapters WHERE work_id = ? AND volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", workId, volumeId)
|
|
1935
|
+
: this.db.all("SELECT id, content FROM chapters WHERE work_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", workId);
|
|
1891
1936
|
for (const row of chapters) {
|
|
1892
1937
|
const chapterId = requiredString(row, "id");
|
|
1893
1938
|
const result = replaceLiteral(requiredString(row, "content"));
|
|
@@ -1916,6 +1961,7 @@ export class Store {
|
|
|
1916
1961
|
this.audit(workId, "work.global-replace", "work", workId, {
|
|
1917
1962
|
operationId,
|
|
1918
1963
|
scope: input.scope,
|
|
1964
|
+
volumeId,
|
|
1919
1965
|
chapterCount,
|
|
1920
1966
|
settingCount,
|
|
1921
1967
|
totalMatches,
|
|
@@ -1927,6 +1973,7 @@ export class Store {
|
|
|
1927
1973
|
return {
|
|
1928
1974
|
operationId,
|
|
1929
1975
|
scope: input.scope,
|
|
1976
|
+
volumeId,
|
|
1930
1977
|
chapterCount,
|
|
1931
1978
|
settingCount,
|
|
1932
1979
|
totalMatches,
|
|
@@ -2485,8 +2532,6 @@ export class Store {
|
|
|
2485
2532
|
invalidateChapter(workId, chapterId, versionNo) {
|
|
2486
2533
|
this.db.run(`UPDATE analysis_tasks SET status = 'expired', updated_at = ?
|
|
2487
2534
|
WHERE work_id = ? AND status IN ('pending', 'running', 'completed', 'partial', 'review')
|
|
2488
|
-
AND NOT (status = 'pending' AND task_type = 'chapter-analysis'
|
|
2489
|
-
AND json_extract(scope_json, '$.chapterId') = ?)
|
|
2490
2535
|
AND (json_extract(scope_json, '$.chapterId') = ?
|
|
2491
2536
|
OR EXISTS (SELECT 1 FROM json_each(scope_json, '$.chapterIds') WHERE json_each.value = ?)
|
|
2492
2537
|
OR json_extract(scope_json, '$.type') = 'book'
|
|
@@ -2495,18 +2540,8 @@ export class Store {
|
|
|
2495
2540
|
OR EXISTS (
|
|
2496
2541
|
SELECT 1 FROM json_each(scope_json, '$.volumeIds')
|
|
2497
2542
|
WHERE json_each.value = (SELECT volume_id FROM chapters WHERE id = ?)
|
|
2498
|
-
))))`, now(), workId, chapterId, chapterId, chapterId, chapterId
|
|
2499
|
-
|
|
2500
|
-
AND json_extract(scope_json, '$.chapterId') = ?`, workId, chapterId);
|
|
2501
|
-
if (!existing) {
|
|
2502
|
-
const timestamp = now();
|
|
2503
|
-
this.db.run(`INSERT INTO analysis_tasks (id, work_id, task_type, scope_json, status, source_versions_json, created_at, updated_at, created_by_user_id)
|
|
2504
|
-
VALUES (?, ?, 'chapter-analysis', ?, 'pending', ?, ?, ?, ?)`, id("task"), workId, JSON.stringify({ type: "chapter", chapterId }), JSON.stringify({ [chapterId]: versionNo }), timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
2505
|
-
}
|
|
2506
|
-
else {
|
|
2507
|
-
this.db.run("UPDATE analysis_tasks SET source_versions_json = ?, updated_at = ? WHERE id = ?", JSON.stringify({ [chapterId]: versionNo }), now(), requiredString(existing, "id"));
|
|
2508
|
-
}
|
|
2509
|
-
this.notifyAnalysisTaskQueued(workId);
|
|
2543
|
+
))))`, now(), workId, chapterId, chapterId, chapterId, chapterId);
|
|
2544
|
+
this.notifyChapterAnalysisInvalidated(workId, chapterId, versionNo);
|
|
2510
2545
|
}
|
|
2511
2546
|
mapWorks(rows) {
|
|
2512
2547
|
if (rows.length === 0)
|
|
@@ -4021,6 +4056,7 @@ export class Store {
|
|
|
4021
4056
|
delete profile.sections;
|
|
4022
4057
|
return {
|
|
4023
4058
|
name: String(character.name),
|
|
4059
|
+
gender: characterGender(character.gender),
|
|
4024
4060
|
isDead: Boolean(character.isDead),
|
|
4025
4061
|
code: String(character.code),
|
|
4026
4062
|
aliases: [...character.aliases],
|
|
@@ -4074,9 +4110,9 @@ export class Store {
|
|
|
4074
4110
|
const organizationIds = [...new Set(input.organizationIds ?? [])];
|
|
4075
4111
|
this.assertOrganizationsInWork(workId, organizationIds);
|
|
4076
4112
|
this.db.transaction(() => {
|
|
4077
|
-
this.db.run(`INSERT INTO characters (id, work_id, name, code, aliases_json, species, race_id, attributes_json, profile_json, current_state_json,
|
|
4113
|
+
this.db.run(`INSERT INTO characters (id, work_id, name, code, gender, aliases_json, species, race_id, attributes_json, profile_json, current_state_json,
|
|
4078
4114
|
is_dead, locked_fields_json, first_chapter_id, created_at, updated_at)
|
|
4079
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, characterId, workId, names.name, input.code?.trim() ?? "", JSON.stringify(names.aliases), species, raceId, JSON.stringify(input.attributes ?? {}), JSON.stringify(input.profile ?? {}), JSON.stringify(input.currentState ?? {}), input.isDead ? 1 : 0, JSON.stringify(input.lockedFields ?? []), input.firstChapterId ?? null, timestamp, timestamp);
|
|
4115
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, characterId, workId, names.name, input.code?.trim() ?? "", input.gender ?? "unknown", JSON.stringify(names.aliases), species, raceId, JSON.stringify(input.attributes ?? {}), JSON.stringify(input.profile ?? {}), JSON.stringify(input.currentState ?? {}), input.isDead ? 1 : 0, JSON.stringify(input.lockedFields ?? []), input.firstChapterId ?? null, timestamp, timestamp);
|
|
4080
4116
|
this.insertCharacterNames(workId, characterId, names.entries);
|
|
4081
4117
|
this.replaceCharacterOrganizations(characterId, organizationIds);
|
|
4082
4118
|
this.insertCharacterVersion(characterId, 1, source, sourceRef, changeNote, timestamp);
|
|
@@ -4311,7 +4347,8 @@ export class Store {
|
|
|
4311
4347
|
searchCharacterProfileSections(workId, query, limit = 20) {
|
|
4312
4348
|
this.getWork(workId);
|
|
4313
4349
|
const normalized = normalizeDocumentSearchText(query);
|
|
4314
|
-
const columns = `SELECT section.*, character.name AS character_name, character.
|
|
4350
|
+
const columns = `SELECT section.*, character.name AS character_name, character.gender AS character_gender,
|
|
4351
|
+
character.is_dead AS character_is_dead
|
|
4315
4352
|
FROM character_profile_section_search search
|
|
4316
4353
|
JOIN character_profile_sections section ON section.id = search.section_id
|
|
4317
4354
|
JOIN characters character ON character.id = search.character_id`;
|
|
@@ -4326,6 +4363,7 @@ export class Store {
|
|
|
4326
4363
|
return rows.map((row) => ({
|
|
4327
4364
|
...this.mapCharacterProfileSection(row),
|
|
4328
4365
|
characterName: requiredString(row, "character_name"),
|
|
4366
|
+
gender: requiredString(row, "character_gender"),
|
|
4329
4367
|
isDead: booleanValue(row, "character_is_dead")
|
|
4330
4368
|
}));
|
|
4331
4369
|
}
|
|
@@ -4553,8 +4591,8 @@ export class Store {
|
|
|
4553
4591
|
this.db.transaction(() => {
|
|
4554
4592
|
const lockedCurrent = this.getCharacter(characterId);
|
|
4555
4593
|
this.assertExpectedRevision("character", characterId, expectedVersionNo, "人物", Number(lockedCurrent.versionNo));
|
|
4556
|
-
this.db.run(`UPDATE characters SET name = ?, code = ?, aliases_json = ?, species = ?, race_id = ?, attributes_json = ?, profile_json = ?, current_state_json = ?,
|
|
4557
|
-
is_dead = ?, locked_fields_json = ?, first_chapter_id = ?, updated_at = ? WHERE id = ?`, names.name, input.code === undefined ? String(current.code) : input.code.trim(), JSON.stringify(names.aliases), species, raceId, JSON.stringify(attributes), JSON.stringify(input.profile ?? current.profile), JSON.stringify(input.currentState ?? current.currentState), input.isDead === undefined ? (current.isDead ? 1 : 0) : (input.isDead ? 1 : 0), JSON.stringify(input.lockedFields ?? current.lockedFields), input.firstChapterId === undefined ? current.firstChapterId : input.firstChapterId, now(), characterId);
|
|
4594
|
+
this.db.run(`UPDATE characters SET name = ?, code = ?, gender = ?, aliases_json = ?, species = ?, race_id = ?, attributes_json = ?, profile_json = ?, current_state_json = ?,
|
|
4595
|
+
is_dead = ?, locked_fields_json = ?, first_chapter_id = ?, updated_at = ? WHERE id = ?`, names.name, input.code === undefined ? String(current.code) : input.code.trim(), input.gender ?? characterGender(current.gender), JSON.stringify(names.aliases), species, raceId, JSON.stringify(attributes), JSON.stringify(input.profile ?? current.profile), JSON.stringify(input.currentState ?? current.currentState), input.isDead === undefined ? (current.isDead ? 1 : 0) : (input.isDead ? 1 : 0), JSON.stringify(input.lockedFields ?? current.lockedFields), input.firstChapterId === undefined ? current.firstChapterId : input.firstChapterId, now(), characterId);
|
|
4558
4596
|
this.db.run("DELETE FROM character_names WHERE character_id = ?", characterId);
|
|
4559
4597
|
this.insertCharacterNames(workId, characterId, names.entries);
|
|
4560
4598
|
if (organizationIds)
|
|
@@ -4623,7 +4661,7 @@ export class Store {
|
|
|
4623
4661
|
this.assertExpectedRevision("character", characterId, expectedVersionNo, "人物", this.currentCharacterVersionNo(characterId));
|
|
4624
4662
|
return this.recreateCharacterFromVersion(characterId, version, snapshot, versionNo);
|
|
4625
4663
|
}
|
|
4626
|
-
return this.updateCharacter(characterId, { ...snapshot, isDead: snapshot.isDead ?? false, code: snapshot.code ?? "" }, "restore", requiredString(version, "id"), `恢复至 v${versionNo}`, expectedVersionNo);
|
|
4664
|
+
return this.updateCharacter(characterId, { ...snapshot, gender: snapshot.gender ?? "unknown", isDead: snapshot.isDead ?? false, code: snapshot.code ?? "" }, "restore", requiredString(version, "id"), `恢复至 v${versionNo}`, expectedVersionNo);
|
|
4627
4665
|
}
|
|
4628
4666
|
recreateCharacterFromVersion(characterId, version, snapshot, versionNo) {
|
|
4629
4667
|
const workId = requiredString(version, "work_id");
|
|
@@ -4640,9 +4678,9 @@ export class Store {
|
|
|
4640
4678
|
const timestamp = now();
|
|
4641
4679
|
const nextVersionNo = numberValue(this.db.get("SELECT COALESCE(MAX(version_no), 0) AS version_no FROM character_versions WHERE character_id = ?", characterId) ?? {}, "version_no") + 1;
|
|
4642
4680
|
this.db.transaction(() => {
|
|
4643
|
-
this.db.run(`INSERT INTO characters (id, work_id, name, code, aliases_json, species, race_id, attributes_json, profile_json, current_state_json,
|
|
4681
|
+
this.db.run(`INSERT INTO characters (id, work_id, name, code, gender, aliases_json, species, race_id, attributes_json, profile_json, current_state_json,
|
|
4644
4682
|
is_dead, locked_fields_json, first_chapter_id, version_no, created_at, updated_at)
|
|
4645
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, characterId, workId, names.name, snapshot.code ?? "", JSON.stringify(names.aliases), species, raceId, JSON.stringify(snapshot.attributes ?? {}), JSON.stringify(snapshot.profile ?? {}), JSON.stringify(snapshot.currentState ?? {}), snapshot.isDead ? 1 : 0, JSON.stringify(snapshot.lockedFields ?? []), snapshot.firstChapterId ?? null, nextVersionNo, timestamp, timestamp);
|
|
4683
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, characterId, workId, names.name, snapshot.code ?? "", snapshot.gender ?? "unknown", JSON.stringify(names.aliases), species, raceId, JSON.stringify(snapshot.attributes ?? {}), JSON.stringify(snapshot.profile ?? {}), JSON.stringify(snapshot.currentState ?? {}), snapshot.isDead ? 1 : 0, JSON.stringify(snapshot.lockedFields ?? []), snapshot.firstChapterId ?? null, nextVersionNo, timestamp, timestamp);
|
|
4646
4684
|
this.insertCharacterNames(workId, characterId, names.entries);
|
|
4647
4685
|
this.replaceCharacterOrganizations(characterId, organizationIds);
|
|
4648
4686
|
this.insertCharacterVersion(characterId, nextVersionNo, "restore", requiredString(version, "id"), `恢复至 v${versionNo}`, timestamp, workId);
|
|
@@ -4719,6 +4757,7 @@ export class Store {
|
|
|
4719
4757
|
workId: requiredString(row, "work_id"),
|
|
4720
4758
|
name: requiredString(row, "name"),
|
|
4721
4759
|
code: requiredString(row, "code"),
|
|
4760
|
+
gender: characterGender(row.gender),
|
|
4722
4761
|
aliases: indexedAliases.length > 0 ? indexedAliases : json(requiredString(row, "aliases_json"), []),
|
|
4723
4762
|
raceId: race ? String(race.id) : null,
|
|
4724
4763
|
race: race ? {
|
|
@@ -7375,15 +7414,16 @@ export class Store {
|
|
|
7375
7414
|
}
|
|
7376
7415
|
taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames, includeCharacterNames = true) {
|
|
7377
7416
|
const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
|
|
7417
|
+
const chapterSettingsLabel = scope.includeAllSettings === true ? " + 设定集" : "";
|
|
7378
7418
|
if (Array.isArray(scope.chapterIds) && scope.chapterIds.length > 0) {
|
|
7379
7419
|
const labels = scope.chapterIds
|
|
7380
7420
|
.filter((chapterId) => typeof chapterId === "string")
|
|
7381
7421
|
.map((chapterId) => chapterSummaries.get(chapterId) ?? "章节已删除");
|
|
7382
7422
|
const preview = labels.slice(0, 3).join("、");
|
|
7383
|
-
return
|
|
7423
|
+
return `指定章节${chapterSettingsLabel}(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
|
|
7384
7424
|
}
|
|
7385
7425
|
if (typeof scope.chapterId === "string")
|
|
7386
|
-
return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${targetedSuffix}`;
|
|
7426
|
+
return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${chapterSettingsLabel}${targetedSuffix}`;
|
|
7387
7427
|
if (Array.isArray(scope.volumeIds) && scope.volumeIds.length > 0) {
|
|
7388
7428
|
const labels = scope.volumeIds
|
|
7389
7429
|
.filter((volumeId) => typeof volumeId === "string")
|
|
@@ -7409,6 +7449,7 @@ export class Store {
|
|
|
7409
7449
|
}
|
|
7410
7450
|
taskScopeSummary(workId, scope, characterNames, includeCharacterNames = true) {
|
|
7411
7451
|
const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
|
|
7452
|
+
const chapterSettingsLabel = scope.includeAllSettings === true ? " + 设定集" : "";
|
|
7412
7453
|
if (Array.isArray(scope.chapterIds) && scope.chapterIds.length > 0) {
|
|
7413
7454
|
const labels = scope.chapterIds.map((chapterId) => {
|
|
7414
7455
|
const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
|
|
@@ -7418,7 +7459,7 @@ export class Store {
|
|
|
7418
7459
|
return chapter ? `${requiredString(chapter, "volume_title")} · ${requiredString(chapter, "title")}` : "章节已删除";
|
|
7419
7460
|
});
|
|
7420
7461
|
const preview = labels.slice(0, 3).join("、");
|
|
7421
|
-
return
|
|
7462
|
+
return `指定章节${chapterSettingsLabel}(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
|
|
7422
7463
|
}
|
|
7423
7464
|
if (typeof scope.chapterId === "string") {
|
|
7424
7465
|
const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
|
|
@@ -7426,10 +7467,10 @@ export class Store {
|
|
|
7426
7467
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
7427
7468
|
WHERE chapter.id = ? AND chapter.work_id = ? AND chapter.deleted_at IS NULL`, scope.chapterId, workId);
|
|
7428
7469
|
if (!chapter)
|
|
7429
|
-
return `章节已删除${targetedSuffix}`;
|
|
7470
|
+
return `章节已删除${chapterSettingsLabel}${targetedSuffix}`;
|
|
7430
7471
|
const title = requiredString(chapter, "title");
|
|
7431
7472
|
const volumeTitle = requiredString(chapter, "volume_title");
|
|
7432
|
-
return `${volumeTitle} · ${title}${targetedSuffix}`;
|
|
7473
|
+
return `${volumeTitle} · ${title}${chapterSettingsLabel}${targetedSuffix}`;
|
|
7433
7474
|
}
|
|
7434
7475
|
if (Array.isArray(scope.volumeIds) && scope.volumeIds.length > 0) {
|
|
7435
7476
|
const labels = scope.volumeIds.map((volumeId) => {
|
|
@@ -7668,7 +7709,7 @@ export class Store {
|
|
|
7668
7709
|
), character_race_paths AS (
|
|
7669
7710
|
SELECT character_id, path FROM character_race_lineage WHERE parent_race_id IS NULL
|
|
7670
7711
|
)
|
|
7671
|
-
SELECT character.id, character.name, character.aliases_json, character.species, character.is_dead,
|
|
7712
|
+
SELECT character.id, character.name, character.aliases_json, character.species, character.gender, character.is_dead,
|
|
7672
7713
|
COALESCE(path.path, character.species) AS race_path
|
|
7673
7714
|
FROM characters character LEFT JOIN character_race_paths path ON path.character_id = character.id
|
|
7674
7715
|
WHERE character.work_id = ? AND character.merged_into_character_id IS NULL AND (
|
|
@@ -7691,6 +7732,7 @@ export class Store {
|
|
|
7691
7732
|
title: requiredString(row, "name"),
|
|
7692
7733
|
snippet: [requiredString(row, "race_path"), ...json(requiredString(row, "aliases_json"), [])].filter(Boolean).join("、"),
|
|
7693
7734
|
racePath: requiredString(row, "race_path"),
|
|
7735
|
+
gender: requiredString(row, "gender"),
|
|
7694
7736
|
isDead: booleanValue(row, "is_dead")
|
|
7695
7737
|
})),
|
|
7696
7738
|
...characterSections.map((section) => ({
|
|
@@ -7700,6 +7742,7 @@ export class Store {
|
|
|
7700
7742
|
title: `${String(section.characterName)} / ${String(section.title)}`,
|
|
7701
7743
|
snippet: snippet(String(section.contentMarkdown)),
|
|
7702
7744
|
sectionType: String(section.sectionType),
|
|
7745
|
+
gender: String(section.gender),
|
|
7703
7746
|
isDead: Boolean(section.isDead)
|
|
7704
7747
|
})),
|
|
7705
7748
|
...settings.map((row) => ({ type: "setting", id: requiredString(row, "id"), title: requiredString(row, "title"), snippet: snippet(requiredString(row, "content")), category: requiredString(row, "category") })),
|