@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.
Files changed (44) hide show
  1. package/README.en.md +4 -0
  2. package/README.md +5 -0
  3. package/dist/ai-chat-tab-limit.js +14 -0
  4. package/dist/ai-chat-tab-limit.js.map +1 -0
  5. package/dist/ai-protocol.js +10 -2
  6. package/dist/ai-protocol.js.map +1 -1
  7. package/dist/ai-retry.js +52 -0
  8. package/dist/ai-retry.js.map +1 -0
  9. package/dist/ai.js +525 -117
  10. package/dist/ai.js.map +1 -1
  11. package/dist/app.js +41 -12
  12. package/dist/app.js.map +1 -1
  13. package/dist/cli-contract.js +2 -1
  14. package/dist/cli-contract.js.map +1 -1
  15. package/dist/database.js +127 -2
  16. package/dist/database.js.map +1 -1
  17. package/dist/domain.js +1 -0
  18. package/dist/domain.js.map +1 -1
  19. package/dist/public/ai-chat-tabs.js +75 -0
  20. package/dist/public/ai-request-manager.js +32 -15
  21. package/dist/public/app.js +1151 -252
  22. package/dist/public/background-task-center.d.ts +9 -0
  23. package/dist/public/background-task-center.js +18 -0
  24. package/dist/public/chapter-search.d.ts +6 -0
  25. package/dist/public/chapter-search.js +23 -0
  26. package/dist/public/character-filters.d.ts +3 -1
  27. package/dist/public/character-filters.js +17 -2
  28. package/dist/public/character-version.js +1 -0
  29. package/dist/public/display-labels.d.ts +1 -0
  30. package/dist/public/display-labels.js +5 -1
  31. package/dist/public/index.html +66 -25
  32. package/dist/public/model-config.d.ts +3 -1
  33. package/dist/public/model-config.js +13 -0
  34. package/dist/public/relationship-filters.d.ts +5 -0
  35. package/dist/public/relationship-filters.js +31 -0
  36. package/dist/public/relationship-graph.js +289 -58
  37. package/dist/public/styles.css +170 -32
  38. package/dist/server-runtime.js +6 -0
  39. package/dist/server-runtime.js.map +1 -1
  40. package/dist/store.js +77 -34
  41. package/dist/store.js.map +1 -1
  42. package/dist/version.js +6 -1
  43. package/dist/version.js.map +1 -1
  44. 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 = this.db.all("SELECT id, content FROM chapters WHERE work_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", workId);
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, chapterId);
2499
- const existing = this.db.get(`SELECT id FROM analysis_tasks WHERE work_id = ? AND task_type = 'chapter-analysis' AND status = 'pending'
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.is_dead AS character_is_dead
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 `指定章节(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
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 `指定章节(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
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") })),