Skip to content

Commit e907f1e

Browse files
author
Hermes Cron
committed
night: novel-import-export
1 parent 5c08bf9 commit e907f1e

1 file changed

Lines changed: 238 additions & 7 deletions

File tree

services/novel-decomposition/novelDecompositionStore.ts

Lines changed: 238 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,208 @@ const 读取数字 = (value: unknown, fallback: number): number => {
2929
return Number.isFinite(num) ? num : fallback;
3030
};
3131

32+
/**
33+
* 校验数据集结构完整性,返回错误信息列表
34+
*/
35+
const 校验分享数据集结构 = (raw: any, index: number): string[] => {
36+
const errors: string[] = [];
37+
const prefix = `数据集[${index + 1}]`;
38+
39+
if (!raw || typeof raw !== 'object') {
40+
errors.push(`${prefix}: 无效的数据集对象`);
41+
return errors;
42+
}
43+
44+
const id = 读取文本(raw?.id).trim();
45+
const 标题 = 读取文本(raw?.标题).trim();
46+
const 作品名 = 读取文本(raw?.作品名).trim();
47+
48+
if (!id) errors.push(`${prefix}: 缺少有效ID`);
49+
if (!标题 && !作品名) errors.push(`${prefix}: 缺少标题和作品名`);
50+
51+
// 校验章节列表结构
52+
const 章节列表 = raw?.章节列表;
53+
if (章节列表 !== undefined && !Array.isArray(章节列表)) {
54+
errors.push(`${prefix}: 章节列表必须是数组`);
55+
} else if (Array.isArray(章节列表)) {
56+
章节列表.forEach((chapter: any, ci: number) => {
57+
if (!chapter || typeof chapter !== 'object') {
58+
errors.push(`${prefix}.章节[${ci}]: 无效的章节对象`);
59+
} else if (!读取文本(chapter?.id).trim()) {
60+
errors.push(`${prefix}.章节[${ci}]: 缺少有效ID`);
61+
}
62+
});
63+
}
64+
65+
// 校验分段列表结构
66+
const 分段列表 = raw?.分段列表;
67+
if (分段列表 !== undefined && !Array.isArray(分段列表)) {
68+
errors.push(`${prefix}: 分段列表必须是数组`);
69+
} else if (Array.isArray(分段列表)) {
70+
分段列表.forEach((segment: any, si: number) => {
71+
if (!segment || typeof segment !== 'object') {
72+
errors.push(`${prefix}.分段[${si}]: 无效的分段对象`);
73+
} else if (!读取文本(segment?.id).trim()) {
74+
errors.push(`${prefix}.分段[${si}]: 缺少有效ID`);
75+
}
76+
});
77+
}
78+
79+
// 校验注入树结构
80+
const 注入树 = raw?.注入树;
81+
if (注入树 !== undefined && !Array.isArray(注入树)) {
82+
errors.push(`${prefix}: 注入树必须是数组`);
83+
} else if (Array.isArray(注入树)) {
84+
const validateNodes = (nodes: any[], path: string) => {
85+
nodes.forEach((node: any, ni: number) => {
86+
if (!node || typeof node !== 'object') {
87+
errors.push(`${path}[${ni}]: 无效的节点对象`);
88+
return;
89+
}
90+
if (!读取文本(node?.id).trim()) {
91+
errors.push(`${path}[${ni}]: 缺少有效ID`);
92+
}
93+
if (Array.isArray(node?.子节点)) {
94+
validateNodes(node.子节点, `${path}[${ni}].子节点`);
95+
}
96+
});
97+
};
98+
validateNodes(注入树, `${prefix}.注入树`);
99+
}
100+
101+
return errors;
102+
};
103+
104+
/**
105+
* 校验任务结构完整性
106+
*/
107+
const 校验分享任务结构 = (raw: any, index: number): string[] => {
108+
const errors: string[] = [];
109+
const prefix = `任务[${index + 1}]`;
110+
111+
if (!raw || typeof raw !== 'object') {
112+
errors.push(`${prefix}: 无效的任务对象`);
113+
return errors;
114+
}
115+
116+
if (!读取文本(raw?.数据集ID).trim()) {
117+
errors.push(`${prefix}: 缺少数据集ID`);
118+
}
119+
120+
// 校验已完成/失败分段ID列表是字符串数组
121+
const completedSegments = raw?.已完成分段ID列表;
122+
if (completedSegments !== undefined && !Array.isArray(completedSegments)) {
123+
errors.push(`${prefix}: 已完成分段ID列表必须是数组`);
124+
}
125+
126+
const failedSegments = raw?.失败分段ID列表;
127+
if (failedSegments !== undefined && !Array.isArray(failedSegments)) {
128+
errors.push(`${prefix}: 失败分段ID列表必须是数组`);
129+
}
130+
131+
return errors;
132+
};
133+
134+
/**
135+
* 对导入的旧版本数据进行兼容性修复
136+
*/
137+
const 兼容修复分享数据集 = (raw: any): any => {
138+
if (!raw || typeof raw !== 'object') return raw;
139+
140+
const version = Math.max(1, Math.floor(读取数字(raw?.schemaVersion, 1)));
141+
let fixed = { ...raw };
142+
143+
// v1->v2: 确保存在分段模式字段
144+
if (version < 2) {
145+
if (!fixed.分段模式) {
146+
fixed.分段模式 = 'single_chapter';
147+
}
148+
}
149+
150+
// v2->v3: 补全每批章数
151+
if (version < 3) {
152+
if (typeof fixed.每批章数 !== 'number' || fixed.每批章数 < 1) {
153+
fixed.每批章数 = 1;
154+
}
155+
}
156+
157+
// v3->v4: 补全时间线起点
158+
if (version < 4) {
159+
if (!fixed.默认时间线起点) {
160+
fixed.默认时间线起点 = 默认小说时间线起点;
161+
}
162+
fixed.是否识别原著时间线 = 读取布尔(fixed.是否识别原著时间线, false);
163+
}
164+
165+
// v4->v5: 补全核心角色摘要
166+
if (version < 5) {
167+
if (!Array.isArray(fixed.核心角色摘要)) {
168+
fixed.核心角色摘要 = [];
169+
}
170+
if (!Array.isArray(fixed.核心角色)) {
171+
fixed.核心角色 = [];
172+
}
173+
}
174+
175+
// v5->v6: 补全当前阶段概括
176+
if (version < 6) {
177+
if (!读取文本(fixed.当前阶段概括)) {
178+
fixed.当前阶段概括 = '';
179+
}
180+
}
181+
182+
// v6->v7: 确保注入树结构完整
183+
if (version < 7) {
184+
if (!Array.isArray(fixed.注入树)) {
185+
fixed.注入树 = [];
186+
}
187+
}
188+
189+
// 更新版本号
190+
fixed.schemaVersion = 小说拆分数据集版本;
191+
192+
return fixed;
193+
};
194+
195+
/**
196+
* 对导入的旧版本任务数据进行兼容性修复
197+
*/
198+
const 兼容修复分享任务 = (raw: any): any => {
199+
if (!raw || typeof raw !== 'object') return raw;
200+
201+
const fixed = { ...raw };
202+
203+
// 确保存在必要字段
204+
if (typeof fixed.后台运行 !== 'boolean') {
205+
fixed.后台运行 = true;
206+
}
207+
if (typeof fixed.自动续跑 !== 'boolean') {
208+
fixed.自动续跑 = true;
209+
}
210+
if (typeof fixed.单次处理批量 !== 'number' || fixed.单次处理批量 < 1) {
211+
fixed.单次处理批量 = 1;
212+
}
213+
if (typeof fixed.自动重试次数 !== 'number' || fixed.自动重试次数 < 0) {
214+
fixed.自动重试次数 = 0;
215+
}
216+
if (!Array.isArray(fixed.已完成分段ID列表)) {
217+
fixed.已完成分段ID列表 = [];
218+
}
219+
if (!Array.isArray(fixed.失败分段ID列表)) {
220+
fixed.失败分段ID列表 = [];
221+
}
222+
223+
// 标准化状态值
224+
fixed.状态 = ['queued', 'running', 'paused', 'completed', 'failed', 'cancelled', 'idle'].includes(fixed.状态)
225+
? fixed.状态
226+
: 'idle';
227+
fixed.当前阶段 = ['prepare', 'segmenting', 'processing', 'snapshotting', 'completed', 'failed', 'idle'].includes(fixed.当前阶段)
228+
? fixed.当前阶段
229+
: 'idle';
230+
231+
return fixed;
232+
};
233+
32234
const 标准化登场角色项文本 = (value: unknown): string => 读取文本(value)
33235
.replace(/\r\n/g, '\n')
34236
.split('\n')
@@ -1007,7 +1209,7 @@ export const 导入小说拆分分享数据 = async (
10071209
const schema = 读取文本(parsed?.schema).trim();
10081210
const version = Math.max(1, Math.floor(读取数字(parsed?.version, 1)));
10091211
if (schema !== 小说拆分分享格式标识) {
1010-
throw new Error(`导入失败:分解数据格式${schema || '未知类型'}不受支持。`);
1212+
throw new Error(`导入失败:分解数据格式"${schema || '未知类型'}"不受支持。`);
10111213
}
10121214
if (version > 小说拆分分享版本) {
10131215
throw new Error(`导入失败:分享包版本 ${version} 高于当前支持版本 ${小说拆分分享版本}。`);
@@ -1019,6 +1221,27 @@ export const 导入小说拆分分享数据 = async (
10191221

10201222
const sourceTasks = Array.isArray(parsed?.tasks) ? parsed.tasks : [];
10211223
const sourceSnapshots = Array.isArray(parsed?.snapshots) ? parsed.snapshots : [];
1224+
1225+
// 对源数据进行兼容修复和校验
1226+
const allValidationErrors: string[] = [];
1227+
const fixedSourceDatasets = sourceDatasets.map((raw: any, index: number) => {
1228+
const fixed = 兼容修复分享数据集(raw);
1229+
const errors = 校验分享数据集结构(fixed, index);
1230+
allValidationErrors.push(...errors);
1231+
return fixed;
1232+
});
1233+
1234+
const fixedSourceTasks = sourceTasks.map((raw: any, index: number) => {
1235+
const fixed = 兼容修复分享任务(raw);
1236+
const errors = 校验分享任务结构(fixed, index);
1237+
allValidationErrors.push(...errors);
1238+
return fixed;
1239+
});
1240+
1241+
// 如果有校验错误,记录警告但继续导入(兼容修复已尽可能修复问题)
1242+
if (allValidationErrors.length > 0) {
1243+
console.warn('[小说分解导入] 部分数据结构存在兼容性问题,已自动修复:', allValidationErrors);
1244+
}
10221245
let rawMap = new Map<string, 小说拆分分享原文项结构>();
10231246
if (options?.includeRawText !== false) {
10241247
const rawEntry = entries[manifest.rawFile || 小说拆分分享原文文件];
@@ -1050,7 +1273,7 @@ export const 导入小说拆分分享数据 = async (
10501273

10511274
sourceDatasets.forEach((rawDataset: any, datasetIndex: number) => {
10521275
const normalizedDataset = 合并小说拆分分享原文(
1053-
规范化小说拆分数据集(rawDataset),
1276+
规范化小说拆分数据集(fixedSourceDatasets[datasetIndex]),
10541277
rawMap.get(读取文本(rawDataset?.id).trim())
10551278
);
10561279
const nextDatasetId = 生成ID('novel_dataset');
@@ -1079,15 +1302,23 @@ export const 导入小说拆分分享数据 = async (
10791302
importedDatasetIds.push(nextDatasetId);
10801303

10811304
sourceTasks
1082-
.filter((item: any) => 读取文本(item?.数据集ID).trim() === normalizedDataset.id)
1083-
.forEach((rawTask: any) => {
1084-
const normalizedTask = 规范化小说拆分任务(rawTask);
1305+
.filter((item: any, taskIndex: number) => {
1306+
const fixedTask = fixedSourceTasks[taskIndex];
1307+
return 读取文本(item?.数据集ID).trim() === normalizedDataset.id;
1308+
})
1309+
.forEach((rawTask: any, taskArrayIndex: number) => {
1310+
// 找到在 sourceTasks 中的原始索引
1311+
const originalTaskIndex = sourceTasks.findIndex((t: any) =>
1312+
读取文本(t?.数据集ID).trim() === normalizedDataset.id
1313+
);
1314+
const fixedTask = originalTaskIndex >= 0 ? fixedSourceTasks[originalTaskIndex] : rawTask;
1315+
const normalizedTask = 规范化小说拆分任务(fixedTask);
10851316
nextTasks.unshift(规范化小说拆分任务({
10861317
...normalizedTask,
10871318
id: 生成ID('novel_task'),
10881319
数据集ID: nextDatasetId,
1089-
已完成分段ID列表: normalizedTask.已完成分段ID列表.map((item) => segmentIdMap.get(item) || item),
1090-
失败分段ID列表: normalizedTask.失败分段ID列表.map((item) => segmentIdMap.get(item) || item),
1320+
已完成分段ID列表: normalizedTask.已完成分段ID列表.map((item: string) => segmentIdMap.get(item) || item),
1321+
失败分段ID列表: normalizedTask.失败分段ID列表.map((item: string) => segmentIdMap.get(item) || item),
10911322
createdAt: Date.now(),
10921323
updatedAt: Date.now()
10931324
}));

0 commit comments

Comments
 (0)