@musnows/scriverse 0.5.0 → 0.5.2

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/store.js CHANGED
@@ -3615,6 +3615,44 @@ export class Store {
3615
3615
  hashContent(content) {
3616
3616
  return createHash("sha256").update(content).digest("hex");
3617
3617
  }
3618
+ relationshipSettingsSourceVersions(workId) {
3619
+ const versions = {};
3620
+ const work = this.getWork(workId);
3621
+ versions[`work:${workId}`] = Number(work.versionNo);
3622
+ for (const setting of this.listSettings(workId))
3623
+ versions[`setting:${String(setting.id)}`] = Number(setting.versionNo);
3624
+ const characters = this.listCharacters(workId, true);
3625
+ for (const character of characters) {
3626
+ versions[`character:${String(character.id)}`] = Number(character.versionNo);
3627
+ for (const section of this.listCharacterProfileSections(String(character.id))) {
3628
+ versions[`character-section:${String(section.id)}`] = Number(section.versionNo);
3629
+ }
3630
+ }
3631
+ for (const race of this.listRaces(workId))
3632
+ versions[`race:${String(race.id)}`] = Number(race.versionNo);
3633
+ for (const organization of this.listOrganizations(workId)) {
3634
+ versions[`organization:${String(organization.id)}`] = Number(organization.versionNo);
3635
+ }
3636
+ for (const track of this.listTimelineTracks(workId))
3637
+ versions[`timeline-track:${String(track.id)}`] = Number(track.versionNo);
3638
+ for (const event of this.listTimelineEvents(workId))
3639
+ versions[`timeline-event:${String(event.id)}`] = Number(event.versionNo);
3640
+ for (const relationship of this.listRelationships(workId)) {
3641
+ versions[`relationship:${String(relationship.id)}`] = Number(relationship.versionNo);
3642
+ }
3643
+ for (const outline of this.listChapterOutlines(workId)) {
3644
+ const chapterId = String(outline.chapterId);
3645
+ versions[`chapter-meta:${chapterId}`] = Number(this.getChapter(chapterId).versionNo);
3646
+ versions[`chapter-outline:${chapterId}`] = this.currentEntityVersionNo("chapter-outline", chapterId);
3647
+ }
3648
+ for (const foreshadow of this.listForeshadows(workId)) {
3649
+ versions[`foreshadow:${String(foreshadow.id)}`] = Number(foreshadow.versionNo);
3650
+ }
3651
+ for (const review of this.listReviewItems(workId)) {
3652
+ versions[`review:${String(review.id)}`] = String(review.updatedAt);
3653
+ }
3654
+ return versions;
3655
+ }
3618
3656
  mapContinuationGuard(row) {
3619
3657
  return {
3620
3658
  id: requiredString(row, "id"),
@@ -3666,6 +3704,9 @@ export class Store {
3666
3704
  sourceVersions[String(chapter.id)] = Number(chapter.versionNo);
3667
3705
  }
3668
3706
  }
3707
+ else if (scope.type === "settings") {
3708
+ Object.assign(sourceVersions, this.relationshipSettingsSourceVersions(workId));
3709
+ }
3669
3710
  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)
3670
3711
  VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)`, taskId, workId, input.taskType, JSON.stringify(scope), JSON.stringify(sourceVersions), timestamp, timestamp, currentRequestActor()?.userId ?? null);
3671
3712
  this.audit(workId, "task.created", "analysis-task", taskId, { taskType: input.taskType, scope });
@@ -3702,10 +3743,13 @@ export class Store {
3702
3743
  };
3703
3744
  }
3704
3745
  getTask(taskId) {
3705
- const row = this.db.get("SELECT * FROM analysis_tasks WHERE id = ?", taskId);
3746
+ return this.mapTask(this.getTaskRow(taskId));
3747
+ }
3748
+ getTaskWorkId(taskId) {
3749
+ const row = this.db.get("SELECT work_id FROM analysis_tasks WHERE id = ?", taskId);
3706
3750
  if (!row)
3707
3751
  throw notFound("分析任务");
3708
- return this.mapTask(row);
3752
+ return requiredString(row, "work_id");
3709
3753
  }
3710
3754
  countRunningTasks(workId) {
3711
3755
  const row = this.db.get("SELECT COUNT(*) AS value FROM analysis_tasks WHERE work_id = ? AND status = 'running'", workId);
@@ -3725,6 +3769,13 @@ export class Store {
3725
3769
  const task = this.getTask(taskId);
3726
3770
  const scope = task.scope;
3727
3771
  const expected = task.sourceVersions;
3772
+ if (scope.type === "settings") {
3773
+ const current = this.relationshipSettingsSourceVersions(String(task.workId));
3774
+ const expectedIds = Object.keys(expected).sort();
3775
+ const currentIds = Object.keys(current).sort();
3776
+ return expectedIds.length === currentIds.length
3777
+ && expectedIds.every((settingId, index) => settingId === currentIds[index] && expected[settingId] === current[settingId]);
3778
+ }
3728
3779
  let chapters = [];
3729
3780
  if (typeof scope.chapterId === "string") {
3730
3781
  const row = this.db.get("SELECT id, work_id, version_no FROM chapters WHERE id = ?", scope.chapterId);
@@ -3751,6 +3802,13 @@ export class Store {
3751
3802
  return expectedIds.length === currentIds.length
3752
3803
  && expectedIds.every((chapterId, index) => chapterId === currentIds[index] && expected[chapterId] === current[chapterId]);
3753
3804
  }
3805
+ refreshTaskSourceVersions(taskId) {
3806
+ const task = this.getTask(taskId);
3807
+ const scope = task.scope;
3808
+ if (scope.type !== "settings")
3809
+ return;
3810
+ this.db.run("UPDATE analysis_tasks SET source_versions_json = ?, updated_at = ? WHERE id = ?", JSON.stringify(this.relationshipSettingsSourceVersions(String(task.workId))), now(), taskId);
3811
+ }
3754
3812
  cancelTask(taskId) {
3755
3813
  const current = this.getTask(taskId);
3756
3814
  if (current.status === "cancelled")
@@ -3771,21 +3829,784 @@ export class Store {
3771
3829
  this.db.run("UPDATE analysis_tasks SET status = ?, progress = ?, result_json = ?, failure_json = ?, updated_at = ? WHERE id = ?", input.status, input.progress ?? Number(current.progress), JSON.stringify(input.result ?? current.result), JSON.stringify(input.failures ?? current.failures), now(), taskId);
3772
3830
  return this.getTask(taskId);
3773
3831
  }
3832
+ getTaskStoredResult(taskId) {
3833
+ const row = this.getTaskRow(taskId, "result_json");
3834
+ return json(requiredString(row, "result_json"), {});
3835
+ }
3836
+ getTaskResultPayload(taskId) {
3837
+ const row = this.getTaskRow(taskId, "id, work_id, task_type, scope_json, result_json");
3838
+ return {
3839
+ id: requiredString(row, "id"),
3840
+ workId: requiredString(row, "work_id"),
3841
+ taskType: requiredString(row, "task_type"),
3842
+ scope: json(requiredString(row, "scope_json"), {}),
3843
+ result: this.taskResultForClient(json(requiredString(row, "result_json"), {}))
3844
+ };
3845
+ }
3846
+ getTaskDetail(taskId) {
3847
+ const row = this.getTaskRow(taskId);
3848
+ const task = this.mapTask(row);
3849
+ const { result: _result, ...detail } = task;
3850
+ const storedResultJson = requiredString(row, "result_json").trim();
3851
+ const hasResult = storedResultJson !== "" && storedResultJson !== "{}" && storedResultJson !== "null";
3852
+ return {
3853
+ ...detail,
3854
+ hasResult,
3855
+ resultSummary: this.buildTaskResultSummary(task, hasResult)
3856
+ };
3857
+ }
3858
+ getTaskRow(taskId, columns = "*") {
3859
+ const row = this.db.get(`SELECT ${columns} FROM analysis_tasks WHERE id = ?`, taskId);
3860
+ if (!row)
3861
+ throw notFound("分析任务");
3862
+ return row;
3863
+ }
3864
+ taskResultObjects(value) {
3865
+ return Array.isArray(value)
3866
+ ? value.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item))
3867
+ : [];
3868
+ }
3869
+ taskResultEvidence(value) {
3870
+ return this.taskResultObjects(value).map((item) => ({
3871
+ chapterId: String(item.chapterId ?? ""),
3872
+ chapterTitle: String(item.chapterTitle ?? ""),
3873
+ quote: String(item.quote ?? ""),
3874
+ supports: String(item.supports ?? item.conclusion ?? "")
3875
+ })).filter((item) => item.chapterId || item.chapterTitle || item.quote || item.supports).slice(0, 5);
3876
+ }
3877
+ taskResultItem(value, fallbackTitle) {
3878
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3879
+ return { title: String(value) };
3880
+ }
3881
+ if (!value || typeof value !== "object" || Array.isArray(value))
3882
+ return { title: fallbackTitle };
3883
+ const item = value;
3884
+ const firstText = (keys) => {
3885
+ for (const key of keys) {
3886
+ if (typeof item[key] === "string" && String(item[key]).trim())
3887
+ return String(item[key]).trim();
3888
+ }
3889
+ return "";
3890
+ };
3891
+ const title = firstText(["title", "name", "question", "canonicalName", "conclusion", "summary"]) || fallbackTitle;
3892
+ const subtitle = firstText(["category", "eventType", "itemType", "role", "type"]);
3893
+ const description = firstText(["description", "content", "conclusion", "reason", "identity", "suggestion", "supports"]);
3894
+ const tags = [item.tags, item.aliases, item.keywords, item.contradictions]
3895
+ .flatMap((candidate) => Array.isArray(candidate) ? candidate : [])
3896
+ .filter((candidate) => typeof candidate === "string" && Boolean(candidate.trim()))
3897
+ .map((candidate) => candidate.trim())
3898
+ .slice(0, 20);
3899
+ const detailFields = [
3900
+ ["严重程度", "severity"],
3901
+ ["发生时间", "timeLabel"],
3902
+ ["地点", "location"],
3903
+ ["影响范围", "impactScope"],
3904
+ ["当前状态", "currentStatus"],
3905
+ ["确认状态", "confirmationStatus"],
3906
+ ["处理状态", "status"],
3907
+ ["种族", "species"],
3908
+ ["身份", "identity"],
3909
+ ["修改建议", "suggestion"],
3910
+ ["原文引文", "quote"]
3911
+ ];
3912
+ const statusLabels = {
3913
+ active: "持续中",
3914
+ ongoing: "持续中",
3915
+ ended: "已结束",
3916
+ historical: "历史关系",
3917
+ pending: "待确认",
3918
+ confirmed: "已确认",
3919
+ rejected: "已否决",
3920
+ candidate: "候选",
3921
+ fixed: "已修复",
3922
+ ignored: "已忽略"
3923
+ };
3924
+ const valueLabels = {
3925
+ high: "高",
3926
+ medium: "中",
3927
+ low: "低",
3928
+ "character-duplicate": "角色重复",
3929
+ "setting-conflict": "设定冲突",
3930
+ consistency: "一致性问题",
3931
+ conflict: "冲突",
3932
+ other: "其他"
3933
+ };
3934
+ const details = detailFields.flatMap(([label, key]) => {
3935
+ const candidate = item[key];
3936
+ if (typeof candidate !== "string" || !candidate.trim())
3937
+ return [];
3938
+ const value = ["currentStatus", "confirmationStatus", "status"].includes(key)
3939
+ ? statusLabels[candidate.trim()] ?? candidate.trim()
3940
+ : valueLabels[candidate.trim()] ?? candidate.trim();
3941
+ return [{ label, value }];
3942
+ });
3943
+ if (typeof item.confidence === "number") {
3944
+ details.push({ label: "置信度", value: `${Math.round(item.confidence * 100)}%` });
3945
+ }
3946
+ return {
3947
+ title,
3948
+ subtitle: valueLabels[subtitle] ?? subtitle,
3949
+ description,
3950
+ tags,
3951
+ details,
3952
+ evidence: this.taskResultEvidence(item.evidence)
3953
+ };
3954
+ }
3955
+ taskCharacterReferenceSummary(workId, reference) {
3956
+ const separator = reference.lastIndexOf("@");
3957
+ if (separator <= 0)
3958
+ return null;
3959
+ const characterId = reference.slice(0, separator);
3960
+ const versionNo = Number(reference.slice(separator + 1));
3961
+ if (!Number.isInteger(versionNo) || versionNo <= 0)
3962
+ return null;
3963
+ const version = this.db.get("SELECT work_id, snapshot_json FROM character_versions WHERE character_id = ? AND version_no = ?", characterId, versionNo);
3964
+ let character = null;
3965
+ if (version && optionalString(version, "work_id") === workId) {
3966
+ character = json(requiredString(version, "snapshot_json"), {});
3967
+ }
3968
+ else {
3969
+ try {
3970
+ const current = this.getCharacter(characterId);
3971
+ if (current.workId === workId)
3972
+ character = current;
3973
+ }
3974
+ catch {
3975
+ return null;
3976
+ }
3977
+ }
3978
+ if (!character)
3979
+ return null;
3980
+ const name = typeof character.name === "string" && character.name.trim() ? character.name.trim() : "分析时角色";
3981
+ const aliases = Array.isArray(character.aliases)
3982
+ ? character.aliases.filter((alias) => typeof alias === "string" && Boolean(alias.trim())).map((alias) => alias.trim())
3983
+ : [];
3984
+ const attributes = character.attributes && typeof character.attributes === "object" && !Array.isArray(character.attributes)
3985
+ ? character.attributes
3986
+ : {};
3987
+ const identity = typeof attributes.identity === "string" ? attributes.identity.trim() : "";
3988
+ const species = typeof character.species === "string" ? character.species.trim() : "";
3989
+ const basics = [
3990
+ aliases.length ? `别名:${aliases.join("、")}` : "",
3991
+ identity ? `身份:${identity}` : "",
3992
+ species ? `种族:${species}` : ""
3993
+ ].filter(Boolean);
3994
+ return { name, summary: basics.length ? `${name}(${basics.join(";")})` : name };
3995
+ }
3996
+ taskSkippedIdentityCandidate(value, fallbackTitle, workId) {
3997
+ if (!value || typeof value !== "object" || Array.isArray(value))
3998
+ return this.taskResultItem(value, fallbackTitle);
3999
+ const item = value;
4000
+ const references = typeof item.pair === "string" ? item.pair.split("|") : [];
4001
+ if (references.length !== 2)
4002
+ return this.taskResultItem(value, fallbackTitle);
4003
+ const left = this.taskCharacterReferenceSummary(workId, references[0] ?? "");
4004
+ const right = this.taskCharacterReferenceSummary(workId, references[1] ?? "");
4005
+ if (!left || !right)
4006
+ return this.taskResultItem(value, fallbackTitle);
4007
+ const reason = typeof item.reason === "string" && item.reason.trim() ? item.reason.trim() : "分析信息不足";
4008
+ return {
4009
+ title: `${left.name} ↔ ${right.name}`,
4010
+ subtitle: "未生成查重建议",
4011
+ description: `AI 未生成可提交审核的角色查重建议:${reason}`,
4012
+ tags: [],
4013
+ details: [
4014
+ { label: "候选角色一", value: left.summary },
4015
+ { label: "候选角色二", value: right.summary },
4016
+ { label: "未生成原因", value: reason }
4017
+ ],
4018
+ evidence: []
4019
+ };
4020
+ }
4021
+ buildTaskResultSummary(task, hasResult) {
4022
+ const taskType = String(task.taskType);
4023
+ const workId = String(task.workId);
4024
+ const result = task.result && typeof task.result === "object" && !Array.isArray(task.result)
4025
+ ? task.result
4026
+ : {};
4027
+ const labelByType = {
4028
+ "chapter-analysis": "章节理解",
4029
+ "character-extraction": "全书角色抽取",
4030
+ "character-summary": "全书角色抽取",
4031
+ "character-identity-audit": "AI 角色查重",
4032
+ "timeline-analysis": "时间轴与事件抽取",
4033
+ "relationship-analysis": "人物关系分析",
4034
+ "worldview-analysis": "世界观分析",
4035
+ "setting-extraction": "设定抽取",
4036
+ "consistency-check": "一致性校对",
4037
+ "book-analysis": "全书综合分析",
4038
+ structure: "结构分析",
4039
+ "report-update": "报告更新"
4040
+ };
4041
+ const analysisLabel = labelByType[taskType] ?? "AI 分析";
4042
+ const taskStorage = {
4043
+ label: "完整任务结果",
4044
+ entity: "AI 分析记录",
4045
+ key: "analysis-record",
4046
+ count: hasResult ? 1 : 0,
4047
+ note: "完整返回 JSON 保存在本次 AI 分析记录中,可按需查看。"
4048
+ };
4049
+ const productStorageTargets = (targets) => targets.map((target) => ({
4050
+ label: String(target.label ?? target.entity ?? "分析结果"),
4051
+ location: `当前作品 · ${String(target.entity ?? "AI 分析记录")}`,
4052
+ count: Number(target.count ?? 0),
4053
+ ...(typeof target.note === "string" && target.note.trim() ? { note: target.note.trim() } : {})
4054
+ }));
4055
+ if (!hasResult) {
4056
+ return {
4057
+ title: `${analysisLabel}结果`,
4058
+ analysisContent: `${analysisLabel};范围:${String(task.scopeSummary ?? "未指定")}`,
4059
+ summary: "任务尚未产生分析结果。",
4060
+ metrics: [],
4061
+ storageTargets: productStorageTargets([taskStorage]),
4062
+ sections: []
4063
+ };
4064
+ }
4065
+ const metric = (label, value) => ({ label, value: Number(value ?? 0) });
4066
+ const section = (title, values, emptyMessage) => {
4067
+ const source = Array.isArray(values) ? values : [];
4068
+ return {
4069
+ title,
4070
+ totalCount: source.length,
4071
+ items: source.slice(0, 100).map((item, index) => this.taskResultItem(item, `${title} ${index + 1}`)),
4072
+ emptyMessage
4073
+ };
4074
+ };
4075
+ const idList = (value) => Array.isArray(value)
4076
+ ? value.filter((item) => typeof item === "string")
4077
+ : [];
4078
+ const resultFieldLabels = {
4079
+ sourceChapterCount: "分析章节",
4080
+ sourceChunkCount: "分析分段",
4081
+ eventCount: "时间轴事件",
4082
+ resumable: "可继续处理",
4083
+ completedChunkCount: "已完成分段",
4084
+ chunkResults: "分段结果",
4085
+ consolidationResults: "汇总结果",
4086
+ semanticReviewResults: "语义复核结果",
4087
+ relationships: "审核关系",
4088
+ confirmed: "已确认",
4089
+ pending: "待确认",
4090
+ characters: "角色档案",
4091
+ timelineEvents: "时间轴事件",
4092
+ foreshadows: "伏笔",
4093
+ races: "种族",
4094
+ organizations: "组织",
4095
+ removedForeshadows: "已移除伏笔",
4096
+ correctedRaceClassification: "种族分类修正",
4097
+ timelineTypoFixed: "时间轴错字修正",
4098
+ ianAliasAdded: "补充角色别名",
4099
+ fixedRelationships: "修正人物关系",
4100
+ semanticCorrections: "关系语义修正",
4101
+ mergedCanonicalIds: "合并规范关系",
4102
+ mergedCharacterId: "合并后角色",
4103
+ removedDuplicateCharacterId: "移除重复角色",
4104
+ settingIds: "设定",
4105
+ settings: "设定",
4106
+ raceIds: "种族",
4107
+ organizationIds: "组织",
4108
+ timelineEventIds: "时间轴事件",
4109
+ eventIds: "时间轴事件",
4110
+ foreshadowIds: "伏笔",
4111
+ correctedCharacterIds: "修正角色",
4112
+ removedDuplicateCharacterIds: "移除重复角色"
4113
+ };
4114
+ const resultFieldLabel = (key) => resultFieldLabels[key] ?? key;
4115
+ const resultRecord = (value) => value && typeof value === "object" && !Array.isArray(value)
4116
+ ? value
4117
+ : {};
4118
+ const genericMetrics = (source) => {
4119
+ const ignored = new Set(["content", "bookSummary", "callId", "callIds", "summarySettingId", "summarySuggestionId"]);
4120
+ const values = [];
4121
+ const labels = new Set();
4122
+ const append = (key, value) => {
4123
+ if (ignored.has(key) || key.endsWith("Id"))
4124
+ return;
4125
+ const label = resultFieldLabel(key);
4126
+ if (labels.has(label))
4127
+ return;
4128
+ let displayValue;
4129
+ if (typeof value === "number")
4130
+ displayValue = value;
4131
+ else if (typeof value === "boolean")
4132
+ displayValue = value ? "是" : "否";
4133
+ else if (typeof value === "string" && value.trim() && value.length <= 100)
4134
+ displayValue = value.trim();
4135
+ else if (Array.isArray(value) && key !== "callIds")
4136
+ displayValue = value.length;
4137
+ else
4138
+ return;
4139
+ labels.add(label);
4140
+ values.push({ label, value: displayValue });
4141
+ };
4142
+ for (const [key, value] of Object.entries(source)) {
4143
+ if (key === "counts") {
4144
+ for (const [countKey, countValue] of Object.entries(resultRecord(value)))
4145
+ append(countKey, countValue);
4146
+ }
4147
+ else
4148
+ append(key, value);
4149
+ }
4150
+ return values.slice(0, 12);
4151
+ };
4152
+ const genericSections = (source) => Object.entries(source).flatMap(([key, value]) => {
4153
+ if (!Array.isArray(value) || value.length === 0 || key === "callIds" || key === "fixedRelationships" || key.endsWith("Ids"))
4154
+ return [];
4155
+ return [section(resultFieldLabel(key), value, `没有可展示的${resultFieldLabel(key)}。`)];
4156
+ });
4157
+ const resolveIds = (ids, getter) => ids.flatMap((entityId) => {
4158
+ try {
4159
+ const entity = getter(entityId);
4160
+ return entity.workId === workId ? [entity] : [];
4161
+ }
4162
+ catch {
4163
+ return [];
4164
+ }
4165
+ });
4166
+ const storageTargets = [taskStorage];
4167
+ const addStorageTarget = (target) => {
4168
+ if (!storageTargets.some((item) => item.key === target.key))
4169
+ storageTargets.unshift(target);
4170
+ };
4171
+ let summary = "分析已完成。";
4172
+ let metrics = [];
4173
+ let sections = [];
4174
+ if (taskType === "chapter-analysis") {
4175
+ let chapterTitle = String(result.chapterId ?? "指定章节");
4176
+ if (typeof result.chapterId === "string") {
4177
+ try {
4178
+ chapterTitle = String(this.getChapter(result.chapterId).title);
4179
+ }
4180
+ catch { /* 历史章节可能已删除 */ }
4181
+ }
4182
+ summary = typeof result.summary === "string" && result.summary.trim() ? result.summary.trim() : "章节理解已生成。";
4183
+ metrics = [
4184
+ metric("事件", Array.isArray(result.events) ? result.events.length : 0),
4185
+ metric("出场角色", Array.isArray(result.characters) ? result.characters.length : 0),
4186
+ metric("设定", Array.isArray(result.settings) ? result.settings.length : 0),
4187
+ metric("不确定项", Array.isArray(result.uncertainties) ? result.uncertainties.length : 0)
4188
+ ];
4189
+ storageTargets.unshift({
4190
+ label: "章节理解记录",
4191
+ entity: chapterTitle,
4192
+ key: "chapter-insight",
4193
+ count: result.insightId ? 1 : 0,
4194
+ note: `对应章节版本 v${Number(result.chapterVersion ?? 0)}。`
4195
+ });
4196
+ sections = [
4197
+ section("情节事件", result.events, "没有提取到明确事件。"),
4198
+ section("出场角色", result.characters, "没有提取到角色信息。"),
4199
+ section("章节设定", result.settings, "没有提取到设定信息。"),
4200
+ section("原文依据", result.evidence, "没有可展示的原文依据。"),
4201
+ section("不确定项", result.uncertainties, "没有标记不确定项。")
4202
+ ];
4203
+ }
4204
+ else if (taskType === "timeline-analysis") {
4205
+ const ids = idList(result.eventIds);
4206
+ const events = ids.flatMap((eventId) => {
4207
+ try {
4208
+ const event = this.getTimelineEvent(eventId);
4209
+ return event.workId === workId ? [event] : [];
4210
+ }
4211
+ catch {
4212
+ return [];
4213
+ }
4214
+ });
4215
+ summary = `提取并写入 ${events.length} 个时间轴事件候选。`;
4216
+ metrics = [metric("写入事件", events.length), metric("已不存在", Math.max(0, ids.length - events.length))];
4217
+ storageTargets.unshift({ label: "时间轴候选", entity: "时间轴与事件", key: "timeline", count: events.length, note: "以候选状态写入,等待作者确认。" });
4218
+ sections = [section("事件候选", events, "没有形成可写入的时间轴事件。")];
4219
+ }
4220
+ else if (taskType === "worldview-analysis") {
4221
+ summary = typeof result.summary === "string" && result.summary.trim() ? result.summary.trim() : "世界观分析已完成。";
4222
+ metrics = [metric("世界观维度", result.dimensionCount), metric("冲突", Array.isArray(result.conflicts) ? result.conflicts.length : 0), metric("待确认问题", Array.isArray(result.uncertainties) ? result.uncertainties.length : 0), metric("覆盖章节", result.coveredChapterCount)];
4223
+ sections = [
4224
+ section("世界观结论", result.dimensions, "没有形成有证据支持的世界观结论。"),
4225
+ section("设定冲突", result.conflicts, "没有发现明确冲突。"),
4226
+ section("待确认问题", result.uncertainties, "没有标记待确认问题。")
4227
+ ];
4228
+ }
4229
+ else if (taskType === "setting-extraction") {
4230
+ const ids = idList(result.settingIds);
4231
+ const settings = ids.flatMap((settingId) => {
4232
+ try {
4233
+ const setting = this.getSetting(settingId);
4234
+ return setting.workId === workId ? [setting] : [];
4235
+ }
4236
+ catch {
4237
+ return [];
4238
+ }
4239
+ });
4240
+ summary = `识别 ${Number(result.rawCandidateCount ?? settings.length)} 个候选,写入 ${settings.length} 条设定。`;
4241
+ metrics = [metric("新建", result.createdCount), metric("更新", result.updatedCount), metric("跳过", Array.isArray(result.skipped) ? result.skipped.length : 0), metric("覆盖章节", result.coveredChapterCount)];
4242
+ storageTargets.unshift({ label: "设定候选", entity: "设定库", key: "settings", count: settings.length, note: "写入为待确认设定,不覆盖已确认或锁定内容。" });
4243
+ sections = [section("写入的设定", settings, "没有形成可写入的设定。"), section("未写入候选", result.skipped, "没有候选被跳过。")];
4244
+ }
4245
+ else if (taskType === "consistency-check" && !("reviewIds" in result)) {
4246
+ const relationshipCount = typeof result.relationships === "number" ? result.relationships : null;
4247
+ const confirmedCount = Number(result.confirmed ?? 0);
4248
+ const pendingCount = Number(result.pending ?? 0);
4249
+ const semanticCorrections = Array.isArray(result.semanticCorrections) ? result.semanticCorrections : [];
4250
+ const mergedRelationships = idList(result.mergedCanonicalIds);
4251
+ const fixedRelationships = idList(result.fixedRelationships);
4252
+ if (relationshipCount !== null) {
4253
+ summary = `审核 ${relationshipCount} 条人物关系,其中 ${confirmedCount} 条已确认、${pendingCount} 条待确认;完成 ${mergedRelationships.length} 组关系合并和 ${semanticCorrections.length} 项语义修正。`;
4254
+ }
4255
+ else {
4256
+ summary = "一致性校对已完成,以下是本次实际检查和修正的数据。";
4257
+ }
4258
+ metrics = genericMetrics(result);
4259
+ sections = genericSections(result);
4260
+ const relationshipChanges = mergedRelationships.length + fixedRelationships.length + semanticCorrections.length;
4261
+ if (relationshipChanges > 0) {
4262
+ addStorageTarget({
4263
+ label: "人物关系修正",
4264
+ entity: "人物关系库",
4265
+ key: "relationships",
4266
+ count: relationshipChanges,
4267
+ note: "数量按任务记录的合并、修正和语义校正项统计。"
4268
+ });
4269
+ }
4270
+ if (typeof result.mergedCharacterId === "string" || typeof result.removedDuplicateCharacterId === "string" || typeof result.ianAliasAdded === "string") {
4271
+ addStorageTarget({ label: "角色档案修正", entity: "角色库", key: "characters", count: 1, note: "包含角色合并、重复档案清理或别名补充。" });
4272
+ }
4273
+ if (result.timelineTypoFixed === true) {
4274
+ addStorageTarget({ label: "时间轴修正", entity: "时间轴与事件", key: "timeline", count: 1, note: "任务记录已修正时间轴文本。" });
4275
+ }
4276
+ if (typeof result.removedForeshadows === "number" && result.removedForeshadows > 0) {
4277
+ addStorageTarget({ label: "伏笔清理", entity: "伏笔库", key: "foreshadows", count: result.removedForeshadows, note: "任务记录已移除重复或无效伏笔。" });
4278
+ }
4279
+ if (result.correctedRaceClassification === true) {
4280
+ addStorageTarget({ label: "种族分类修正", entity: "种族库", key: "races", count: 1, note: "任务记录已修正种族分类。" });
4281
+ }
4282
+ }
4283
+ else if (taskType === "consistency-check" || taskType === "character-identity-audit") {
4284
+ const ids = idList(result.reviewIds);
4285
+ const reviews = ids.flatMap((reviewId) => {
4286
+ try {
4287
+ const review = this.getReviewItem(reviewId);
4288
+ return review.workId === workId ? [review] : [];
4289
+ }
4290
+ catch {
4291
+ return [];
4292
+ }
4293
+ });
4294
+ summary = taskType === "character-identity-audit"
4295
+ ? `检查 ${Number(result.characterCount ?? 0)} 个角色档案,生成 ${reviews.length} 条查重建议。`
4296
+ : `发现并写入 ${reviews.length} 条一致性审核事项。`;
4297
+ metrics = taskType === "character-identity-audit"
4298
+ ? [metric("角色档案", result.characterCount), metric("疑似重复", reviews.length), metric("跳过", Array.isArray(result.skipped) ? result.skipped.length : 0), metric("工具调用", result.toolCallCount)]
4299
+ : [metric("审核事项", reviews.length), metric("已不存在", Math.max(0, ids.length - reviews.length))];
4300
+ storageTargets.unshift({ label: "审核建议", entity: "审核中心", key: "reviews", count: reviews.length, note: "只生成待处理建议,不会自动修正文或合并角色。" });
4301
+ const skipped = Array.isArray(result.skipped) ? result.skipped : [];
4302
+ sections = [
4303
+ section(taskType === "character-identity-audit" ? "角色查重建议" : "一致性问题", reviews, "没有发现需要审核的问题。"),
4304
+ ...(taskType === "character-identity-audit" ? [{
4305
+ title: "未生成建议的候选",
4306
+ totalCount: skipped.length,
4307
+ items: skipped.slice(0, 100).map((item, index) => this.taskSkippedIdentityCandidate(item, `未生成建议的候选 ${index + 1}`, workId)),
4308
+ emptyMessage: "没有候选被跳过。"
4309
+ }] : [])
4310
+ ];
4311
+ }
4312
+ else if (taskType === "character-extraction" || taskType === "character-summary") {
4313
+ const ids = idList(result.characterIds);
4314
+ const characters = ids.flatMap((characterId) => {
4315
+ try {
4316
+ const character = this.getCharacter(characterId);
4317
+ if (character.workId !== workId)
4318
+ return [];
4319
+ const attributes = character.attributes && typeof character.attributes === "object" && !Array.isArray(character.attributes)
4320
+ ? character.attributes
4321
+ : {};
4322
+ const profile = character.profile && typeof character.profile === "object" && !Array.isArray(character.profile)
4323
+ ? character.profile
4324
+ : {};
4325
+ return [{
4326
+ ...character,
4327
+ identity: String(attributes.identity ?? ""),
4328
+ description: String(profile.summary ?? "")
4329
+ }];
4330
+ }
4331
+ catch {
4332
+ return [];
4333
+ }
4334
+ });
4335
+ summary = `识别 ${Number(result.candidateCount ?? characters.length)} 个角色候选,保存 ${characters.length} 个角色档案。`;
4336
+ const verification = result.verification && typeof result.verification === "object" && !Array.isArray(result.verification)
4337
+ ? result.verification
4338
+ : {};
4339
+ metrics = [metric("保存角色", characters.length), metric("跳过", Array.isArray(result.skipped) ? result.skipped.length : 0), metric("覆盖章节", result.coveredChapterCount), metric("身份复核", verification.pairCount)];
4340
+ storageTargets.unshift({ label: "角色档案", entity: "角色库", key: "characters", count: characters.length, note: "新角色会创建档案,命中已有角色时会合并可靠信息。" });
4341
+ sections = [section("保存的角色", characters, "没有形成可保存的角色档案。"), section("未写入候选", result.skipped, "没有候选被跳过。")];
4342
+ }
4343
+ else if (taskType === "book-analysis") {
4344
+ const timelineIds = [...new Set([...idList(result.eventIds), ...idList(result.timelineEventIds)])];
4345
+ const settingIds = [...new Set([
4346
+ ...idList(result.settingIds),
4347
+ ...(typeof result.summarySettingId === "string" ? [result.summarySettingId] : [])
4348
+ ])];
4349
+ const raceIds = idList(result.raceIds);
4350
+ const organizationIds = idList(result.organizationIds);
4351
+ const foreshadowIds = idList(result.foreshadowIds);
4352
+ const correctedCharacterIds = idList(result.correctedCharacterIds);
4353
+ const removedDuplicateCharacterIds = idList(result.removedDuplicateCharacterIds);
4354
+ const timelineEvents = resolveIds(timelineIds, (entityId) => this.getTimelineEvent(entityId));
4355
+ const settings = resolveIds(settingIds, (entityId) => this.getSetting(entityId));
4356
+ const races = resolveIds(raceIds, (entityId) => this.getRace(entityId, false));
4357
+ const organizations = resolveIds(organizationIds, (entityId) => this.getOrganization(entityId));
4358
+ const foreshadows = resolveIds(foreshadowIds, (entityId) => this.getForeshadow(entityId));
4359
+ const correctedCharacters = resolveIds(correctedCharacterIds, (entityId) => this.getCharacter(entityId));
4360
+ const bookSummary = resultRecord(result.bookSummary);
4361
+ const oneSentence = typeof bookSummary.oneSentence === "string" ? bookSummary.oneSentence.trim() : "";
4362
+ const content = typeof result.content === "string" ? result.content.trim() : "";
4363
+ if (oneSentence)
4364
+ summary = oneSentence;
4365
+ else if (timelineIds.length > 0) {
4366
+ summary = `分析 ${Number(result.sourceChapterCount ?? 0)} 章正文,重建并记录 ${timelineIds.length} 个时间轴事件;当前作品中仍可查看 ${timelineEvents.length} 个。`;
4367
+ }
4368
+ else if (correctedCharacterIds.length > 0 || removedDuplicateCharacterIds.length > 0) {
4369
+ summary = `核验人物档案后修正 ${correctedCharacterIds.length} 个角色,并移除 ${removedDuplicateCharacterIds.length} 个重复角色档案。`;
4370
+ }
4371
+ else if (Array.isArray(result.chunkResults)) {
4372
+ summary = `已完成 ${Number(result.completedChunkCount ?? result.chunkResults.length)}/${Number(result.sourceChunkCount ?? result.chunkResults.length)} 个正文分段的阶段分析${result.resumable === true ? ",当前结果可继续处理" : ""}。`;
4373
+ }
4374
+ else if (content)
4375
+ summary = content;
4376
+ else
4377
+ summary = "全书综合分析已完成,以下展示任务记录的统计、结论和写入数据。";
4378
+ metrics = genericMetrics(result);
4379
+ if (timelineIds.length > 0) {
4380
+ addStorageTarget({ label: "时间轴事件", entity: "时间轴与事件", key: "timeline", count: timelineIds.length, note: `任务记录 ${timelineIds.length} 个事件,当前仍可查看 ${timelineEvents.length} 个。` });
4381
+ }
4382
+ if (settingIds.length > 0) {
4383
+ addStorageTarget({ label: "世界设定", entity: "设定库", key: "settings", count: settingIds.length, note: `任务记录 ${settingIds.length} 个设定,当前仍可查看 ${settings.length} 个。` });
4384
+ }
4385
+ if (raceIds.length > 0) {
4386
+ addStorageTarget({ label: "种族资料", entity: "种族库", key: "races", count: raceIds.length, note: `任务记录 ${raceIds.length} 个种族,当前仍可查看 ${races.length} 个。` });
4387
+ }
4388
+ if (organizationIds.length > 0) {
4389
+ addStorageTarget({ label: "组织资料", entity: "组织库", key: "organizations", count: organizationIds.length, note: `任务记录 ${organizationIds.length} 个组织,当前仍可查看 ${organizations.length} 个。` });
4390
+ }
4391
+ if (foreshadowIds.length > 0) {
4392
+ addStorageTarget({ label: "伏笔资料", entity: "伏笔库", key: "foreshadows", count: foreshadowIds.length, note: `任务记录 ${foreshadowIds.length} 个伏笔,当前仍可查看 ${foreshadows.length} 个。` });
4393
+ }
4394
+ if (typeof result.summarySuggestionId === "string") {
4395
+ addStorageTarget({ label: "全书分析建议", entity: "AI 建议", key: "suggestions", count: 1, note: "保存全书概要对应的分析建议。" });
4396
+ }
4397
+ if (correctedCharacterIds.length > 0 || removedDuplicateCharacterIds.length > 0) {
4398
+ addStorageTarget({
4399
+ label: "角色档案核验",
4400
+ entity: "角色库",
4401
+ key: "characters",
4402
+ count: correctedCharacterIds.length + removedDuplicateCharacterIds.length,
4403
+ note: `修正 ${correctedCharacterIds.length} 个角色,移除 ${removedDuplicateCharacterIds.length} 个重复档案;当前可读取 ${correctedCharacters.length} 个修正后角色。`
4404
+ });
4405
+ }
4406
+ sections = [];
4407
+ if (Object.keys(bookSummary).length > 0) {
4408
+ const overviewDetails = [
4409
+ ["剧情概要", bookSummary.synopsis],
4410
+ ["当前故事状态", bookSummary.endingState]
4411
+ ].flatMap(([label, value]) => typeof value === "string" && value.trim() ? [{ label, value: value.trim() }] : []);
4412
+ sections.push({
4413
+ title: "全书概要",
4414
+ totalCount: 1,
4415
+ items: [{
4416
+ title: String(bookSummary.title ?? "全书概要"),
4417
+ description: oneSentence,
4418
+ details: overviewDetails,
4419
+ tags: Array.isArray(bookSummary.themes) ? bookSummary.themes.map(String).slice(0, 20) : [],
4420
+ evidence: []
4421
+ }],
4422
+ emptyMessage: "没有可展示的全书概要。"
4423
+ });
4424
+ const volumeSummaries = this.taskResultObjects(bookSummary.volumeSummaries).map((item) => ({
4425
+ title: String(item.volumeTitle ?? item.title ?? "未命名分卷"),
4426
+ description: String(item.summary ?? ""),
4427
+ tags: Array.isArray(item.turningPoints) ? item.turningPoints.map(String).slice(0, 20) : []
4428
+ }));
4429
+ sections.push(section("分卷摘要", volumeSummaries, "没有可展示的分卷摘要。"), section("主要故事线", bookSummary.mainArcs, "没有可展示的主要故事线。"), section("未解决问题", bookSummary.unresolvedQuestions, "没有标记未解决问题。"), section("原文依据", bookSummary.evidence, "没有可展示的原文依据。"));
4430
+ }
4431
+ if (timelineIds.length > 0)
4432
+ sections.push(section("写入的时间轴事件", timelineEvents, "任务记录的时间轴事件当前均已不存在。"));
4433
+ if (settingIds.length > 0)
4434
+ sections.push(section("写入的设定", settings, "任务记录的设定当前均已不存在。"));
4435
+ if (raceIds.length > 0)
4436
+ sections.push(section("写入的种族", races, "任务记录的种族当前均已不存在。"));
4437
+ if (organizationIds.length > 0)
4438
+ sections.push(section("写入的组织", organizations, "任务记录的组织当前均已不存在。"));
4439
+ if (foreshadowIds.length > 0)
4440
+ sections.push(section("写入的伏笔", foreshadows, "任务记录的伏笔当前均已不存在。"));
4441
+ if (correctedCharacterIds.length > 0)
4442
+ sections.push(section("修正后的角色", correctedCharacters, "任务记录的修正角色当前均已不存在。"));
4443
+ if (Array.isArray(result.evidence))
4444
+ sections.push(section("核验依据", result.evidence, "没有可展示的核验依据。"));
4445
+ }
4446
+ else if (taskType === "relationship-analysis") {
4447
+ const relationshipIds = idList(result.relationshipIds);
4448
+ const missingRelationshipIds = idList(result.missingRelationshipIds);
4449
+ const relationships = this.taskResultObjects(result.relationshipResults).map((relationship) => {
4450
+ const actionLabels = { created: "已新建", updated: "已更新", unchanged: "已保留原记录" };
4451
+ const categoryLabels = { family: "亲属", social: "社交", emotional: "情感", conflict: "冲突", uncertain: "未确定" };
4452
+ const statusLabels = { active: "持续中", ongoing: "持续中", ended: "已结束", historical: "历史关系" };
4453
+ const confirmationLabels = { pending: "待确认", confirmed: "已确认", rejected: "已否决" };
4454
+ const fromName = String(relationship.fromCharacterName ?? relationship.fromCharacterId ?? "未知人物");
4455
+ const toName = String(relationship.toCharacterName ?? relationship.toCharacterId ?? "未知人物");
4456
+ const keywords = Array.isArray(relationship.keywords) ? relationship.keywords.map(String) : [];
4457
+ return {
4458
+ title: `${fromName} ${relationship.directed ? "→" : "↔"} ${toName}`,
4459
+ subtitle: `${categoryLabels[String(relationship.category)] ?? String(relationship.category ?? "其他关系")} / ${String(relationship.subtype ?? "未细分")} · ${actionLabels[String(relationship.action)] ?? "已处理"}`,
4460
+ description: [String(relationship.subtype ?? ""), keywords.join("、")].filter(Boolean).join(";"),
4461
+ tags: keywords,
4462
+ details: [
4463
+ { label: "当前状态", value: statusLabels[String(relationship.currentStatus)] ?? String(relationship.currentStatus ?? "未说明") },
4464
+ { label: "置信度", value: `${Math.round(Number(relationship.confidence ?? 0) * 100)}%` },
4465
+ { label: "确认状态", value: confirmationLabels[String(relationship.confirmationStatus)] ?? String(relationship.confirmationStatus ?? "待确认") }
4466
+ ],
4467
+ evidence: this.taskResultEvidence(relationship.evidence)
4468
+ };
4469
+ });
4470
+ const analysisTarget = result.analysisTarget && typeof result.analysisTarget === "object" && !Array.isArray(result.analysisTarget)
4471
+ ? result.analysisTarget
4472
+ : {};
4473
+ const targetNames = Array.isArray(analysisTarget.characterNames) ? analysisTarget.characterNames.map(String) : [];
4474
+ const targetSummary = analysisTarget.mode === "targeted-characters" && targetNames.length
4475
+ ? `重点分析 ${targetNames.join("、")} 与其他已建档人物之间的关系`
4476
+ : "分析范围内已建档人物之间的长期关系";
4477
+ summary = missingRelationshipIds.length > 0
4478
+ ? `${targetSummary}。任务结果记录 ${relationshipIds.length} 条关系,当前作品中保留 ${relationships.length} 条可展示关系,另有 ${missingRelationshipIds.length} 条已删除或合并。`
4479
+ : `${targetSummary},共形成 ${relationships.length} 条可展示结果。`;
4480
+ const actionMetrics = [
4481
+ ["新建", result.createdCount],
4482
+ ["更新", result.updatedCount],
4483
+ ["保留", result.unchangedCount]
4484
+ ].flatMap(([label, value]) => typeof value === "number" ? [metric(String(label), value)] : []);
4485
+ metrics = [
4486
+ ...actionMetrics,
4487
+ metric("任务记录", relationshipIds.length || relationships.length),
4488
+ metric("当前可展示", relationships.length),
4489
+ metric("已删除或合并", missingRelationshipIds.length),
4490
+ metric("跳过", Array.isArray(result.skipped) ? result.skipped.length : 0)
4491
+ ];
4492
+ storageTargets.unshift({
4493
+ label: "人物关系",
4494
+ entity: "人物关系库",
4495
+ key: "relationships",
4496
+ count: relationshipIds.length || relationships.length,
4497
+ note: `任务记录 ${relationshipIds.length || relationships.length} 条关系,当前可读取 ${relationships.length} 条;关系候选需由作者确认。`
4498
+ });
4499
+ sections = [
4500
+ { title: "分析出的关系", totalCount: relationships.length, items: relationships.slice(0, 100), emptyMessage: "没有形成可展示的人物关系。" },
4501
+ section("未写入候选", result.skipped, "没有候选被跳过。")
4502
+ ];
4503
+ }
4504
+ else {
4505
+ summary = typeof result.content === "string" && result.content.trim() ? result.content.trim() : "分析已完成,未生成可结构化展示的结论。";
4506
+ metrics = [];
4507
+ sections = [];
4508
+ }
4509
+ return {
4510
+ title: `${analysisLabel}结果`,
4511
+ analysisContent: `${analysisLabel};范围:${String(task.scopeSummary ?? "未指定")}`,
4512
+ summary,
4513
+ metrics,
4514
+ storageTargets: productStorageTargets(storageTargets),
4515
+ sections
4516
+ };
4517
+ }
4518
+ taskResultForClient(result) {
4519
+ const sanitize = (value) => {
4520
+ if (Array.isArray(value))
4521
+ return value.map(sanitize);
4522
+ if (!value || typeof value !== "object")
4523
+ return value;
4524
+ return Object.fromEntries(Object.entries(value)
4525
+ .filter(([key]) => !["storageTarget", "database", "table", "taskResultTable"].includes(key))
4526
+ .map(([key, nestedValue]) => [key, sanitize(nestedValue)]));
4527
+ };
4528
+ return sanitize(result);
4529
+ }
4530
+ enrichRelationshipTaskResult(workId, taskType, scope, result) {
4531
+ if (taskType !== "relationship-analysis")
4532
+ return result;
4533
+ const relationshipIds = Array.isArray(result.relationshipIds)
4534
+ ? result.relationshipIds.filter((value) => typeof value === "string")
4535
+ : [];
4536
+ let relationshipResults = Array.isArray(result.relationshipResults) ? result.relationshipResults : null;
4537
+ let missingRelationshipIds = [];
4538
+ if (relationshipResults === null && relationshipIds.length > 0) {
4539
+ const requestedIds = new Set(relationshipIds);
4540
+ const rows = this.db.all(`SELECT relationship.*, source.name AS from_character_name, target.name AS to_character_name
4541
+ FROM relationships relationship
4542
+ JOIN characters source ON source.id = relationship.from_character_id
4543
+ JOIN characters target ON target.id = relationship.to_character_id
4544
+ WHERE relationship.work_id = ?`, workId).filter((row) => requestedIds.has(requiredString(row, "id")));
4545
+ const foundIds = new Set(rows.map((row) => requiredString(row, "id")));
4546
+ missingRelationshipIds = relationshipIds.filter((relationshipId) => !foundIds.has(relationshipId));
4547
+ relationshipResults = rows.map((row) => {
4548
+ const evidence = json(requiredString(row, "evidence_json"), []);
4549
+ return {
4550
+ relationshipId: requiredString(row, "id"),
4551
+ action: "created",
4552
+ snapshotSource: "current-record",
4553
+ fromCharacterId: requiredString(row, "from_character_id"),
4554
+ fromCharacterName: requiredString(row, "from_character_name"),
4555
+ toCharacterId: requiredString(row, "to_character_id"),
4556
+ toCharacterName: requiredString(row, "to_character_name"),
4557
+ category: requiredString(row, "category"),
4558
+ subtype: requiredString(row, "subtype"),
4559
+ keywords: json(requiredString(row, "keywords_json"), []),
4560
+ directed: booleanValue(row, "directed"),
4561
+ currentStatus: requiredString(row, "current_status"),
4562
+ timeRange: json(requiredString(row, "time_range_json"), {}),
4563
+ confidence: numberValue(row, "confidence"),
4564
+ confirmationStatus: requiredString(row, "confirmation_status"),
4565
+ evidenceCount: evidence.length,
4566
+ evidence: evidence.slice(0, 3).map((item) => ({
4567
+ chapterId: String(item.chapterId ?? ""),
4568
+ chapterTitle: String(item.chapterTitle ?? ""),
4569
+ quote: String(item.quote ?? ""),
4570
+ supports: String(item.supports ?? "")
4571
+ })),
4572
+ evidenceTruncated: evidence.length > 3
4573
+ };
4574
+ });
4575
+ }
4576
+ const targetCharacters = Array.isArray(scope.targetCharacters)
4577
+ ? scope.targetCharacters.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item))
4578
+ : [];
4579
+ return this.taskResultForClient({
4580
+ ...result,
4581
+ ...(relationshipResults === null ? {} : { relationshipResults }),
4582
+ ...(missingRelationshipIds.length > 0 ? { missingRelationshipIds } : {}),
4583
+ analysisTarget: result.analysisTarget ?? {
4584
+ mode: targetCharacters.length > 0 ? "targeted-characters" : "all-relationships",
4585
+ scopeType: String(scope.type ?? "book"),
4586
+ characterIds: targetCharacters.map((character) => String(character.id ?? "")).filter(Boolean),
4587
+ characterNames: targetCharacters.map((character) => String(character.name ?? "")).filter(Boolean),
4588
+ coveredChapterCount: Number(result.coveredChapterCount ?? 0),
4589
+ includeAllSettings: scope.includeAllSettings === true
4590
+ },
4591
+ });
4592
+ }
3774
4593
  mapTask(row) {
3775
4594
  const workId = requiredString(row, "work_id");
4595
+ const taskType = requiredString(row, "task_type");
3776
4596
  const scope = json(requiredString(row, "scope_json"), {});
3777
4597
  const characterNames = this.taskCharacterNames(workId, [scope]);
4598
+ const taskResult = this.enrichRelationshipTaskResult(workId, taskType, scope, json(requiredString(row, "result_json"), {}));
3778
4599
  return {
3779
4600
  id: requiredString(row, "id"),
3780
4601
  workId,
3781
- taskType: requiredString(row, "task_type"),
4602
+ taskType,
3782
4603
  scope,
3783
4604
  scopeSummary: this.taskScopeSummary(workId, scope, characterNames),
3784
4605
  scopeSummaryWithoutCharacterNames: this.taskScopeSummary(workId, scope, new Map(), false),
3785
4606
  scopeDetails: this.taskScopeDetails(workId, scope),
3786
4607
  status: requiredString(row, "status"),
3787
4608
  progress: numberValue(row, "progress"),
3788
- result: json(requiredString(row, "result_json"), {}),
4609
+ result: taskResult,
3789
4610
  failures: json(requiredString(row, "failure_json"), []),
3790
4611
  sourceVersions: json(requiredString(row, "source_versions_json"), {}),
3791
4612
  createdAt: requiredString(row, "created_at"),
@@ -3813,8 +4634,16 @@ export class Store {
3813
4634
  const title = volumeTitles.get(scope.volumeId);
3814
4635
  return `${title ? `分卷 · ${title}` : "分卷已删除"}${targetedSuffix}`;
3815
4636
  }
4637
+ if (scope.type === "settings")
4638
+ return `仅设定集${targetedSuffix}`;
3816
4639
  if (scope.type === "book" || Object.keys(scope).length === 0)
3817
- return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
4640
+ return `${scope.includeAllSettings === true ? "全书 + 设定集" : "全书"}${targetedSuffix}`;
4641
+ if (scope.type === "selection" && typeof scope.selection === "string" && scope.selection.trim()) {
4642
+ const selection = scope.selection.trim().replace(/\s+/gu, " ");
4643
+ return `选定内容:${selection.slice(0, 80)}${selection.length > 80 ? "……" : ""}${targetedSuffix}`;
4644
+ }
4645
+ if (scope.type === "none")
4646
+ return `无上下文${targetedSuffix}`;
3818
4647
  return "未指定范围";
3819
4648
  }
3820
4649
  taskScopeSummary(workId, scope, characterNames, includeCharacterNames = true) {
@@ -3834,8 +4663,16 @@ export class Store {
3834
4663
  const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
3835
4664
  return `${volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除"}${targetedSuffix}`;
3836
4665
  }
4666
+ if (scope.type === "settings")
4667
+ return `仅设定集${targetedSuffix}`;
3837
4668
  if (scope.type === "book" || Object.keys(scope).length === 0)
3838
- return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
4669
+ return `${scope.includeAllSettings === true ? "全书 + 设定集" : "全书"}${targetedSuffix}`;
4670
+ if (scope.type === "selection" && typeof scope.selection === "string" && scope.selection.trim()) {
4671
+ const selection = scope.selection.trim().replace(/\s+/gu, " ");
4672
+ return `选定内容:${selection.slice(0, 80)}${selection.length > 80 ? "……" : ""}${targetedSuffix}`;
4673
+ }
4674
+ if (scope.type === "none")
4675
+ return `无上下文${targetedSuffix}`;
3839
4676
  return "未指定范围";
3840
4677
  }
3841
4678
  taskCharacterNames(workId, scopes) {
@@ -3919,6 +4756,14 @@ export class Store {
3919
4756
  if (scope.type === "book" || Object.keys(scope).length === 0) {
3920
4757
  return [{ type: "book", title: "全书" }];
3921
4758
  }
4759
+ if (scope.type === "settings") {
4760
+ return [{ type: "settings", title: "仅设定集" }];
4761
+ }
4762
+ if (scope.type === "selection" && typeof scope.selection === "string") {
4763
+ return [{ type: "selection", selection: scope.selection }];
4764
+ }
4765
+ if (scope.type === "none")
4766
+ return [{ type: "none" }];
3922
4767
  return [{ type: "unknown", scope }];
3923
4768
  }
3924
4769
  search(workId, query) {