@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/ai.js +473 -78
- package/dist/ai.js.map +1 -1
- package/dist/app.js +89 -9
- package/dist/app.js.map +1 -1
- package/dist/database.js +19 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +169 -34
- package/dist/public/index.html +2 -2
- package/dist/public/styles.css +92 -9
- package/dist/store.js +851 -6
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +1 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 ? "全书 +
|
|
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 ? "全书 +
|
|
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) {
|